From 4e9f33a1d53656cf9b76ee3d6100ddbf0b5fcf69 Mon Sep 17 00:00:00 2001 From: bracesproul Date: Wed, 5 Mar 2025 13:32:51 -0800 Subject: [PATCH] fix: finish hooking up accommodations flow --- agent/trip-planner/nodes/classify.ts | 11 +- agent/trip-planner/nodes/extraction.tsx | 7 +- agent/trip-planner/nodes/tools.tsx | 11 +- .../accommodations-list/index.tsx | 160 +++++++++++++++++- agent/uis/utils/get-tool-response.ts | 23 +++ 5 files changed, 196 insertions(+), 16 deletions(-) create mode 100644 agent/uis/utils/get-tool-response.ts diff --git a/agent/trip-planner/nodes/classify.ts b/agent/trip-planner/nodes/classify.ts index 05e2df8..ee82a7f 100644 --- a/agent/trip-planner/nodes/classify.ts +++ b/agent/trip-planner/nodes/classify.ts @@ -46,10 +46,13 @@ If they do NOT change their request details (or they never specified them), plea const humanMessage = `Here is the entire conversation so far:\n${formatMessages(state.messages)}`; - const response = await model.invoke([ - { role: "system", content: prompt }, - { role: "human", content: humanMessage }, - ]); + const response = await model.invoke( + [ + { role: "system", content: prompt }, + { role: "human", content: humanMessage }, + ], + { tags: ["langsmith:nostream"] }, + ); const classificationDetails = response.tool_calls?.[0]?.args as | z.infer diff --git a/agent/trip-planner/nodes/extraction.tsx b/agent/trip-planner/nodes/extraction.tsx index af68a8f..719997a 100644 --- a/agent/trip-planner/nodes/extraction.tsx +++ b/agent/trip-planner/nodes/extraction.tsx @@ -114,10 +114,9 @@ Extract only what is specified by the user. It is okay to leave fields blank if const extractionDetailsWithDefaults: TripDetails = { startDate, endDate, - numberOfGuests: - extractedDetails.numberOfGuests !== undefined - ? extractedDetails.numberOfGuests - : 2, + numberOfGuests: extractedDetails.numberOfGuests + ? extractedDetails.numberOfGuests + : 2, location: extractedDetails.location, }; diff --git a/agent/trip-planner/nodes/tools.tsx b/agent/trip-planner/nodes/tools.tsx index d906148..3a60fda 100644 --- a/agent/trip-planner/nodes/tools.tsx +++ b/agent/trip-planner/nodes/tools.tsx @@ -81,15 +81,16 @@ export async function callTools( const tripPlan = response.tool_calls?.[0]?.args as | z.infer | undefined; - if (!tripPlan) { + const toolCallId = response.tool_calls?.[0]?.id; + if (!tripPlan || !toolCallId) { throw new Error("No trip plan found"); } if (tripPlan.listAccommodations) { - ui.write( - "accommodations-list", - getAccommodationsListProps(state.tripDetails), - ); + ui.write("accommodations-list", { + toolCallId, + ...getAccommodationsListProps(state.tripDetails), + }); } if (tripPlan.bookAccommodation && tripPlan.accommodationName) { ui.write("book-accommodation", { diff --git a/agent/uis/trip-planner/accommodations-list/index.tsx b/agent/uis/trip-planner/accommodations-list/index.tsx index f7d373e..87e28f2 100644 --- a/agent/uis/trip-planner/accommodations-list/index.tsx +++ b/agent/uis/trip-planner/accommodations-list/index.tsx @@ -1,5 +1,10 @@ import "./index.css"; -import React from "react"; +import { v4 as uuidv4 } from "uuid"; +import { + useStreamContext, + type UIMessage, +} from "@langchain/langgraph-sdk/react-ui"; +import { useEffect, useState } from "react"; import { X } from "lucide-react"; import { Button } from "@/components/ui/button"; import { TripDetails } from "../../../trip-planner/types"; @@ -13,6 +18,9 @@ import { import { format } from "date-fns"; import { Accommodation } from "agent/types"; import { capitalizeSentence } from "../../../utils/capitalize"; +import { Message } from "@langchain/langgraph-sdk"; +import { getToolResponse } from "../../utils/get-tool-response"; +import { DO_NOT_RENDER_ID_PREFIX } from "@/lib/ensure-tool-responses"; const StarSVG = ({ fill = "white" }: { fill?: string }) => ( void; tripDetails: TripDetails; + onBook: (accommodation: Accommodation) => void; }) { const startDate = new Date(tripDetails.startDate); const endDate = new Date(tripDetails.endDate); @@ -127,7 +137,7 @@ function SelectedAccommodation({
+
+

Booked Accommodation

+ +
+

+
+
+
+ Address: +
+
+ + {accommodation.name}, {capitalizeSentence(accommodation.city)} + +
+ +
+ Rating: +
+
+ + + {accommodation.rating} + +
+ +
+ Dates: +
+
+ + {format(startDate, "MMM d, yyyy")} -{" "} + {format(endDate, "MMM d, yyyy")} + +
+ +
+ Guests: +
+
+ {tripDetails.numberOfGuests} +
+ +
+ Total Price: +
+
+ ${totalPrice.toLocaleString()} +
+
+
+
+ ); +} + export default function AccommodationsList({ + toolCallId, tripDetails, accommodations, }: { + toolCallId: string; tripDetails: TripDetails; accommodations: Accommodation[]; }) { - const [selectedAccommodation, setSelectedAccommodation] = React.useState< + const thread = useStreamContext< + { messages: Message[]; ui: UIMessage[] }, + { MetaType: { ui: UIMessage | undefined } } + >(); + + const [selectedAccommodation, setSelectedAccommodation] = useState< Accommodation | undefined >(); + const [accommodationBooked, setAccommodationBooked] = useState(false); + + useEffect(() => { + if (typeof window === "undefined" || accommodationBooked) return; + const toolResponse = getToolResponse(toolCallId, thread); + if (toolResponse) { + setAccommodationBooked(true); + try { + const parsedContent: { + accommodation: Accommodation; + tripDetails: TripDetails; + } = JSON.parse(toolResponse.content as string); + setSelectedAccommodation(parsedContent.accommodation); + } catch { + console.error("Failed to parse tool response content."); + } + } + }, []); + + function handleBookAccommodation(accommodation: Accommodation) { + const orderDetails = { + accommodation, + tripDetails, + }; + + thread.submit({ + messages: [ + { + type: "tool", + tool_call_id: toolCallId, + id: `${DO_NOT_RENDER_ID_PREFIX}${uuidv4()}`, + name: "trip-planner", + content: JSON.stringify(orderDetails), + }, + { + type: "human", + content: `Booked ${accommodation.name} for ${tripDetails.numberOfGuests}.`, + }, + ], + }); + + setAccommodationBooked(true); + if (selectedAccommodation?.id !== accommodation.id) { + setSelectedAccommodation(accommodation); + } + } + + if (accommodationBooked && selectedAccommodation) { + return ( + + ); + } else if (accommodationBooked) { + return
Successfully booked accommodation!
; + } if (selectedAccommodation) { return ( @@ -155,6 +308,7 @@ export default function AccommodationsList({ tripDetails={tripDetails} onHide={() => setSelectedAccommodation(undefined)} accommodation={selectedAccommodation} + onBook={handleBookAccommodation} /> ); } diff --git a/agent/uis/utils/get-tool-response.ts b/agent/uis/utils/get-tool-response.ts new file mode 100644 index 0000000..be5191c --- /dev/null +++ b/agent/uis/utils/get-tool-response.ts @@ -0,0 +1,23 @@ +import { + useStreamContext, + type UIMessage, +} from "@langchain/langgraph-sdk/react-ui"; +import { Message, ToolMessage } from "@langchain/langgraph-sdk"; + +type StreamContextType = ReturnType< + typeof useStreamContext< + { messages: Message[]; ui: UIMessage[] }, + { MetaType: { ui: UIMessage | undefined } } + > +>; + +export function getToolResponse( + toolCallId: string, + thread: StreamContextType, +): ToolMessage | undefined { + const toolResponse = thread.messages.findLast( + (message): message is ToolMessage => + message.type === "tool" && message.tool_call_id === toolCallId, + ); + return toolResponse; +}