fix: finish hooking up accommodations flow

This commit is contained in:
bracesproul
2025-03-05 13:32:51 -08:00
parent 6923c99c32
commit 4e9f33a1d5
5 changed files with 196 additions and 16 deletions

View File

@@ -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 humanMessage = `Here is the entire conversation so far:\n${formatMessages(state.messages)}`;
const response = await model.invoke([ const response = await model.invoke(
{ role: "system", content: prompt }, [
{ role: "human", content: humanMessage }, { role: "system", content: prompt },
]); { role: "human", content: humanMessage },
],
{ tags: ["langsmith:nostream"] },
);
const classificationDetails = response.tool_calls?.[0]?.args as const classificationDetails = response.tool_calls?.[0]?.args as
| z.infer<typeof schema> | z.infer<typeof schema>

View File

@@ -114,10 +114,9 @@ Extract only what is specified by the user. It is okay to leave fields blank if
const extractionDetailsWithDefaults: TripDetails = { const extractionDetailsWithDefaults: TripDetails = {
startDate, startDate,
endDate, endDate,
numberOfGuests: numberOfGuests: extractedDetails.numberOfGuests
extractedDetails.numberOfGuests !== undefined ? extractedDetails.numberOfGuests
? extractedDetails.numberOfGuests : 2,
: 2,
location: extractedDetails.location, location: extractedDetails.location,
}; };

View File

@@ -81,15 +81,16 @@ export async function callTools(
const tripPlan = response.tool_calls?.[0]?.args as const tripPlan = response.tool_calls?.[0]?.args as
| z.infer<typeof schema> | z.infer<typeof schema>
| undefined; | undefined;
if (!tripPlan) { const toolCallId = response.tool_calls?.[0]?.id;
if (!tripPlan || !toolCallId) {
throw new Error("No trip plan found"); throw new Error("No trip plan found");
} }
if (tripPlan.listAccommodations) { if (tripPlan.listAccommodations) {
ui.write( ui.write("accommodations-list", {
"accommodations-list", toolCallId,
getAccommodationsListProps(state.tripDetails), ...getAccommodationsListProps(state.tripDetails),
); });
} }
if (tripPlan.bookAccommodation && tripPlan.accommodationName) { if (tripPlan.bookAccommodation && tripPlan.accommodationName) {
ui.write("book-accommodation", { ui.write("book-accommodation", {

View File

@@ -1,5 +1,10 @@
import "./index.css"; 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 { X } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { TripDetails } from "../../../trip-planner/types"; import { TripDetails } from "../../../trip-planner/types";
@@ -13,6 +18,9 @@ import {
import { format } from "date-fns"; import { format } from "date-fns";
import { Accommodation } from "agent/types"; import { Accommodation } from "agent/types";
import { capitalizeSentence } from "../../../utils/capitalize"; 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 }) => ( const StarSVG = ({ fill = "white" }: { fill?: string }) => (
<svg <svg
@@ -63,10 +71,12 @@ function SelectedAccommodation({
accommodation, accommodation,
onHide, onHide,
tripDetails, tripDetails,
onBook,
}: { }: {
accommodation: Accommodation; accommodation: Accommodation;
onHide: () => void; onHide: () => void;
tripDetails: TripDetails; tripDetails: TripDetails;
onBook: (accommodation: Accommodation) => void;
}) { }) {
const startDate = new Date(tripDetails.startDate); const startDate = new Date(tripDetails.startDate);
const endDate = new Date(tripDetails.endDate); const endDate = new Date(tripDetails.endDate);
@@ -127,7 +137,7 @@ function SelectedAccommodation({
</div> </div>
</div> </div>
<Button <Button
onClick={() => console.log("Booked")} onClick={() => onBook(accommodation)}
variant="secondary" variant="secondary"
className="w-full bg-gray-800 text-white hover:bg-gray-900 cursor-pointer transition-colors ease-in-out duration-200" className="w-full bg-gray-800 text-white hover:bg-gray-900 cursor-pointer transition-colors ease-in-out duration-200"
> >
@@ -138,16 +148,159 @@ function SelectedAccommodation({
); );
} }
function BookedAccommodation({
accommodation,
tripDetails,
}: {
accommodation: Accommodation;
tripDetails: TripDetails;
}) {
const startDate = new Date(tripDetails.startDate);
const endDate = new Date(tripDetails.endDate);
const totalTripDurationDays = Math.max(
startDate.getDate() - endDate.getDate(),
1,
);
const totalPrice = totalTripDurationDays * accommodation.price;
return (
<div
className="relative w-full h-[400px] rounded-2xl shadow-md overflow-hidden"
style={{
backgroundImage: `url(${accommodation.image})`,
backgroundSize: "cover",
backgroundPosition: "center",
}}
>
<div className="absolute bottom-0 left-0 right-0 flex flex-col gap-2 p-6 text-white bg-gradient-to-t from-black/90 via-black/70 to-transparent">
<p className="text-lg font-medium">Booked Accommodation</p>
<div className="flex justify-between items-baseline">
<h3 className="text-xl font-semibold"></h3>
</div>
<div className="grid grid-cols-2 gap-x-12 gap-y-2 text-sm">
<div className="flex justify-between">
<span>Address:</span>
</div>
<div className="flex justify-between">
<span>
{accommodation.name}, {capitalizeSentence(accommodation.city)}
</span>
</div>
<div className="flex justify-between">
<span>Rating:</span>
</div>
<div className="flex justify-between">
<span className="flex items-center gap-1">
<StarSVG />
{accommodation.rating}
</span>
</div>
<div className="flex justify-between">
<span>Dates:</span>
</div>
<div className="flex justify-between">
<span>
{format(startDate, "MMM d, yyyy")} -{" "}
{format(endDate, "MMM d, yyyy")}
</span>
</div>
<div className="flex justify-between">
<span>Guests:</span>
</div>
<div className="flex justify-between">
<span>{tripDetails.numberOfGuests}</span>
</div>
<div className="flex justify-between font-semibold">
<span>Total Price:</span>
</div>
<div className="flex justify-between font-semibold">
<span>${totalPrice.toLocaleString()}</span>
</div>
</div>
</div>
</div>
);
}
export default function AccommodationsList({ export default function AccommodationsList({
toolCallId,
tripDetails, tripDetails,
accommodations, accommodations,
}: { }: {
toolCallId: string;
tripDetails: TripDetails; tripDetails: TripDetails;
accommodations: Accommodation[]; accommodations: Accommodation[];
}) { }) {
const [selectedAccommodation, setSelectedAccommodation] = React.useState< const thread = useStreamContext<
{ messages: Message[]; ui: UIMessage[] },
{ MetaType: { ui: UIMessage | undefined } }
>();
const [selectedAccommodation, setSelectedAccommodation] = useState<
Accommodation | undefined 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 (
<BookedAccommodation
tripDetails={tripDetails}
accommodation={selectedAccommodation}
/>
);
} else if (accommodationBooked) {
return <div>Successfully booked accommodation!</div>;
}
if (selectedAccommodation) { if (selectedAccommodation) {
return ( return (
@@ -155,6 +308,7 @@ export default function AccommodationsList({
tripDetails={tripDetails} tripDetails={tripDetails}
onHide={() => setSelectedAccommodation(undefined)} onHide={() => setSelectedAccommodation(undefined)}
accommodation={selectedAccommodation} accommodation={selectedAccommodation}
onBook={handleBookAccommodation}
/> />
); );
} }

View File

@@ -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;
}