From 472325ad1c5c36b00368c37241b789e761168c01 Mon Sep 17 00:00:00 2001 From: zvictor Date: Mon, 24 Jun 2024 18:52:41 +0200 Subject: [PATCH] replace Gemini by Anthropic --- components/flights/purchase-ticket.tsx | 2 +- lib/chat/actions.tsx | 567 ++++++++----------------- lib/chat/tools/checkoutBooking.tsx | 40 ++ lib/chat/tools/index.ts | 7 + lib/chat/tools/listDestinations.tsx | 57 +++ lib/chat/tools/showBoardingPass.tsx | 68 +++ lib/chat/tools/showFlightStatus.tsx | 67 +++ lib/chat/tools/showFlights.tsx | 63 +++ lib/chat/tools/showHotels.tsx | 50 +++ lib/chat/tools/showSeatPicker.tsx | 61 +++ lib/chat/types.d.ts | 33 ++ package.json | 80 ++-- 12 files changed, 662 insertions(+), 433 deletions(-) create mode 100644 lib/chat/tools/checkoutBooking.tsx create mode 100644 lib/chat/tools/index.ts create mode 100644 lib/chat/tools/listDestinations.tsx create mode 100644 lib/chat/tools/showBoardingPass.tsx create mode 100644 lib/chat/tools/showFlightStatus.tsx create mode 100644 lib/chat/tools/showFlights.tsx create mode 100644 lib/chat/tools/showHotels.tsx create mode 100644 lib/chat/tools/showSeatPicker.tsx create mode 100644 lib/chat/types.d.ts diff --git a/components/flights/purchase-ticket.tsx b/components/flights/purchase-ticket.tsx index 73f7148..5d02680 100644 --- a/components/flights/purchase-ticket.tsx +++ b/components/flights/purchase-ticket.tsx @@ -18,7 +18,7 @@ type Status = | 'expired' | 'in_progress' -interface PurchaseProps { +export interface PurchaseProps { status: Status summary: { airline: string diff --git a/lib/chat/actions.tsx b/lib/chat/actions.tsx index edff8b6..ae5cc41 100644 --- a/lib/chat/actions.tsx +++ b/lib/chat/actions.tsx @@ -13,38 +13,33 @@ import { } from 'ai/rsc' import { BotCard, BotMessage } from '@/components/stocks' - import { nanoid, sleep } from '@/lib/utils' import { saveChat } from '@/app/actions' -import { SpinnerMessage, UserMessage } from '@/components/stocks/message' +import { + SpinnerMessage, + UserMessage, + SystemMessage +} from '@/components/stocks/message' import { Chat } from '../types' import { auth } from '@/auth' -import { FlightStatus } from '@/components/flights/flight-status' -import { SelectSeats } from '@/components/flights/select-seats' -import { ListFlights } from '@/components/flights/list-flights' -import { BoardingPass } from '@/components/flights/boarding-pass' import { PurchaseTickets } from '@/components/flights/purchase-ticket' import { CheckIcon, SpinnerIcon } from '@/components/ui/icons' import { format } from 'date-fns' -import { experimental_streamText } from 'ai' -import { google } from 'ai/google' -import { GoogleGenerativeAI } from '@google/generative-ai' -import { z } from 'zod' -import { ListHotels } from '@/components/hotels/list-hotels' -import { Destinations } from '@/components/flights/destinations' +import { streamText } from 'ai' +import { anthropic } from '@ai-sdk/anthropic' import { Video } from '@/components/media/video' import { rateLimit } from './ratelimit' +import * as tools from './tools' +import type { Message, AIState, UIState, AIProvider } from './types' -const genAI = new GoogleGenerativeAI( - process.env.GOOGLE_GENERATIVE_AI_API_KEY || '' -) +const model = anthropic('claude-3-haiku-20240307') async function describeImage(imageBase64: string) { 'use server' await rateLimit() - const aiState = getMutableAIState() + const aiState = getMutableAIState() const spinnerStream = createStreamableUI(null) const messageStream = createStreamableUI(null) const uiStream = createStreamableUI() @@ -129,366 +124,192 @@ async function describeImage(imageBase64: string) { } } +const processAIState = async ( + aiState: MutableAIState, + streams: ReturnType +) => { + try { + await processLLMRequest(aiState, streams) + } catch (error) { + console.error('Error in LLM request:', error) + + // Inform the user about the error + streams.uiStream.update( + <> + + Please, allow me just one more second while I try again... + {' '} + + + ) + + // Retry the LLM request with error information + try { + await processLLMRequest(aiState, streams, error) + } catch (retryError) { + console.error('Error in retry attempt:', retryError) + streams.uiStream.error(retryError) + streams.textStream.error(retryError) + streams.messageStream.error(retryError) + } + } finally { + streams.spinnerStream.done(null) + streams.uiStream.done() + streams.textStream.done() + streams.messageStream.done() + aiState.done() + } +} + async function submitUserMessage(content: string) { 'use server' await rateLimit() - const aiState = getMutableAIState() + const streams = createStreams() + const aiState = getMutableAIState() + + appendMessageToAIState(aiState, { + role: 'user', + content: [aiState.get().interactions.join('\n\n'), content] + .filter(Boolean) + .join('\n\n') + }) + // Intentionally not awaiting this: + processAIState(aiState, streams) + + return { + id: nanoid(), + attachments: streams.uiStream.value, + spinner: streams.spinnerStream.value, + display: streams.messageStream.value + } +} + +const appendMessageToAIState = ( + aiState: MutableAIState, + newMessage: Message +) => aiState.update({ ...aiState.get(), messages: [ ...aiState.get().messages, { id: nanoid(), - role: 'user', - content: `${aiState.get().interactions.join('\n\n')}\n\n${content}` + ...newMessage } ] }) +const createStreams = () => ({ + textStream: createStreamableValue(''), + spinnerStream: createStreamableUI(), + messageStream: createStreamableUI(null), + uiStream: createStreamableUI() +}) + +async function processLLMRequest( + aiState: MutableAIState, + streams: ReturnType, + previousError?: Error +) { + const prompt = `\ + You are a friendly assistant that helps the user with booking flights to destinations that are based on a list of books. You can give travel recommendations based on the books, and will continue to help the user book a flight to their destination. + + Here's the flow: + 1. List holiday destinations based on a collection of books. + 2. List flights to destination. + 3. Choose a flight. + 4. Choose a seat. + 5. Choose hotel + 6. Purchase booking. + 7. Show boarding pass. + 8. Show flight status. + ` + const history = aiState.get().messages.map(message => ({ role: message.role, content: message.content })) - // console.log(history) - const textStream = createStreamableValue('') - const spinnerStream = createStreamableUI() - const messageStream = createStreamableUI(null) - const uiStream = createStreamableUI() + history.unshift({ role: 'system', content: prompt.trim() }) - ;(async () => { - try { - const result = await experimental_streamText({ - model: google.generativeAI('models/gemini-1.5-flash'), - temperature: 0, - tools: { - showFlights: { - description: - "List available flights in the UI. List 3 that match user's query.", - parameters: z.object({ - departingCity: z.string(), - arrivalCity: z.string(), - departingAirport: z.string().describe('Departing airport code'), - arrivalAirport: z.string().describe('Arrival airport code'), - date: z - .string() - .describe( - "Date of the user's flight, example format: 6 April, 1998" - ) - }) - }, - listDestinations: { - description: 'List destinations to travel cities, max 5.', - parameters: z.object({ - destinations: z.array( - z - .string() - .describe( - 'List of destination cities. Include rome as one of the cities.' - ) - ) - }) - }, - showSeatPicker: { - description: - 'Show the UI to choose or change seat for the selected flight.', - parameters: z.object({ - departingCity: z.string(), - arrivalCity: z.string(), - flightCode: z.string(), - date: z.string() - }) - }, - showHotels: { - description: 'Show the UI to choose a hotel for the trip.', - parameters: z.object({}) - }, - checkoutBooking: { - description: - 'Show the UI to purchase/checkout a flight and hotel booking.', - parameters: z.object({}) - }, - showBoardingPass: { - description: "Show user's imaginary boarding pass.", - parameters: z.object({ - airline: z.string(), - arrival: z.string(), - departure: z.string(), - departureTime: z.string(), - arrivalTime: z.string(), - price: z.number(), - seat: z.string(), - date: z - .string() - .describe('Date of the flight, example format: 6 April, 1998'), - gate: z.string() - }) - }, - showFlightStatus: { - description: - 'Get the current status of imaginary flight by flight number and date.', - parameters: z.object({ - flightCode: z.string(), - date: z.string(), - departingCity: z.string(), - departingAirport: z.string(), - departingAirportCode: z.string(), - departingTime: z.string(), - arrivalCity: z.string(), - arrivalAirport: z.string(), - arrivalAirportCode: z.string(), - arrivalTime: z.string() - }) - } - }, - system: `\ - You are a friendly assistant that helps the user with booking flights to destinations that are based on a list of books. You can you give travel recommendations based on the books, and will continue to help the user book a flight to their destination. - - The date today is ${format(new Date(), 'd LLLL, yyyy')}. - The user's current location is San Francisco, CA, so the departure city will be San Francisco and airport will be San Francisco International Airport (SFO). The user would like to book the flight out on May 12, 2024. - - List United Airlines flights only. - - Here's the flow: - 1. List holiday destinations based on a collection of books. - 2. List flights to destination. - 3. Choose a flight. - 4. Choose a seat. - 5. Choose hotel - 6. Purchase booking. - 7. Show boarding pass. - `, - messages: [...history] + if (previousError) { + if (previousError.toolName) { + history.push({ + role: 'assistant', + content: `Call '${previousError.toolName}' with arguments: ${previousError.toolArgs || {}}` }) + } - let textContent = '' - spinnerStream.done(null) + history.push({ role: 'user', content: previousError.message }) + history.push({ role: 'user', content: 'Do not apologize for errors' }) + } - for await (const delta of result.fullStream) { - const { type } = delta - - if (type === 'text-delta') { - const { textDelta } = delta - - textContent += textDelta - messageStream.update() - - aiState.update({ - ...aiState.get(), - messages: [ - ...aiState.get().messages, - { - id: nanoid(), - role: 'assistant', - content: textContent - } - ] - }) - } else if (type === 'tool-call') { - const { toolName, args } = delta + const result = await streamText({ + model, + temperature: 0, + tools: Object.fromEntries( + Object.entries(tools).map(([k, v]) => [k, v.definition]) + ), + messages: [...history] + }) - if (toolName === 'listDestinations') { - const { destinations } = args + await handleLLMStream(result, aiState, streams) +} - uiStream.update( - - - - ) +async function handleLLMStream( + result: any, + aiState: MutableAIState, + streams: ReturnType +) { + let textContent = '' - aiState.done({ - ...aiState.get(), - interactions: [], - messages: [ - ...aiState.get().messages, - { - id: nanoid(), - role: 'assistant', - content: `Here's a list of holiday destinations based on the books you've read. Choose one to proceed to booking a flight. \n\n ${args.destinations.join(', ')}.`, - display: { - name: 'listDestinations', - props: { - destinations - } - } - } - ] - }) - } else if (toolName === 'showFlights') { - aiState.done({ - ...aiState.get(), - interactions: [], - messages: [ - ...aiState.get().messages, - { - id: nanoid(), - role: 'assistant', - content: - "Here's a list of flights for you. Choose one and we can proceed to pick a seat.", - display: { - name: 'showFlights', - props: { - summary: args - } - } - } - ] - }) - - uiStream.update( - - - - ) - } else if (toolName === 'showSeatPicker') { - aiState.done({ - ...aiState.get(), - interactions: [], - messages: [ - ...aiState.get().messages, - { - id: nanoid(), - role: 'assistant', - content: - "Here's a list of available seats for you to choose from. Select one to proceed to payment.", - display: { - name: 'showSeatPicker', - props: { - summary: args - } - } - } - ] - }) - - uiStream.update( - - - - ) - } else if (toolName === 'showHotels') { - aiState.done({ - ...aiState.get(), - interactions: [], - messages: [ - ...aiState.get().messages, - { - id: nanoid(), - role: 'assistant', - content: - "Here's a list of hotels for you to choose from. Select one to proceed to payment.", - display: { - name: 'showHotels', - props: {} - } - } - ] - }) - - uiStream.update( - - - - ) - } else if (toolName === 'checkoutBooking') { - aiState.done({ - ...aiState.get(), - interactions: [] - }) - - uiStream.update( - - - - ) - } else if (toolName === 'showBoardingPass') { - aiState.done({ - ...aiState.get(), - interactions: [], - messages: [ - ...aiState.get().messages, - { - id: nanoid(), - role: 'assistant', - content: - "Here's your boarding pass. Please have it ready for your flight.", - display: { - name: 'showBoardingPass', - props: { - summary: args - } - } - } - ] - }) - - uiStream.update( - - - - ) - } else if (toolName === 'showFlightStatus') { - aiState.update({ - ...aiState.get(), - interactions: [], - messages: [ - ...aiState.get().messages, - { - id: nanoid(), - role: 'assistant', - content: `The flight status of ${args.flightCode} is as follows: - Departing: ${args.departingCity} at ${args.departingTime} from ${args.departingAirport} (${args.departingAirportCode}) - ` - } - ], - display: { - name: 'showFlights', - props: { - summary: args - } - } - }) - - uiStream.update( - - - - ) - } + streams.spinnerStream.update(null) + + for await (const delta of result.fullStream) { + switch (delta.type) { + case 'text-delta': + const { textDelta } = delta + textContent += textDelta + streams.messageStream.update() + break + + case 'tool-call': + const { toolName, args } = delta + + if (tools[toolName] === undefined) { + throw new Error(`No tool '${toolName}' found.`) } - } - uiStream.done() - textStream.done() - messageStream.done() - } catch (e) { - console.error(e) + tools[toolName].call(args, aiState, streams.uiStream) + break - const error = new Error( - 'The AI got rate limited, please try again later.' - ) - uiStream.error(error) - textStream.error(error) - messageStream.error(error) - aiState.done() - } - })() + case 'finish': + console.log(`Finished as`, JSON.stringify(delta)) - return { - id: nanoid(), - attachments: uiStream.value, - spinner: spinnerStream.value, - display: messageStream.value + if (textContent) { + appendMessageToAIState(aiState, { + role: 'assistant', + content: textContent + }) + } + break + + case 'error': + throw delta.error + + default: + throw new Error(`Unknown stream type: ${delta.type}`) + } } } export async function requestCode() { 'use server' - const aiState = getMutableAIState() + const aiState = getMutableAIState() aiState.done({ ...aiState.get(), @@ -522,7 +343,7 @@ export async function requestCode() { export async function validateCode() { 'use server' - const aiState = getMutableAIState() + const aiState = getMutableAIState() const status = createStreamableValue('in_progress') const ui = createStreamableUI( @@ -570,46 +391,24 @@ export async function validateCode() { } } -export type Message = { - role: 'user' | 'assistant' | 'system' | 'function' | 'data' | 'tool' - content: string - id?: string - name?: string - display?: { - name: string - props: Record - } -} - -export type AIState = { - chatId: string - interactions?: string[] - messages: Message[] -} +const actions = { + submitUserMessage, + requestCode, + validateCode, + describeImage +} as const -export type UIState = { - id: string - display: React.ReactNode - spinner?: React.ReactNode - attachments?: React.ReactNode -}[] - -export const AI = createAI({ - actions: { - submitUserMessage, - requestCode, - validateCode, - describeImage - }, +export const AI = createAI({ + actions, initialUIState: [], initialAIState: { chatId: nanoid(), interactions: [], messages: [] }, - unstable_onGetUIState: async () => { + onGetUIState: async () => { 'use server' const session = await auth() if (session && session.user) { - const aiState = getAIState() + const aiState = getAIState() if (aiState) { const uiState = getUIStateFromAIState(aiState) @@ -619,7 +418,7 @@ export const AI = createAI({ return } }, - unstable_onSetAIState: async ({ state }) => { + onSetAIState: async ({ state }) => { 'use server' const session = await auth() @@ -655,30 +454,14 @@ export const getUIStateFromAIState = (aiState: Chat) => { id: `${aiState.chatId}-${index}`, display: message.role === 'assistant' ? ( - message.display?.name === 'showFlights' ? ( - - - - ) : message.display?.name === 'showSeatPicker' ? ( - - - - ) : message.display?.name === 'showHotels' ? ( - - - + tools[message.display?.name as string] !== undefined ? ( + tools[message.display?.name as keyof typeof tools].UIFromAI( + message.display.props + ) ) : message.content === 'The purchase has completed successfully.' ? ( - ) : message.display?.name === 'showBoardingPass' ? ( - - - - ) : message.display?.name === 'listDestinations' ? ( - - - ) : ( ) diff --git a/lib/chat/tools/checkoutBooking.tsx b/lib/chat/tools/checkoutBooking.tsx new file mode 100644 index 0000000..16b8f9a --- /dev/null +++ b/lib/chat/tools/checkoutBooking.tsx @@ -0,0 +1,40 @@ +import 'server-only' + +import { z } from 'zod' +import { nanoid } from '@/lib/utils' +import { BotCard, BotMessage } from '@/components/stocks' +import { + PurchaseTickets, + PurchaseProps +} from '@/components/flights/purchase-ticket' +import { createStreamableUI } from 'ai/rsc' +import type { MutableAIState } from '../types' + +export type ToolParameters = z.input +export type ToolProps = any + +export const definition = { + description: 'Show the UI to purchase/checkout a flight and hotel booking.', + parameters: z.object({}) +} + +export const call = ( + args: ToolParameters, + aiState: MutableAIState, + uiStream: ReturnType +) => { + debugger + + aiState.done({ + ...aiState.get(), + interactions: [] + }) + + uiStream.update(UIFromAI()) +} + +export const UIFromAI = (props?: PurchaseProps) => ( + + + +) diff --git a/lib/chat/tools/index.ts b/lib/chat/tools/index.ts new file mode 100644 index 0000000..dcaadaa --- /dev/null +++ b/lib/chat/tools/index.ts @@ -0,0 +1,7 @@ +export * as showFlights from './showFlights' +export * as listDestinations from './listDestinations' +export * as showSeatPicker from './showSeatPicker' +export * as showHotels from './showHotels' +export * as checkoutBooking from './checkoutBooking' +export * as showBoardingPass from './showBoardingPass' +export * as showFlightStatus from './showFlightStatus' diff --git a/lib/chat/tools/listDestinations.tsx b/lib/chat/tools/listDestinations.tsx new file mode 100644 index 0000000..f4c2135 --- /dev/null +++ b/lib/chat/tools/listDestinations.tsx @@ -0,0 +1,57 @@ +import 'server-only' + +import { z } from 'zod' +import { nanoid, sleep } from '@/lib/utils' +import { BotCard, BotMessage } from '@/components/stocks' +import { Destinations } from '@/components/flights/destinations' +import { createStreamableUI } from 'ai/rsc' +import type { MutableAIState } from '../types' + +export type ToolParameters = z.input +export type ToolProps = ToolParameters + +export const definition = { + description: 'List destinations to travel cities, max 5.', + parameters: z.object({ + destinations: z.array( + z + .string() + .describe( + 'List of destination cities. Include rome as one of the cities.' + ) + ) + }) +} + +export const call = ( + args: ToolParameters, + aiState: MutableAIState, + uiStream: ReturnType +) => { + debugger + + uiStream.update(UIFromAI(args)) + + aiState.done({ + ...aiState.get(), + interactions: [], + messages: [ + ...aiState.get().messages, + { + id: nanoid(), + role: 'assistant', + content: `Here's a list of holiday destinations based on the books you've read. Choose one to proceed to booking a flight. \n\n ${args.destinations.join(', ')}.`, + display: { + name: 'listDestinations', + props: args + } + } + ] + }) +} + +export const UIFromAI = (args: ToolProps) => ( + + + +) diff --git a/lib/chat/tools/showBoardingPass.tsx b/lib/chat/tools/showBoardingPass.tsx new file mode 100644 index 0000000..214aae8 --- /dev/null +++ b/lib/chat/tools/showBoardingPass.tsx @@ -0,0 +1,68 @@ +import 'server-only' + +import { z } from 'zod' +import { nanoid } from '@/lib/utils' +import { BotCard, BotMessage } from '@/components/stocks' +import { BoardingPass } from '@/components/flights/boarding-pass' +import { createStreamableUI } from 'ai/rsc' +import type { MutableAIState } from '../types' + +export type ToolParameters = z.input +export type ToolProps = { + summary: ToolParameters +} + +export const definition = { + description: "Show user's imaginary boarding pass.", + parameters: z.object({ + airline: z.string(), + arrival: z.string(), + departure: z.string(), + departureTime: z.string(), + arrivalTime: z.string(), + price: z.number(), + seat: z.string(), + date: z + .string() + .describe('Date of the flight, example format: 6 April, 1998'), + gate: z.string() + }) +} + +export const call = ( + args: ToolParameters, + aiState: MutableAIState, + uiStream: ReturnType +) => { + debugger + + const props: ToolProps = { + summary: args + } + + aiState.done({ + ...aiState.get(), + interactions: [], + messages: [ + ...aiState.get().messages, + { + id: nanoid(), + role: 'assistant', + content: + "Here's your boarding pass. Please have it ready for your flight.", + display: { + name: 'showBoardingPass', + props + } + } + ] + }) + + uiStream.update(UIFromAI(props)) +} + +export const UIFromAI = (props: ToolProps) => ( + + + +) diff --git a/lib/chat/tools/showFlightStatus.tsx b/lib/chat/tools/showFlightStatus.tsx new file mode 100644 index 0000000..8ae66b2 --- /dev/null +++ b/lib/chat/tools/showFlightStatus.tsx @@ -0,0 +1,67 @@ +import 'server-only' + +import { z } from 'zod' +import { nanoid } from '@/lib/utils' +import { BotCard, BotMessage } from '@/components/stocks' +import { FlightStatus } from '@/components/flights/flight-status' +import { createStreamableUI } from 'ai/rsc' +import type { MutableAIState } from '../types' + +export type ToolParameters = z.input +export type ToolProps = { + summary: ToolParameters +} + +export const definition = { + description: + 'Get the current status of imaginary flight by flight number and date.', + parameters: z.object({ + flightCode: z.string(), + date: z.string(), + departingCity: z.string(), + departingAirport: z.string(), + departingAirportCode: z.string(), + departingTime: z.string(), + arrivalCity: z.string(), + arrivalAirport: z.string(), + arrivalAirportCode: z.string(), + arrivalTime: z.string() + }) +} + +export const call = ( + args: ToolParameters, + aiState: MutableAIState, + uiStream: ReturnType +) => { + const props: ToolProps = { + summary: args + } + + aiState.update({ + ...aiState.get(), + interactions: [], + messages: [ + ...aiState.get().messages, + { + id: nanoid(), + role: 'assistant', + content: `The flight status of ${args.flightCode} is as follows: + Departing: ${args.departingCity} at ${args.departingTime} from ${args.departingAirport} (${args.departingAirportCode}) + ` + } + ], + display: { + name: 'showFlights', + props + } + }) + + uiStream.update(UIFromAI(props)) +} + +export const UIFromAI = (props: ToolProps) => ( + + + +) diff --git a/lib/chat/tools/showFlights.tsx b/lib/chat/tools/showFlights.tsx new file mode 100644 index 0000000..f810a98 --- /dev/null +++ b/lib/chat/tools/showFlights.tsx @@ -0,0 +1,63 @@ +import 'server-only' + +import { z } from 'zod' +import { nanoid } from '@/lib/utils' +import { BotCard, BotMessage } from '@/components/stocks' +import { ListFlights } from '@/components/flights/list-flights' +import { createStreamableUI } from 'ai/rsc' +import type { MutableAIState } from '../types' + +export type ToolParameters = z.input +export type ToolProps = { + summary: ToolParameters +} + +export const definition = { + description: + "List available flights in the UI. List 3 that match user's query.", + parameters: z.object({ + departingCity: z.string(), + arrivalCity: z.string(), + departingAirport: z.string().describe('Departing airport code'), + arrivalAirport: z.string().describe('Arrival airport code'), + date: z + .string() + .describe("Date of the user's flight, example format: 6 April, 1998") + }) +} + +export const call = ( + args: ToolParameters, + aiState: MutableAIState, + uiStream: ReturnType +) => { + const props: ToolProps = { + summary: args + } + + aiState.done({ + ...aiState.get(), + interactions: [], + messages: [ + ...aiState.get().messages, + { + id: nanoid(), + role: 'assistant', + content: + "Here's a list of flights for you. Choose one and we can proceed to pick a seat.", + display: { + name: 'showFlights', + props + } + } + ] + }) + + uiStream.update(UIFromAI(props)) +} + +export const UIFromAI = (args: ToolProps) => ( + + + +) diff --git a/lib/chat/tools/showHotels.tsx b/lib/chat/tools/showHotels.tsx new file mode 100644 index 0000000..5734ba8 --- /dev/null +++ b/lib/chat/tools/showHotels.tsx @@ -0,0 +1,50 @@ +import 'server-only' + +import { z } from 'zod' +import { nanoid } from '@/lib/utils' +import { BotCard, BotMessage } from '@/components/stocks' +import { ListHotels } from '@/components/hotels/list-hotels' +import { createStreamableUI } from 'ai/rsc' +import type { MutableAIState } from '../types' + +export type ToolParameters = z.input +export type ToolProps = any + +export const definition = { + description: 'Show the UI to choose a hotel for the trip.', + parameters: z.object({}) +} + +export const call = ( + args: ToolParameters, + aiState: MutableAIState, + uiStream: ReturnType +) => { + debugger + + aiState.done({ + ...aiState.get(), + interactions: [], + messages: [ + ...aiState.get().messages, + { + id: nanoid(), + role: 'assistant', + content: + "Here's a list of hotels for you to choose from. Select one to proceed to payment.", + display: { + name: 'showHotels', + props: {} + } + } + ] + }) + + uiStream.update(UIFromAI()) +} + +export const UIFromAI = (props?: ToolProps) => ( + + + +) diff --git a/lib/chat/tools/showSeatPicker.tsx b/lib/chat/tools/showSeatPicker.tsx new file mode 100644 index 0000000..ab4a546 --- /dev/null +++ b/lib/chat/tools/showSeatPicker.tsx @@ -0,0 +1,61 @@ +import 'server-only' + +import { z } from 'zod' +import { nanoid } from '@/lib/utils' +import { BotCard, BotMessage } from '@/components/stocks' +import { SelectSeats } from '@/components/flights/select-seats' +import { createStreamableUI } from 'ai/rsc' +import type { MutableAIState } from '../types' + +export type ToolParameters = z.input +export type ToolProps = { + summary: ToolParameters +} + +export const definition = { + description: 'Show the UI to choose or change seat for the selected flight.', + parameters: z.object({ + departingCity: z.string(), + arrivalCity: z.string(), + flightCode: z.string(), + date: z.string() + }) +} + +export const call = ( + args: ToolParameters, + aiState: MutableAIState, + uiStream: ReturnType +) => { + debugger + + const props: ToolProps = { + summary: args + } + + aiState.done({ + ...aiState.get(), + interactions: [], + messages: [ + ...aiState.get().messages, + { + id: nanoid(), + role: 'assistant', + content: + "Here's a list of available seats for you to choose from. Select one to proceed to payment.", + display: { + name: 'showSeatPicker', + props + } + } + ] + }) + + uiStream.update(UIFromAI(props)) +} + +export const UIFromAI = (props: ToolProps) => ( + + + +) diff --git a/lib/chat/types.d.ts b/lib/chat/types.d.ts new file mode 100644 index 0000000..e3b3e2c --- /dev/null +++ b/lib/chat/types.d.ts @@ -0,0 +1,33 @@ +import { AI } from './actions' + +export type Message = { + role: 'user' | 'assistant' | 'system' | 'function' | 'data' | 'tool' + content: string + id?: string + name?: string + display?: { + name: string + props: Record + } +} + +export type AIState = { + chatId: string + interactions?: string[] + messages: Message[] +} + +export type UIState = { + id: string + display: React.ReactNode + spinner?: React.ReactNode + attachments?: React.ReactNode +}[] + +export type MutableAIState = { + get: () => AIState + update: (newState: ValueOrUpdater) => void + done: ((newState: AIState) => void) | (() => void) +} + +export type AIProvider = typeof AI diff --git a/package.json b/package.json index 562ce85..af75337 100644 --- a/package.json +++ b/package.json @@ -13,67 +13,67 @@ "format:check": "prettier --check \"{app,lib,components}**/*.{ts,tsx,mdx}\" --cache" }, "dependencies": { - "@google/generative-ai": "^0.3.1", - "@radix-ui/react-alert-dialog": "^1.0.5", - "@radix-ui/react-dialog": "^1.0.5", - "@radix-ui/react-dropdown-menu": "^2.0.6", + "@ai-sdk/anthropic": "^0.0.24", + "@radix-ui/react-alert-dialog": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-icons": "^1.3.0", - "@radix-ui/react-label": "^2.0.2", - "@radix-ui/react-select": "^2.0.0", - "@radix-ui/react-separator": "^1.0.3", - "@radix-ui/react-slot": "^1.0.2", - "@radix-ui/react-switch": "^1.0.3", - "@radix-ui/react-tooltip": "^1.0.7", - "@upstash/ratelimit": "^1.0.3", - "@vercel/analytics": "^1.1.2", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-select": "^2.1.1", + "@radix-ui/react-separator": "^1.1.0", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-switch": "^1.1.0", + "@radix-ui/react-tooltip": "^1.1.1", + "@upstash/ratelimit": "^1.2.1", + "@vercel/analytics": "^1.3.1", "@vercel/kv": "^1.0.1", "@vercel/og": "^0.6.2", - "ai": "^3.0.17", + "ai": "^3.2.7", "class-variance-authority": "^0.7.0", - "clsx": "^2.1.0", + "clsx": "^2.1.1", "d3-scale": "^4.0.2", - "date-fns": "^3.3.1", + "date-fns": "^3.6.0", "focus-trap-react": "^10.2.3", "framer-motion": "^10.18.0", - "geist": "^1.2.1", - "nanoid": "^5.0.4", - "next": "14.1.3", + "geist": "^1.3.0", + "nanoid": "^5.0.7", + "next": "14.2.4", "next-auth": "5.0.0-beta.4", - "next-themes": "^0.2.1", - "openai": "^4.24.7", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-intersection-observer": "^9.5.3", + "next-themes": "^0.3.0", + "openai": "^4.52.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-intersection-observer": "^9.10.3", "react-jsbarcode": "^0.4.2", "react-markdown": "^8.0.7", "react-syntax-highlighter": "^15.5.0", "react-textarea-autosize": "^8.5.3", "remark-gfm": "^3.0.1", "remark-math": "^5.1.1", - "sonner": "^1.4.3", + "sonner": "^1.5.0", "usehooks-ts": "^2.16.0", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "devDependencies": { - "@tailwindcss/typography": "^0.5.10", + "@tailwindcss/typography": "^0.5.13", "@types/d3-scale": "^4.0.8", - "@types/node": "^20.11.5", - "@types/react": "^18.2.48", - "@types/react-dom": "^18.2.18", - "@types/react-syntax-highlighter": "^15.5.11", - "@typescript-eslint/parser": "^6.19.0", - "autoprefixer": "^10.4.17", + "@types/node": "^20.14.8", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@types/react-syntax-highlighter": "^15.5.13", + "@typescript-eslint/parser": "^7.13.1", + "autoprefixer": "^10.4.19", "dotenv": "^16.4.5", - "eslint": "^8.56.0", - "eslint-config-next": "14.1.0", + "eslint": "^9.5.0", + "eslint-config-next": "14.2.4", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-tailwindcss": "^3.14.0", - "postcss": "^8.4.33", - "prettier": "^3.2.4", - "tailwind-merge": "^2.2.0", - "tailwindcss": "^3.4.1", + "eslint-plugin-tailwindcss": "^3.17.4", + "postcss": "^8.4.38", + "prettier": "^3.3.2", + "tailwind-merge": "^2.3.0", + "tailwindcss": "^3.4.4", "tailwindcss-animate": "^1.0.7", - "typescript": "^5.3.3" + "typescript": "^5.5.2" }, "packageManager": "pnpm@8.6.3" }