From ada7e70206c1c2576624b5c72174458a84235157 Mon Sep 17 00:00:00 2001 From: Georges KABBOUCHI Date: Sat, 8 Feb 2025 16:32:57 +0200 Subject: [PATCH] feat (ui/vue): add support for prepareRequestBody --- .changeset/wet-fishes-sleep.md | 5 ++ .../07-reference/02-ai-sdk-ui/01-use-chat.mdx | 2 +- .../pages/use-chat-request/index.vue | 39 ++++++++++++ .../server/api/use-chat-request.ts | 29 +++++++++ .../TestChatPrepareRequestBodyComponent.vue | 50 ++++++++++++++++ packages/vue/src/use-chat.ts | 25 +++++++- packages/vue/src/use-chat.ui.test.tsx | 59 +++++++++++++++++++ 7 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 .changeset/wet-fishes-sleep.md create mode 100644 examples/nuxt-openai/pages/use-chat-request/index.vue create mode 100644 examples/nuxt-openai/server/api/use-chat-request.ts create mode 100644 packages/vue/src/TestChatPrepareRequestBodyComponent.vue diff --git a/.changeset/wet-fishes-sleep.md b/.changeset/wet-fishes-sleep.md new file mode 100644 index 000000000000..ea0f8965bc08 --- /dev/null +++ b/.changeset/wet-fishes-sleep.md @@ -0,0 +1,5 @@ +--- +'@ai-sdk/vue': patch +--- + +feat (ui/vue): add support for prepareRequestBody diff --git a/content/docs/07-reference/02-ai-sdk-ui/01-use-chat.mdx b/content/docs/07-reference/02-ai-sdk-ui/01-use-chat.mdx index 8e03e3f59b98..49daa632c762 100644 --- a/content/docs/07-reference/02-ai-sdk-ui/01-use-chat.mdx +++ b/content/docs/07-reference/02-ai-sdk-ui/01-use-chat.mdx @@ -192,7 +192,7 @@ Allows you to easily create a conversational user interface for your chatbot app type: '(options: { messages: UIMessage[]; requestData?: JSONValue; requestBody?: object, id: string }) => unknown', isOptional: true, description: - 'Experimental (React & Solid only). When a function is provided, it will be used to prepare the request body for the chat API. This can be useful for customizing the request body based on the messages and data in the chat.', + 'Experimental (React, Solid & Vue only). When a function is provided, it will be used to prepare the request body for the chat API. This can be useful for customizing the request body based on the messages and data in the chat.', }, { name: 'experimental_throttle', diff --git a/examples/nuxt-openai/pages/use-chat-request/index.vue b/examples/nuxt-openai/pages/use-chat-request/index.vue new file mode 100644 index 000000000000..034cb18d86e5 --- /dev/null +++ b/examples/nuxt-openai/pages/use-chat-request/index.vue @@ -0,0 +1,39 @@ + + + diff --git a/examples/nuxt-openai/server/api/use-chat-request.ts b/examples/nuxt-openai/server/api/use-chat-request.ts new file mode 100644 index 000000000000..748a06801db1 --- /dev/null +++ b/examples/nuxt-openai/server/api/use-chat-request.ts @@ -0,0 +1,29 @@ +import { createOpenAI } from '@ai-sdk/openai'; +import { streamText, Message } from 'ai'; + +export default defineLazyEventHandler(async () => { + const openai = createOpenAI({ + apiKey: useRuntimeConfig().openaiApiKey, + }); + + return defineEventHandler(async (event: any) => { + // Extract the `messages` from the body of the request + const { message } = await readBody(event); + + // Implement your own logic here to add message history + const previousMessages: Message[] = []; + const messages = [...previousMessages, message]; + + // Call the language model + const result = streamText({ + model: openai('gpt-4o-mini'), + messages, + async onFinish({ text, toolCalls, toolResults, usage, finishReason }) { + // Implement your own logic here, e.g. for storing messages + }, + }); + + // Respond with the stream + return result.toDataStreamResponse(); + }); +}); diff --git a/packages/vue/src/TestChatPrepareRequestBodyComponent.vue b/packages/vue/src/TestChatPrepareRequestBodyComponent.vue new file mode 100644 index 000000000000..e6ef4e04c729 --- /dev/null +++ b/packages/vue/src/TestChatPrepareRequestBodyComponent.vue @@ -0,0 +1,50 @@ + + + diff --git a/packages/vue/src/use-chat.ts b/packages/vue/src/use-chat.ts index 421faec87f37..c93a932de3e8 100644 --- a/packages/vue/src/use-chat.ts +++ b/packages/vue/src/use-chat.ts @@ -111,6 +111,7 @@ export function useChat( fetch, keepLastMessageOnError = true, maxSteps = 1, + experimental_prepareRequestBody, }: UseChatOptions & { /** * Maximum number of sequential LLM calls (steps), e.g. when you use tool calls. Must be at least 1. @@ -118,6 +119,23 @@ export function useChat( * By default, it's set to 1, which means that only a single LLM call is made. */ maxSteps?: number; + + /** + * Experimental (Vue only). When a function is provided, it will be used + * to prepare the request body for the chat API. This can be useful for + * customizing the request body based on the messages and data in the chat. + * + * @param id The chat ID + * @param messages The current messages in the chat + * @param requestData The data object passed in the chat request + * @param requestBody The request body object passed in the chat request + */ + experimental_prepareRequestBody?: (options: { + id: string; + messages: UIMessage[]; + requestData?: JSONValue; + requestBody?: object; + }) => unknown; } = { maxSteps: 1, }, @@ -204,7 +222,12 @@ export function useChat( await callChatApi({ api, - body: { + body: experimental_prepareRequestBody?.({ + id: chatId, + messages: chatMessages, + requestData: data, + requestBody: body, + }) ?? { id: chatId, messages: constructedMessagesPayload, data, diff --git a/packages/vue/src/use-chat.ui.test.tsx b/packages/vue/src/use-chat.ui.test.tsx index a7839b2d4e0b..09f5b6edf17e 100644 --- a/packages/vue/src/use-chat.ui.test.tsx +++ b/packages/vue/src/use-chat.ui.test.tsx @@ -24,6 +24,65 @@ import TestChatToolInvocationsComponent from './TestChatToolInvocationsComponent import TestChatAttachmentsComponent from './TestChatAttachmentsComponent.vue'; import TestChatUrlAttachmentsComponent from './TestChatUrlAttachmentsComponent.vue'; import TestChatAppendAttachmentsComponent from './TestChatAppendAttachmentsComponent.vue'; +import TestChatPrepareRequestBodyComponent from './TestChatPrepareRequestBodyComponent.vue'; + +describe('prepareRequestBody', () => { + beforeEach(() => { + render(TestChatPrepareRequestBodyComponent); + }); + + afterEach(() => { + vi.restoreAllMocks(); + cleanup(); + }); + + it( + 'should show streamed response', + withTestServer( + { + url: '/api/chat', + type: 'stream-values', + content: ['0:"Hello"\n', '0:","\n', '0:" world"\n', '0:"."\n'], + }, + async ({ call }) => { + await userEvent.click(screen.getByTestId('do-append')); + + await waitFor(() => { + const element = screen.getByTestId('on-body-options'); + expect(element.textContent?.trim() ?? '').not.toBe(''); + }); + + const value = JSON.parse( + screen.getByTestId('on-body-options').textContent ?? '', + ); + + await screen.findByTestId('message-0'); + expect(screen.getByTestId('message-0')).toHaveTextContent('User: hi'); + expect(value).toStrictEqual({ + id: expect.any(String), + messages: [ + { + role: 'user', + content: 'hi', + id: expect.any(String), + createdAt: expect.any(String), + parts: [{ type: 'text', text: 'hi' }], + }, + ], + requestData: { 'test-data-key': 'test-data-value' }, + requestBody: { 'request-body-key': 'request-body-value' }, + }); + + expect(await call(0).getRequestBodyJson()).toBe('test-request-body'); + + await screen.findByTestId('message-1'); + expect(screen.getByTestId('message-1')).toHaveTextContent( + 'AI: Hello, world.', + ); + }, + ), + ); +}); describe('data protocol stream', () => { beforeEach(() => {