diff --git a/.changeset/pink-hands-find.md b/.changeset/pink-hands-find.md new file mode 100644 index 00000000000..45600c06bc5 --- /dev/null +++ b/.changeset/pink-hands-find.md @@ -0,0 +1,5 @@ +--- +"@thirdweb-dev/ai-sdk-provider": patch +--- + +Auto handle session ids diff --git a/apps/playground-web/src/app/ai/ai-sdk/components/chat-container.tsx b/apps/playground-web/src/app/ai/ai-sdk/components/chat-container.tsx index 3484a8969db..293072b7ebe 100644 --- a/apps/playground-web/src/app/ai/ai-sdk/components/chat-container.tsx +++ b/apps/playground-web/src/app/ai/ai-sdk/components/chat-container.tsx @@ -1,8 +1,11 @@ "use client"; -import { useChat } from "@ai-sdk/react"; +import { type UseChatHelpers, useChat } from "@ai-sdk/react"; import type { ThirdwebAiMessage } from "@thirdweb-dev/ai-sdk-provider"; -import { DefaultChatTransport } from "ai"; +import { + DefaultChatTransport, + lastAssistantMessageIsCompleteWithToolCalls, +} from "ai"; import { useMemo, useState } from "react"; import { defineChain, prepareTransaction } from "thirdweb"; import { @@ -31,30 +34,19 @@ import { Loader } from "../../../../components/loader"; import { THIRDWEB_CLIENT } from "../../../../lib/client"; export function ChatContainer() { - const [sessionId, setSessionId] = useState(""); - const { messages, sendMessage, status, addToolResult } = useChat({ transport: new DefaultChatTransport({ api: "/api/chat", }), - onFinish: ({ message }) => { - setSessionId(message.metadata?.session_id ?? ""); - }, + sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls, }); const [input, setInput] = useState(""); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (input.trim()) { - sendMessage( - { text: input }, - { - body: { - sessionId, - }, - }, - ); + sendMessage({ text: input }); setInput(""); } }; @@ -101,7 +93,6 @@ export function ChatContainer() { addToolResult={addToolResult} sendMessage={sendMessage} toolCallId={part.toolCallId} - sessionId={sessionId} /> ); case "tool-sign_swap": @@ -113,7 +104,6 @@ export function ChatContainer() { addToolResult={addToolResult} sendMessage={sendMessage} toolCallId={part.toolCallId} - sessionId={sessionId} /> ); default: @@ -158,14 +148,13 @@ type SignTransactionButtonProps = { { type: "tool-sign_transaction" } >["input"] | undefined; - addToolResult: ReturnType>["addToolResult"]; + addToolResult: UseChatHelpers["addToolResult"]; + sendMessage: UseChatHelpers["sendMessage"]; toolCallId: string; - sendMessage: ReturnType>["sendMessage"]; - sessionId: string; }; const SignTransactionButton = (props: SignTransactionButtonProps) => { - const { input, addToolResult, toolCallId, sendMessage, sessionId } = props; + const { input, addToolResult, toolCallId, sendMessage } = props; const transactionData: { chain_id: number; to: string; @@ -209,21 +198,9 @@ const SignTransactionButton = (props: SignTransactionButtonProps) => { chain_id: transaction.chain.id, }, }); - sendMessage(undefined, { - body: { - sessionId, - }, - }); }} onError={(error) => { - sendMessage( - { text: `Transaction failed: ${error.message}` }, - { - body: { - sessionId, - }, - }, - ); + sendMessage({ text: `Transaction failed: ${error.message}` }); }} > Sign Transaction @@ -241,13 +218,12 @@ type SignSwapButtonProps = { { type: "tool-sign_swap" } >["input"] | undefined; - addToolResult: ReturnType>["addToolResult"]; + addToolResult: UseChatHelpers["addToolResult"]; + sendMessage: UseChatHelpers["sendMessage"]; toolCallId: string; - sendMessage: ReturnType>["sendMessage"]; - sessionId: string; }; const SignSwapButton = (props: SignSwapButtonProps) => { - const { input, addToolResult, toolCallId, sendMessage, sessionId } = props; + const { input, addToolResult, toolCallId, sendMessage } = props; const transactionData: { chain_id: number; to: string; @@ -293,21 +269,9 @@ const SignSwapButton = (props: SignSwapButtonProps) => { chain_id: transaction.chain.id, }, }); - sendMessage(undefined, { - body: { - sessionId, - }, - }); }} onError={(error) => { - sendMessage( - { text: `Transaction failed: ${error.message}` }, - { - body: { - sessionId, - }, - }, - ); + sendMessage({ text: `Transaction failed: ${error.message}` }); }} > Sign swap diff --git a/apps/playground-web/src/app/ai/ai-sdk/page.tsx b/apps/playground-web/src/app/ai/ai-sdk/page.tsx index b567dcc55e1..ead6798ba90 100644 --- a/apps/playground-web/src/app/ai/ai-sdk/page.tsx +++ b/apps/playground-web/src/app/ai/ai-sdk/page.tsx @@ -29,7 +29,7 @@ export default function Page() { icon={BotIcon} title={title} description={description} - docsLink="https://portal.thirdweb.com/ai/chat?utm_source=playground" + docsLink="https://portal.thirdweb.com/ai/chat/ai-sdk?utm_source=playground" >
@@ -69,12 +69,10 @@ const thirdwebAI = createThirdwebAI({ }); export async function POST(req: Request) { - const { messages, sessionId } = await req.json(); - + const { messages, id } = await req.json(); const result = streamText({ - model: thirdwebAI.chat({ + model: thirdwebAI.chat(id, { context: { - session_id: sessionId, // session id for continuity chain_ids: [8453], // optional chain ids from: "0x...", // optional from address auto_execute_transactions: true, // optional, defaults to false @@ -86,14 +84,6 @@ export async function POST(req: Request) { return result.toUIMessageStreamResponse({ sendReasoning: true, // optional, to send reasoning steps to the client - messageMetadata({ part }) { - // record session id for continuity - if (part.type === "finish-step") { - return { - session_id: part.response.id, - }; - } - }, }); } @@ -122,33 +112,19 @@ import { useState } from 'react'; import { ThirdwebAiMessage } from '@thirdweb-dev/ai-sdk-provider'; export default function Page() { - const [sessionId, setSessionId] = useState(''); - const { messages, sendMessage, status } = useChat({ + const { messages, sendMessage } = useChat({ transport: new DefaultChatTransport({ // see server implementation below api: '/api/chat', }), - onFinish: ({ message }) => { - // record session id for continuity - setSessionId(message.metadata?.session_id ?? ''); - }, }); - const send = (message: string) => { - sendMessage({ text: message }, { - body: { - // send session id for continuity - sessionId, - }, - }); - } - return ( <> {messages.map(message => ( ))} - + ); }`} diff --git a/apps/playground-web/src/app/api/chat/route.ts b/apps/playground-web/src/app/api/chat/route.ts index 3db5feddb26..6ad9617c4be 100644 --- a/apps/playground-web/src/app/api/chat/route.ts +++ b/apps/playground-web/src/app/api/chat/route.ts @@ -11,27 +11,15 @@ const thirdwebAI = createThirdwebAI({ export async function POST(req: Request) { const body = await req.json(); - const { messages, sessionId }: { messages: UIMessage[]; sessionId: string } = - body; + const { messages, id }: { messages: UIMessage[]; id: string } = body; const result = streamText({ - model: thirdwebAI.chat({ - context: { - session_id: sessionId, - }, - }), + model: thirdwebAI.chat(id), messages: convertToModelMessages(messages), tools: thirdwebAI.tools(), }); return result.toUIMessageStreamResponse({ sendReasoning: true, - messageMetadata({ part }) { - if (part.type === "finish-step") { - return { - session_id: part.response.id, - }; - } - }, }); } diff --git a/apps/portal/src/app/ai/chat/ai-sdk/page.mdx b/apps/portal/src/app/ai/chat/ai-sdk/page.mdx index e391e22f0d6..650b83dac0e 100644 --- a/apps/portal/src/app/ai/chat/ai-sdk/page.mdx +++ b/apps/portal/src/app/ai/chat/ai-sdk/page.mdx @@ -24,8 +24,6 @@ Create a thirdweb ai provider instance and compatible with the Vercel AI SDK by Then pass a `thirdwebAI.chat()` instance as the model for the `streamText` function and configure the model context with the `context` option. -For continuous conversations, you can extract the session id from the response and send it back to the client by overriding the `messageMetadata` function as shown below. - ```ts // src/app/api/chat/route.ts @@ -40,12 +38,11 @@ const thirdwebAI = createThirdwebAI({ }); export async function POST(req: Request) { - const { messages, sessionId } = await req.json(); + const { messages, id } = await req.json(); const result = streamText({ - model: thirdwebAI.chat({ + model: thirdwebAI.chat(id, { context: { - session_id: sessionId, // session id for continuity chain_ids: [8453], // optional chain ids from: "0x...", // optional from address auto_execute_transactions: true, // optional, defaults to false @@ -57,18 +54,12 @@ export async function POST(req: Request) { return result.toUIMessageStreamResponse({ sendReasoning: true, // optional, to send reasoning steps to the client - messageMetadata({ part }) { - // record session id for continuity - if (part.type === "finish-step") { - return { - session_id: part.response.id, - }; - } - }, }); } ``` +Continuous conversations are handled automatically. You can create a new conversation by passing a new `id` to the `thirdwebAI.chat()` function. + #### Client side (React, using `useChat`) Use `useChat()` to get typed objects from `useChat()`. This will give you strongly typed tool results in your UI like `tool-sign_transaction`, `tool-sign_swap` or `tool-monitor_transaction`. @@ -77,45 +68,34 @@ Use `useChat()` to get typed objects from `useChat()`. This w "use client"; import { useState } from "react"; -import { useChat, DefaultChatTransport } from "@ai-sdk/react"; +import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls } from "ai"; +import { useChat, type UseChatHelpers } from "@ai-sdk/react"; import type { ThirdwebAiMessage } from "@thirdweb-dev/ai-sdk-provider"; export function Chat() { - const [sessionId, setSessionId] = useState(""); - const { messages, sendMessage, addToolResult, status } = useChat({ transport: new DefaultChatTransport({ api: "/api/chat" }), - onFinish: ({ message }) => { - // save session id for continuity - setSessionId(message.metadata?.session_id ?? ""); - }, + // send tool results automatically + sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls }); - const send = (message: string) => { - sendMessage( - { text: message }, - { - body: { - // send session id for continuity - sessionId, - }, - }, - ); - }; - // You can render messages and reasoning steps as you normally would - // Use your own UI or the vercel ai sdk UI + // Use your own UI or the vercel ai elements UI components // When a tool part arrives (e.g., message.parts[0].type === "tool-sign_transaction"), - // you can render a thirdweb `TransactionButton` wired to the provided input to execute the transaction. - return return ( + // you can render a transaction button with the input to execute the transaction. + return ( <> {messages.map((message) => ( - + ))} - + - );; + ); } ``` @@ -134,11 +114,9 @@ Then on result, you can call the `addToolResult` function to add the tool result ```tsx export function RenderMessage(props: { message: ThirdwebAiMessage; - sessionId: string; - addToolResult: (toolResult: ThirdwebAiMessage) => void; - sendMessage: (message: string) => void; + addToolResult: UseChatHelpers["addToolResult"]; }) { - const { message, sessionId, addToolResult, sendMessage } = props; + const { message, addToolResult } = props; return ( <> {message.parts.map((part, i) => { @@ -163,33 +141,19 @@ export function RenderMessage(props: { }) } onTransactionSent={(transaction) => { - // add the tool result to the messages array + // send the tool result to the model to continue the conversation addToolResult({ tool: "sign_transaction", - toolCallId, + toolCallId: part.toolCallId, output: { transaction_hash: transaction.transactionHash, chain_id: transaction.chain.id, }, }); - // send the message to the model to continue the conversation - sendMessage(undefined, { - body: { - // send session id for continuity - sessionId, - }, - }); }} onError={(error) => { - // in case of error, send the error message to the model - sendMessage( - { text: `Transaction failed: ${error.message}` }, - { - body: { - sessionId, - }, - }, - ); + // in case of error, show error UI or send the error message to the model + console.error(error); }} > Sign Transaction diff --git a/packages/ai-sdk-provider/src/provider.ts b/packages/ai-sdk-provider/src/provider.ts index 2ee3cb033f7..5b7599d9f9c 100644 --- a/packages/ai-sdk-provider/src/provider.ts +++ b/packages/ai-sdk-provider/src/provider.ts @@ -26,15 +26,24 @@ class ThirdwebLanguageModel implements LanguageModelV2 { private config: ThirdwebConfig; private settings: ThirdwebSettings; + private sessionStore: SessionStore; + private chatId: string; constructor( modelId: string, settings: ThirdwebSettings, config: ThirdwebConfig, + sessionStore: SessionStore, + chatId?: string, ) { this.modelId = modelId; this.settings = settings; this.config = config; + this.sessionStore = sessionStore; + this.chatId = chatId || this.generateRandomChatId(); + if (this.chatId && settings.context?.session_id) { + this.sessionStore.setSessionId(this.chatId, settings.context.session_id); + } } private getHeaders() { @@ -61,6 +70,39 @@ class ThirdwebLanguageModel implements LanguageModelV2 { return headers; } + private getSessionId() { + return this.sessionStore.getSessionId(this.chatId); + } + + private setSessionId(sessionId: string) { + this.sessionStore.setSessionId(this.chatId, sessionId); + } + + private generateRandomChatId(): string { + // Use crypto.randomUUID if available (modern browsers and Node.js 14.17+) + if ( + typeof crypto !== "undefined" && + typeof crypto.randomUUID === "function" + ) { + return crypto.randomUUID(); + } + + // Fallback for older environments - generate a random string + const chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let result = ""; + + // Generate timestamp prefix for uniqueness + const timestamp = Date.now().toString(36); + + // Add random suffix + for (let i = 0; i < 8; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + + return `chat_${timestamp}_${result}`; + } + private convertMessages(prompt: LanguageModelV2CallOptions["prompt"]) { return prompt.map((message) => { switch (message.role) { @@ -122,14 +164,12 @@ class ThirdwebLanguageModel implements LanguageModelV2 { .reverse() .find((m) => m.role === "user" || m.role === "tool"); const messages = - this.settings.context?.session_id && lastUserMessage - ? [lastUserMessage] - : allMessages; + this.getSessionId() && lastUserMessage ? [lastUserMessage] : allMessages; const body = { messages, stream: false, - context: this.settings.context, + context: { ...this.settings.context, session_id: this.getSessionId() }, }; const response = await fetch( @@ -187,14 +227,12 @@ class ThirdwebLanguageModel implements LanguageModelV2 { .reverse() .find((m) => m.role === "user" || m.role === "tool"); const messages = - this.settings.context?.session_id && lastUserMessage - ? [lastUserMessage] - : allMessages; + this.getSessionId() && lastUserMessage ? [lastUserMessage] : allMessages; const body = { messages, stream: true, - context: this.settings.context, + context: { ...this.settings.context, session_id: this.getSessionId() }, }; const response = await fetch( @@ -220,6 +258,7 @@ class ThirdwebLanguageModel implements LanguageModelV2 { const reader = response.body.getReader(); const decoder = new TextDecoder(); + const self = this; return { stream: new ReadableStream({ @@ -358,6 +397,7 @@ class ThirdwebLanguageModel implements LanguageModelV2 { if (currentEvent === "init") { const parsed = JSON.parse(data); if (parsed.session_id) { + self.setSessionId(parsed.session_id); controller.enqueue({ type: "response-metadata", id: parsed.session_id || "", @@ -498,13 +538,21 @@ class ThirdwebLanguageModel implements LanguageModelV2 { export class ThirdwebProvider implements ProviderV2 { private config: ThirdwebConfig; + private session: SessionStore; constructor(config: ThirdwebConfig = {}) { this.config = config; + this.session = new SessionStore(); } - chat(settings: ThirdwebSettings = {}) { - return new ThirdwebLanguageModel("t0-latest", settings, this.config); + chat(id?: string, settings: ThirdwebSettings = {}) { + return new ThirdwebLanguageModel( + "t0-latest", + settings, + this.config, + this.session, + id, + ); } tools() { @@ -512,7 +560,12 @@ export class ThirdwebProvider implements ProviderV2 { } languageModel(modelId: string, settings: ThirdwebSettings = {}) { - return new ThirdwebLanguageModel(modelId, settings, this.config); + return new ThirdwebLanguageModel( + modelId, + settings, + this.config, + this.session, + ); } textEmbeddingModel(_modelId: string): EmbeddingModelV2 { @@ -526,6 +579,22 @@ export class ThirdwebProvider implements ProviderV2 { } } +class SessionStore { + private sessionId: Map = new Map(); + + getSessionId(chatId: string) { + return this.sessionId.get(chatId) || null; + } + + setSessionId(chatId: string, sessionId: string) { + this.sessionId.set(chatId, sessionId); + } + + clearSessionId(chatId: string) { + this.sessionId.delete(chatId); + } +} + // Factory function for easier usage export function createThirdwebAI(config: ThirdwebConfig = {}) { return new ThirdwebProvider(config);