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 response = await model.invoke([
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<typeof schema>

View File

@@ -114,8 +114,7 @@ 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
numberOfGuests: extractedDetails.numberOfGuests
? extractedDetails.numberOfGuests
: 2,
location: extractedDetails.location,

View File

@@ -81,15 +81,16 @@ export async function callTools(
const tripPlan = response.tool_calls?.[0]?.args as
| z.infer<typeof schema>
| 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", {

View File

@@ -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 }) => (
<svg
@@ -63,10 +71,12 @@ function SelectedAccommodation({
accommodation,
onHide,
tripDetails,
onBook,
}: {
accommodation: Accommodation;
onHide: () => 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({
</div>
</div>
<Button
onClick={() => console.log("Booked")}
onClick={() => onBook(accommodation)}
variant="secondary"
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({
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 (
<BookedAccommodation
tripDetails={tripDetails}
accommodation={selectedAccommodation}
/>
);
} else if (accommodationBooked) {
return <div>Successfully booked accommodation!</div>;
}
if (selectedAccommodation) {
return (
@@ -155,6 +308,7 @@ export default function AccommodationsList({
tripDetails={tripDetails}
onHide={() => setSelectedAccommodation(undefined)}
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;
}