fix: finish hooking up accommodations flow
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
23
agent/uis/utils/get-tool-response.ts
Normal file
23
agent/uis/utils/get-tool-response.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user