-
Notifications
You must be signed in to change notification settings - Fork 51
[flight-booking-app] Add loading and error UI, add sleep and approval tools #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
2e32002
c2de9c7
4f468f7
3e75b24
774ba98
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import { bookingApprovalHook } from '@/workflows/chat/hooks/approval'; | ||
|
|
||
| export async function POST(request: Request) { | ||
| const { toolCallId, approved, comment } = await request.json(); | ||
| // Schema validation happens automatically | ||
| // Can throw a zod schema validation error, or a | ||
| await bookingApprovalHook.resume(toolCallId, { | ||
| approved, | ||
| comment, | ||
| }); | ||
| return Response.json({ success: true }); | ||
| } | ||
|
Comment on lines
+3
to
+12
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should just use seems simpler imo only use |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| 'use client'; | ||
| import { useState } from 'react'; | ||
| interface BookingApprovalProps { | ||
| toolCallId: string; | ||
| input?: { | ||
| flightNumber: string; | ||
| passengerName: string; | ||
| price: number; | ||
| }; | ||
| output?: string; | ||
| } | ||
| export function BookingApproval({ | ||
| toolCallId, | ||
| input, | ||
| output, | ||
| }: BookingApprovalProps) { | ||
| const [comment, setComment] = useState(''); | ||
| const [isSubmitting, setIsSubmitting] = useState(false); | ||
|
|
||
| // If we have output, the approval has been processed | ||
| if (output) { | ||
| try { | ||
| const json = JSON.parse(output) as { output: { value: string } }; | ||
| return ( | ||
| <div className="border rounded-lg p-4"> | ||
| <p className="text-sm text-muted-foreground">{json.output.value}</p> | ||
| </div> | ||
| ); | ||
| } catch (error) { | ||
| return ( | ||
| <div className="border rounded-lg p-4"> | ||
| <p className="text-sm text-muted-foreground"> | ||
| Error parsing approval result: {(error as Error).message} | ||
| </p> | ||
| </div> | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| const handleSubmit = async (approved: boolean) => { | ||
| setIsSubmitting(true); | ||
| try { | ||
vercel[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| await fetch('/api/hooks/approval', { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ toolCallId, approved, comment }), | ||
| }); | ||
| } finally { | ||
| setIsSubmitting(false); | ||
| } | ||
| }; | ||
| return ( | ||
| <div className="border rounded-lg p-4 space-y-4"> | ||
| <div className="space-y-2"> | ||
| <p className="font-medium">Approve this booking?</p> | ||
| <div className="text-sm text-muted-foreground"> | ||
| {input && ( | ||
| <> | ||
| <div>Flight: {input.flightNumber}</div> | ||
| <div>Passenger: {input.passengerName}</div> | ||
| <div>Price: ${input.price}</div> | ||
| </> | ||
| )} | ||
| </div> | ||
| </div> | ||
| <textarea | ||
| value={comment} | ||
| onChange={(e) => setComment(e.target.value)} | ||
| placeholder="Add a comment (optional)..." | ||
| className="w-full border rounded p-2 text-sm" | ||
| rows={2} | ||
| /> | ||
| <div className="flex gap-2"> | ||
| <button | ||
| type="button" | ||
| onClick={() => handleSubmit(true)} | ||
| disabled={isSubmitting} | ||
| className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50" | ||
| > | ||
| {isSubmitting ? 'Submitting...' : 'Approve'} | ||
| </button> | ||
| <button | ||
| type="button" | ||
| onClick={() => handleSubmit(false)} | ||
| disabled={isSubmitting} | ||
| className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 disabled:opacity-50" | ||
| > | ||
| {isSubmitting ? 'Submitting...' : 'Reject'} | ||
| </button> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { defineHook } from 'workflow'; | ||
| import { z } from 'zod'; | ||
|
|
||
| export const bookingApprovalHook = defineHook({ | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same comment re: using webhook instead :)
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. webhook should support zod schemas too with validation |
||
| schema: z.object({ | ||
| approved: z.boolean(), | ||
| comment: z.string().optional(), | ||
| }), | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| import { FatalError } from 'workflow'; | ||
| import { FatalError, sleep } from 'workflow'; | ||
| import { z } from 'zod'; | ||
| import { bookingApprovalHook } from '../hooks/approval'; | ||
|
|
||
| export const mockAirports: Record< | ||
| string, | ||
|
|
@@ -288,6 +289,30 @@ export async function checkBaggageAllowance({ | |
| }; | ||
| } | ||
|
|
||
| async function executeSleep({ durationMs }: { durationMs: number }) { | ||
| // Note: No "use step" here - sleep is a workflow-level function | ||
| await sleep(durationMs); | ||
| return { message: `Slept for ${durationMs}ms` }; | ||
| } | ||
|
|
||
| async function executeBookingApproval( | ||
| { | ||
| flightNumber, | ||
| passengerName, | ||
| }: { flightNumber: string; passengerName: string; price: number }, | ||
vercel[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { toolCallId }: { toolCallId: string } | ||
| ) { | ||
| // Note: No "use step" here - hooks are workflow-level primitives | ||
| // Use the toolCallId as the hook token so the UI can reference it | ||
| const hook = bookingApprovalHook.create({ token: toolCallId }); | ||
| // Workflow pauses here until the hook is resolved | ||
| const { approved, comment } = await hook; | ||
| if (!approved) { | ||
| return `Booking rejected: ${comment || 'No reason provided'}`; | ||
| } | ||
| return `Booking approved for ${passengerName} on flight ${flightNumber}${comment ? ` - Note: ${comment}` : ''}`; | ||
| } | ||
|
|
||
| // Tool definitions | ||
| export const flightBookingTools = { | ||
| searchFlights: { | ||
|
|
@@ -337,6 +362,22 @@ export const flightBookingTools = { | |
| }), | ||
| execute: checkBaggageAllowance, | ||
| }, | ||
| sleep: { | ||
| description: 'Pause execution for a specified duration', | ||
| inputSchema: z.object({ | ||
| durationMs: z.number().describe('Duration to sleep in milliseconds'), | ||
| }), | ||
| execute: executeSleep, | ||
| }, | ||
|
Comment on lines
+366
to
+372
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for future, we should really ship |
||
| bookingApproval: { | ||
| description: 'Request human approval before booking a flight', | ||
| inputSchema: z.object({ | ||
| flightNumber: z.string().describe('Flight number to book'), | ||
| passengerName: z.string().describe('Name of the passenger'), | ||
| price: z.number().describe('Total price of the booking'), | ||
| }), | ||
| execute: executeBookingApproval, | ||
| }, | ||
| }; | ||
|
|
||
| // System prompt | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.