Use core messages spec (#337)

This commit is contained in:
Jeremy 2024-05-15 09:39:16 -04:00 committed by GitHub
parent 095550d4dc
commit 901e4ccec1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 231 additions and 107 deletions

View file

@ -7,9 +7,8 @@ import { EmptyScreen } from '@/components/empty-screen'
import { useLocalStorage } from '@/lib/hooks/use-local-storage' import { useLocalStorage } from '@/lib/hooks/use-local-storage'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useUIState, useAIState } from 'ai/rsc' import { useUIState, useAIState } from 'ai/rsc'
import { Session } from '@/lib/types' import { Message, Session } from '@/lib/types'
import { usePathname, useRouter } from 'next/navigation' import { usePathname, useRouter } from 'next/navigation'
import { Message } from '@/lib/chat/actions'
import { useScrollAnchor } from '@/lib/hooks/use-scroll-anchor' import { useScrollAnchor } from '@/lib/hooks/use-scroll-anchor'
import { toast } from 'sonner' import { toast } from 'sonner'
@ -71,7 +70,7 @@ export function Chat({ id, className, session, missingKeys }: ChatProps) {
) : ( ) : (
<EmptyScreen /> <EmptyScreen />
)} )}
<div className="h-px w-full" ref={visibilityRef} /> <div className="w-full h-px" ref={visibilityRef} />
</div> </div>
<ChatPanel <ChatPanel
id={id} id={id}

View file

@ -7,7 +7,7 @@ import { CodeBlock } from '../ui/codeblock'
import { MemoizedReactMarkdown } from '../markdown' import { MemoizedReactMarkdown } from '../markdown'
import remarkGfm from 'remark-gfm' import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math' import remarkMath from 'remark-math'
import { StreamableValue } from 'ai/rsc' import { StreamableValue, useStreamableValue } from 'ai/rsc'
import { useStreamableText } from '@/lib/hooks/use-streamable-text' import { useStreamableText } from '@/lib/hooks/use-streamable-text'
// Different types of message bubbles. // Different types of message bubbles.

View file

@ -14,7 +14,7 @@ interface Purchase {
} }
export function Purchase({ export function Purchase({
props: { numberOfShares, symbol, price, status = 'requires_action' } props: { numberOfShares, symbol, price, status = 'expired' }
}: { }: {
props: Purchase props: Purchase
}) { }) {
@ -60,8 +60,8 @@ export function Purchase({
} }
return ( return (
<div className="rounded-xl border bg-zinc-950 p-4 text-green-400"> <div className="p-4 text-green-400 border rounded-xl bg-zinc-950">
<div className="float-right inline-block rounded-full bg-white/10 px-2 py-1 text-xs"> <div className="inline-block float-right px-2 py-1 text-xs rounded-full bg-white/10">
+1.23% +1.23%
</div> </div>
<div className="text-lg text-zinc-300">{symbol}</div> <div className="text-lg text-zinc-300">{symbol}</div>
@ -70,7 +70,7 @@ export function Purchase({
<div className="mt-4 text-zinc-200">{purchasingUI}</div> <div className="mt-4 text-zinc-200">{purchasingUI}</div>
) : status === 'requires_action' ? ( ) : status === 'requires_action' ? (
<> <>
<div className="relative mt-6 pb-6"> <div className="relative pb-6 mt-6">
<p>Shares to purchase</p> <p>Shares to purchase</p>
<input <input
id="labels-range-input" id="labels-range-input"
@ -79,18 +79,18 @@ export function Purchase({
onChange={onSliderChange} onChange={onSliderChange}
min="10" min="10"
max="1000" max="1000"
className="h-1 w-full cursor-pointer appearance-none rounded-lg bg-zinc-600 accent-green-500 dark:bg-zinc-700" className="w-full h-1 rounded-lg appearance-none cursor-pointer bg-zinc-600 accent-green-500 dark:bg-zinc-700"
/> />
<span className="absolute bottom-1 start-0 text-xs text-zinc-400"> <span className="absolute text-xs bottom-1 start-0 text-zinc-400">
10 10
</span> </span>
<span className="absolute bottom-1 start-1/3 -translate-x-1/2 text-xs text-zinc-400 rtl:translate-x-1/2"> <span className="absolute text-xs -translate-x-1/2 bottom-1 start-1/3 text-zinc-400 rtl:translate-x-1/2">
100 100
</span> </span>
<span className="absolute bottom-1 start-2/3 -translate-x-1/2 text-xs text-zinc-400 rtl:translate-x-1/2"> <span className="absolute text-xs -translate-x-1/2 bottom-1 start-2/3 text-zinc-400 rtl:translate-x-1/2">
500 500
</span> </span>
<span className="absolute bottom-1 end-0 text-xs text-zinc-400"> <span className="absolute text-xs bottom-1 end-0 text-zinc-400">
1000 1000
</span> </span>
</div> </div>
@ -98,27 +98,27 @@ export function Purchase({
<div className="mt-6"> <div className="mt-6">
<p>Total cost</p> <p>Total cost</p>
<div className="flex flex-wrap items-center text-xl font-bold sm:items-end sm:gap-2 sm:text-3xl"> <div className="flex flex-wrap items-center text-xl font-bold sm:items-end sm:gap-2 sm:text-3xl">
<div className="flex basis-1/3 flex-col tabular-nums sm:basis-auto sm:flex-row sm:items-center sm:gap-2"> <div className="flex flex-col basis-1/3 tabular-nums sm:basis-auto sm:flex-row sm:items-center sm:gap-2">
{value} {value}
<span className="mb-1 text-sm font-normal text-zinc-600 sm:mb-0 dark:text-zinc-400"> <span className="mb-1 text-sm font-normal text-zinc-600 sm:mb-0 dark:text-zinc-400">
shares shares
</span> </span>
</div> </div>
<div className="basis-1/3 text-center sm:basis-auto">×</div> <div className="text-center basis-1/3 sm:basis-auto">×</div>
<span className="flex basis-1/3 flex-col tabular-nums sm:basis-auto sm:flex-row sm:items-center sm:gap-2"> <span className="flex flex-col basis-1/3 tabular-nums sm:basis-auto sm:flex-row sm:items-center sm:gap-2">
${price} ${price}
<span className="mb-1 ml-1 text-sm font-normal text-zinc-600 sm:mb-0 dark:text-zinc-400"> <span className="mb-1 ml-1 text-sm font-normal text-zinc-600 sm:mb-0 dark:text-zinc-400">
per share per share
</span> </span>
</span> </span>
<div className="mt-2 basis-full border-t border-t-zinc-700 pt-2 text-center sm:mt-0 sm:basis-auto sm:border-0 sm:pt-0 sm:text-left"> <div className="pt-2 mt-2 text-center border-t basis-full border-t-zinc-700 sm:mt-0 sm:basis-auto sm:border-0 sm:pt-0 sm:text-left">
= <span>{formatNumber(value * price)}</span> = <span>{formatNumber(value * price)}</span>
</div> </div>
</div> </div>
</div> </div>
<button <button
className="mt-6 w-full rounded-lg bg-green-400 px-4 py-2 font-bold text-zinc-900 hover:bg-green-500" className="w-full px-4 py-2 mt-6 font-bold bg-green-400 rounded-lg text-zinc-900 hover:bg-green-500"
onClick={async () => { onClick={async () => {
const response = await confirmPurchase(symbol, price, value) const response = await confirmPurchase(symbol, price, value)
setPurchasingUI(response.purchasingUI) setPurchasingUI(response.purchasingUI)

View file

@ -33,7 +33,7 @@ import {
} from '@/lib/utils' } from '@/lib/utils'
import { saveChat } from '@/app/actions' import { saveChat } from '@/app/actions'
import { SpinnerMessage, UserMessage } from '@/components/stocks/message' import { SpinnerMessage, UserMessage } from '@/components/stocks/message'
import { Chat } from '@/lib/types' import { Chat, Message } from '@/lib/types'
import { auth } from '@/auth' import { auth } from '@/auth'
async function confirmPurchase(symbol: string, price: number, amount: number) { async function confirmPurchase(symbol: string, price: number, amount: number) {
@ -85,18 +85,7 @@ async function confirmPurchase(symbol: string, price: number, amount: number) {
aiState.done({ aiState.done({
...aiState.get(), ...aiState.get(),
messages: [ messages: [
...aiState.get().messages.slice(0, -1), ...aiState.get().messages,
{
id: nanoid(),
role: 'function',
name: 'showStockPurchase',
content: JSON.stringify({
symbol,
price,
defaultAmount: amount,
status: 'completed'
})
},
{ {
id: nanoid(), id: nanoid(),
role: 'system', role: 'system',
@ -208,15 +197,35 @@ async function submitUserMessage(content: string) {
await sleep(1000) await sleep(1000)
const toolCallId = nanoid()
aiState.done({ aiState.done({
...aiState.get(), ...aiState.get(),
messages: [ messages: [
...aiState.get().messages, ...aiState.get().messages,
{ {
id: nanoid(), id: nanoid(),
role: 'function', role: 'assistant',
name: 'listStocks', content: [
content: JSON.stringify(stocks) {
type: 'tool-call',
toolName: 'listStocks',
toolCallId,
args: { stocks }
}
]
},
{
id: nanoid(),
role: 'tool',
content: [
{
type: 'tool-result',
toolName: 'listStocks',
toolCallId,
result: stocks
}
]
} }
] ]
}) })
@ -249,15 +258,35 @@ async function submitUserMessage(content: string) {
await sleep(1000) await sleep(1000)
const toolCallId = nanoid()
aiState.done({ aiState.done({
...aiState.get(), ...aiState.get(),
messages: [ messages: [
...aiState.get().messages, ...aiState.get().messages,
{ {
id: nanoid(), id: nanoid(),
role: 'function', role: 'assistant',
name: 'showStockPrice', content: [
content: JSON.stringify({ symbol, price, delta }) {
type: 'tool-call',
toolName: 'showStockPrice',
toolCallId,
args: { symbol, price, delta }
}
]
},
{
id: nanoid(),
role: 'tool',
content: [
{
type: 'tool-result',
toolName: 'showStockPrice',
toolCallId,
result: { symbol, price, delta }
}
]
} }
] ]
}) })
@ -286,11 +315,42 @@ async function submitUserMessage(content: string) {
) )
}), }),
generate: async function* ({ symbol, price, numberOfShares = 100 }) { generate: async function* ({ symbol, price, numberOfShares = 100 }) {
const toolCallId = nanoid()
if (numberOfShares <= 0 || numberOfShares > 1000) { if (numberOfShares <= 0 || numberOfShares > 1000) {
aiState.done({ aiState.done({
...aiState.get(), ...aiState.get(),
messages: [ messages: [
...aiState.get().messages, ...aiState.get().messages,
{
id: nanoid(),
role: 'assistant',
content: [
{
type: 'tool-call',
toolName: 'showStockPurchase',
toolCallId,
args: { symbol, price, numberOfShares }
}
]
},
{
id: nanoid(),
role: 'tool',
content: [
{
type: 'tool-result',
toolName: 'showStockPurchase',
toolCallId,
result: {
symbol,
price,
numberOfShares,
status: 'expired'
}
}
]
},
{ {
id: nanoid(), id: nanoid(),
role: 'system', role: 'system',
@ -300,21 +360,38 @@ async function submitUserMessage(content: string) {
}) })
return <BotMessage content={'Invalid amount'} /> return <BotMessage content={'Invalid amount'} />
} } else {
aiState.done({ aiState.done({
...aiState.get(), ...aiState.get(),
messages: [ messages: [
...aiState.get().messages, ...aiState.get().messages,
{ {
id: nanoid(), id: nanoid(),
role: 'function', role: 'assistant',
name: 'showStockPurchase', content: [
content: JSON.stringify({ {
type: 'tool-call',
toolName: 'showStockPurchase',
toolCallId,
args: { symbol, price, numberOfShares }
}
]
},
{
id: nanoid(),
role: 'tool',
content: [
{
type: 'tool-result',
toolName: 'showStockPurchase',
toolCallId,
result: {
symbol, symbol,
price, price,
numberOfShares numberOfShares
}) }
}
]
} }
] ]
}) })
@ -332,6 +409,7 @@ async function submitUserMessage(content: string) {
</BotCard> </BotCard>
) )
} }
}
}, },
getEvents: { getEvents: {
description: description:
@ -356,15 +434,35 @@ async function submitUserMessage(content: string) {
await sleep(1000) await sleep(1000)
const toolCallId = nanoid()
aiState.done({ aiState.done({
...aiState.get(), ...aiState.get(),
messages: [ messages: [
...aiState.get().messages, ...aiState.get().messages,
{ {
id: nanoid(), id: nanoid(),
role: 'function', role: 'assistant',
name: 'getEvents', content: [
content: JSON.stringify(events) {
type: 'tool-call',
toolName: 'getEvents',
toolCallId,
args: { events }
}
]
},
{
id: nanoid(),
role: 'tool',
content: [
{
type: 'tool-result',
toolName: 'getEvents',
toolCallId,
result: events
}
]
} }
] ]
}) })
@ -385,13 +483,6 @@ async function submitUserMessage(content: string) {
} }
} }
export type Message = {
role: 'user' | 'assistant' | 'system' | 'function' | 'data' | 'tool'
content: string
id: string
name?: string
}
export type AIState = { export type AIState = {
chatId: string chatId: string
messages: Message[] messages: Message[]
@ -425,7 +516,7 @@ export const AI = createAI<AIState, UIState>({
return return
} }
}, },
onSetAIState: async ({ state, done }) => { onSetAIState: async ({ state }) => {
'use server' 'use server'
const session = await auth() const session = await auth()
@ -436,7 +527,9 @@ export const AI = createAI<AIState, UIState>({
const createdAt = new Date() const createdAt = new Date()
const userId = session.user.id as string const userId = session.user.id as string
const path = `/chat/${chatId}` const path = `/chat/${chatId}`
const title = messages[0].content.substring(0, 100)
const firstMessageContent = messages[0].content as string
const title = firstMessageContent.substring(0, 100)
const chat: Chat = { const chat: Chat = {
id: chatId, id: chatId,
@ -460,28 +553,36 @@ export const getUIStateFromAIState = (aiState: Chat) => {
.map((message, index) => ({ .map((message, index) => ({
id: `${aiState.chatId}-${index}`, id: `${aiState.chatId}-${index}`,
display: display:
message.role === 'function' ? ( message.role === 'tool' ? (
message.name === 'listStocks' ? ( message.content.map(tool => {
return tool.toolName === 'listStocks' ? (
<BotCard> <BotCard>
<Stocks props={JSON.parse(message.content)} /> {/* TODO: Infer types based on the tool result*/}
{/* @ts-expect-error */}
<Stocks props={tool.result} />
</BotCard> </BotCard>
) : message.name === 'showStockPrice' ? ( ) : tool.toolName === 'showStockPrice' ? (
<BotCard> <BotCard>
<Stock props={JSON.parse(message.content)} /> {/* @ts-expect-error */}
<Stock props={tool.result} />
</BotCard> </BotCard>
) : message.name === 'showStockPurchase' ? ( ) : tool.toolName === 'showStockPurchase' ? (
<BotCard> <BotCard>
<Purchase props={JSON.parse(message.content)} /> {/* @ts-expect-error */}
<Purchase props={tool.result} />
</BotCard> </BotCard>
) : message.name === 'getEvents' ? ( ) : tool.toolName === 'getEvents' ? (
<BotCard> <BotCard>
<Events props={JSON.parse(message.content)} /> {/* @ts-expect-error */}
<Events props={tool.result} />
</BotCard> </BotCard>
) : null ) : null
})
) : message.role === 'user' ? ( ) : message.role === 'user' ? (
<UserMessage>{message.content}</UserMessage> <UserMessage>{message.content as string}</UserMessage>
) : ( ) : message.role === 'assistant' &&
typeof message.content === 'string' ? (
<BotMessage content={message.content} /> <BotMessage content={message.content} />
) ) : null
})) }))
} }

View file

@ -1,4 +1,8 @@
import { Message } from 'ai' import { CoreMessage } from 'ai'
export type Message = CoreMessage & {
id: string
}
export interface Chat extends Record<string, any> { export interface Chat extends Record<string, any> {
id: string id: string

View file

@ -27,7 +27,7 @@
"@vercel/analytics": "^1.1.2", "@vercel/analytics": "^1.1.2",
"@vercel/kv": "^1.0.1", "@vercel/kv": "^1.0.1",
"@vercel/og": "^0.6.2", "@vercel/og": "^0.6.2",
"ai": "^3.1.1", "ai": "^3.1.5",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.0", "clsx": "^2.1.0",
"d3-scale": "^4.0.2", "d3-scale": "^4.0.2",

30
pnpm-lock.yaml generated
View file

@ -48,8 +48,8 @@ dependencies:
specifier: ^0.6.2 specifier: ^0.6.2
version: 0.6.2 version: 0.6.2
ai: ai:
specifier: ^3.1.1 specifier: ^3.1.5
version: 3.1.1(react@18.2.0)(solid-js@1.8.11)(svelte@4.2.9)(vue@3.4.15)(zod@3.22.4) version: 3.1.5(openai@4.24.7)(react@18.2.0)(solid-js@1.8.11)(svelte@4.2.9)(vue@3.4.15)(zod@3.22.4)
class-variance-authority: class-variance-authority:
specifier: ^0.7.0 specifier: ^0.7.0
version: 0.7.0 version: 0.7.0
@ -216,6 +216,22 @@ packages:
zod: 3.22.4 zod: 3.22.4
dev: false dev: false
/@ai-sdk/provider-utils@0.0.6(zod@3.22.4):
resolution: {integrity: sha512-SxOZgSxnaVlW04/SjfMoAD45kWOWTWx0QcZrHaQnePooLhyk5AqQpgauPijL803uoJPCKfzd0UBv1gSKvWiU0A==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.0.0
peerDependenciesMeta:
zod:
optional: true
dependencies:
'@ai-sdk/provider': 0.0.3
eventsource-parser: 1.1.2
nanoid: 3.3.6
secure-json-parse: 2.7.0
zod: 3.22.4
dev: false
/@ai-sdk/provider@0.0.3: /@ai-sdk/provider@0.0.3:
resolution: {integrity: sha512-0B8P6VZpJ6F9yS9BpmJBYSqIaIfeRtL5tD5SP+qgR8y0pPwalIbRMUFiLz9YUT6g70MJsCLpm/2/fX3cfAYCJw==} resolution: {integrity: sha512-0B8P6VZpJ6F9yS9BpmJBYSqIaIfeRtL5tD5SP+qgR8y0pPwalIbRMUFiLz9YUT6g70MJsCLpm/2/fX3cfAYCJw==}
engines: {node: '>=18'} engines: {node: '>=18'}
@ -1593,16 +1609,19 @@ packages:
humanize-ms: 1.2.1 humanize-ms: 1.2.1
dev: false dev: false
/ai@3.1.1(react@18.2.0)(solid-js@1.8.11)(svelte@4.2.9)(vue@3.4.15)(zod@3.22.4): /ai@3.1.5(openai@4.24.7)(react@18.2.0)(solid-js@1.8.11)(svelte@4.2.9)(vue@3.4.15)(zod@3.22.4):
resolution: {integrity: sha512-pJZc6q7SLd2/NenxN62iagMw9HHQ4Q8FyKqkrZUJntupRTHHgN3fL7exzJU/ICHDAbtn/EcJXOau6P61QgUtKQ==} resolution: {integrity: sha512-uE8EfcigIoep4NCqIOWsNUP/alyZ93XtsDsLjiop51y4lEtgJg0GzpbBRfYKaEK1O74HiQ2CmaeOOBlNVBBiIQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
openai: ^4.42.0
react: ^18.2.0 react: ^18.2.0
solid-js: ^1.7.7 solid-js: ^1.7.7
svelte: ^3.0.0 || ^4.0.0 svelte: ^3.0.0 || ^4.0.0
vue: ^3.3.4 vue: ^3.3.4
zod: ^3.0.0 zod: ^3.0.0
peerDependenciesMeta: peerDependenciesMeta:
openai:
optional: true
react: react:
optional: true optional: true
solid-js: solid-js:
@ -1615,11 +1634,12 @@ packages:
optional: true optional: true
dependencies: dependencies:
'@ai-sdk/provider': 0.0.3 '@ai-sdk/provider': 0.0.3
'@ai-sdk/provider-utils': 0.0.5(zod@3.22.4) '@ai-sdk/provider-utils': 0.0.6(zod@3.22.4)
eventsource-parser: 1.1.2 eventsource-parser: 1.1.2
json-schema: 0.4.0 json-schema: 0.4.0
jsondiffpatch: 0.6.0 jsondiffpatch: 0.6.0
nanoid: 3.3.6 nanoid: 3.3.6
openai: 4.24.7
react: 18.2.0 react: 18.2.0
secure-json-parse: 2.7.0 secure-json-parse: 2.7.0
solid-js: 1.8.11 solid-js: 1.8.11