From ac503e0ae7c28c107c043524f0ecc6f091371644 Mon Sep 17 00:00:00 2001 From: Lars Grammel Date: Tue, 19 Dec 2023 21:19:50 +0100 Subject: [PATCH] Align the chat request options for useChat across all frameworks (#859) Co-authored-by: Lars Grammel --- .changeset/brown-tigers-destroy.md | 5 ++ .changeset/thirty-tips-smash.md | 5 ++ docs/pages/docs/api-reference/use-chat.mdx | 6 +-- examples/nuxt-openai/pages/vision/index.vue | 34 ++++++++++++++ .../server/api/chat-with-vision.ts | 46 +++++++++++++++++++ .../src/routes/api/chat-with-vision/index.ts | 43 +++++++++++++++++ .../src/routes/vision/index.tsx | 46 +++++++++++++++++++ packages/core/react/use-chat.ts | 2 +- packages/core/shared/types.ts | 2 - packages/core/solid/use-chat.ts | 33 ++++++++----- packages/core/vue/use-chat.ts | 27 +++++++---- 11 files changed, 221 insertions(+), 28 deletions(-) create mode 100644 .changeset/brown-tigers-destroy.md create mode 100644 .changeset/thirty-tips-smash.md create mode 100644 examples/nuxt-openai/pages/vision/index.vue create mode 100644 examples/nuxt-openai/server/api/chat-with-vision.ts create mode 100644 examples/solidstart-openai/src/routes/api/chat-with-vision/index.ts create mode 100644 examples/solidstart-openai/src/routes/vision/index.tsx diff --git a/.changeset/brown-tigers-destroy.md b/.changeset/brown-tigers-destroy.md new file mode 100644 index 000000000000..44023b675363 --- /dev/null +++ b/.changeset/brown-tigers-destroy.md @@ -0,0 +1,5 @@ +--- +'ai': patch +--- + +ai/solid: add chat request options to useChat diff --git a/.changeset/thirty-tips-smash.md b/.changeset/thirty-tips-smash.md new file mode 100644 index 000000000000..9910d8f3c786 --- /dev/null +++ b/.changeset/thirty-tips-smash.md @@ -0,0 +1,5 @@ +--- +'ai': patch +--- + +ai/vue: add chat request options to useChat diff --git a/docs/pages/docs/api-reference/use-chat.mdx b/docs/pages/docs/api-reference/use-chat.mdx index 02fbfff20f16..d407f9f14e75 100644 --- a/docs/pages/docs/api-reference/use-chat.mdx +++ b/docs/pages/docs/api-reference/use-chat.mdx @@ -151,8 +151,9 @@ The `useChat` hook returns an object containing several helper methods and varia ], [ 'handleSubmit', - '(e: React.FormEvent) => void', - 'Form submission handler that automatically resets the input field and appends a user message.', + '(e: React.FormEvent, chatRequestOptions?: ChatRequestOptions) => void', + 'Form submission handler that automatically resets the input field and appends a user message. ' + + 'You can use the `options` parameter to send additional data, headers and more to the server.', ], [ 'isLoading', @@ -444,4 +445,3 @@ The `useChat` function returns an object containing several helper methods and v -``` diff --git a/examples/nuxt-openai/pages/vision/index.vue b/examples/nuxt-openai/pages/vision/index.vue new file mode 100644 index 000000000000..878fdb164486 --- /dev/null +++ b/examples/nuxt-openai/pages/vision/index.vue @@ -0,0 +1,34 @@ + + + diff --git a/examples/nuxt-openai/server/api/chat-with-vision.ts b/examples/nuxt-openai/server/api/chat-with-vision.ts new file mode 100644 index 000000000000..43209a076111 --- /dev/null +++ b/examples/nuxt-openai/server/api/chat-with-vision.ts @@ -0,0 +1,46 @@ +import { OpenAIStream, StreamingTextResponse } from 'ai'; +import OpenAI from 'openai'; + +export default defineLazyEventHandler(async () => { + const apiKey = useRuntimeConfig().openaiApiKey; + if (!apiKey) throw new Error('Missing OpenAI API key'); + const openai = new OpenAI({ + apiKey: apiKey, + }); + + return defineEventHandler(async (event: any) => { + // Extract the `prompt` from the body of the request + const { messages, data } = await readBody(event); + + const initialMessages = messages.slice(0, -1); + const currentMessage = messages[messages.length - 1]; + + // Ask OpenAI for a streaming chat completion given the prompt + const response = await openai.chat.completions.create({ + model: 'gpt-4-vision-preview', + stream: true, + max_tokens: 150, + messages: [ + ...initialMessages, + { + ...currentMessage, + content: [ + { type: 'text', text: currentMessage.content }, + + // forward the image information to OpenAI: + { + type: 'image_url', + image_url: data.imageUrl, + }, + ], + }, + ], + }); + + // Convert the response into a friendly text-stream + const stream = OpenAIStream(response); + + // Respond with the stream + return new StreamingTextResponse(stream); + }); +}); diff --git a/examples/solidstart-openai/src/routes/api/chat-with-vision/index.ts b/examples/solidstart-openai/src/routes/api/chat-with-vision/index.ts new file mode 100644 index 000000000000..00a2eecb8550 --- /dev/null +++ b/examples/solidstart-openai/src/routes/api/chat-with-vision/index.ts @@ -0,0 +1,43 @@ +import { OpenAIStream, StreamingTextResponse } from 'ai'; +import OpenAI from 'openai'; +import { APIEvent } from 'solid-start/api'; + +// Create an OpenAI API client +const openai = new OpenAI({ + apiKey: process.env['OPENAI_API_KEY'] || '', +}); + +export const POST = async (event: APIEvent) => { + // 'data' contains the additional data that you have sent: + const { messages, data } = await event.request.json(); + + const initialMessages = messages.slice(0, -1); + const currentMessage = messages[messages.length - 1]; + + // Ask OpenAI for a streaming chat completion given the prompt + const response = await openai.chat.completions.create({ + model: 'gpt-4-vision-preview', + stream: true, + max_tokens: 150, + messages: [ + ...initialMessages, + { + ...currentMessage, + content: [ + { type: 'text', text: currentMessage.content }, + + // forward the image information to OpenAI: + { + type: 'image_url', + image_url: data.imageUrl, + }, + ], + }, + ], + }); + + // Convert the response into a friendly text-stream + const stream = OpenAIStream(response); + // Respond with the stream + return new StreamingTextResponse(stream); +}; diff --git a/examples/solidstart-openai/src/routes/vision/index.tsx b/examples/solidstart-openai/src/routes/vision/index.tsx new file mode 100644 index 000000000000..7512e3e9ca17 --- /dev/null +++ b/examples/solidstart-openai/src/routes/vision/index.tsx @@ -0,0 +1,46 @@ +import { For, JSX } from 'solid-js'; +import { useChat } from 'ai/solid'; + +export default function Chat() { + const { messages, input, setInput, handleSubmit } = useChat({ + api: '/api/chat-with-vision', + }); + + const handleInputChange: JSX.ChangeEventHandlerUnion< + HTMLInputElement, + Event + > = e => { + setInput(e.target.value); + }; + + return ( +
+ + {m => ( +
+ {m.role === 'user' ? 'User: ' : 'AI: '} + {m.content} +
+ )} +
+ +
+ handleSubmit(e, { + data: { + imageUrl: + 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/3c/Field_sparrow_in_CP_%2841484%29_%28cropped%29.jpg/733px-Field_sparrow_in_CP_%2841484%29_%28cropped%29.jpg', + }, + }) + } + > + +
+
+ ); +} diff --git a/packages/core/react/use-chat.ts b/packages/core/react/use-chat.ts index 4373febb28f9..1c064793abf3 100644 --- a/packages/core/react/use-chat.ts +++ b/packages/core/react/use-chat.ts @@ -61,7 +61,7 @@ export type UseChatHelpers = { | React.ChangeEvent | React.ChangeEvent, ) => void; - /** Form submission handler to automatically reset input and append a user message */ + /** Form submission handler to automatically reset input and append a user message */ handleSubmit: ( e: React.FormEvent, chatRequestOptions?: ChatRequestOptions, diff --git a/packages/core/shared/types.ts b/packages/core/shared/types.ts index 7a25b5a4d044..a88dbc28bca0 100644 --- a/packages/core/shared/types.ts +++ b/packages/core/shared/types.ts @@ -145,9 +145,7 @@ export type RequestOptions = { export type ChatRequestOptions = { options?: RequestOptions; - // @deprecated functions?: Array; - // @deprecated function_call?: FunctionCall; tools?: Array; tool_choice?: ToolChoice; diff --git a/packages/core/solid/use-chat.ts b/packages/core/solid/use-chat.ts index 70cd7d0b9962..930ba9de314a 100644 --- a/packages/core/solid/use-chat.ts +++ b/packages/core/solid/use-chat.ts @@ -5,10 +5,10 @@ import { callChatApi } from '../shared/call-chat-api'; import { processChatStream } from '../shared/process-chat-stream'; import type { ChatRequest, + ChatRequestOptions, CreateMessage, JSONValue, Message, - RequestOptions, UseChatOptions, } from '../shared/types'; import { nanoid } from '../shared/utils'; @@ -28,14 +28,16 @@ export type UseChatHelpers = { */ append: ( message: Message | CreateMessage, - options?: RequestOptions, + chatRequestOptions?: ChatRequestOptions, ) => Promise; /** * Reload the last AI chat response for the given chat history. If the last * message isn't from the assistant, it will request the API to generate a * new response. */ - reload: (options?: RequestOptions) => Promise; + reload: ( + chatRequestOptions?: ChatRequestOptions, + ) => Promise; /** * Abort the current request immediately, keep the generated tokens if any. */ @@ -50,8 +52,8 @@ export type UseChatHelpers = { input: Accessor; /** Signal setter to update the input value */ setInput: Setter; - /** Form submission handler to automatically reset input and append a user message */ - handleSubmit: (e: any) => void; + /** Form submission handler to automatically reset input and append a user message */ + handleSubmit: (e: any, chatRequestOptions?: ChatRequestOptions) => void; /** Whether the API request is in progress */ isLoading: Accessor; /** Additional data added on the server via StreamData */ @@ -109,7 +111,7 @@ export function useChat({ let abortController: AbortController | null = null; async function triggerRequest( messagesSnapshot: Message[], - options?: RequestOptions, + { options, data }: ChatRequestOptions = {}, ) { try { setError(undefined); @@ -130,6 +132,7 @@ export function useChat({ let chatRequest: ChatRequest = { messages: messagesSnapshot, options, + data, }; await processChatStream({ @@ -151,6 +154,7 @@ export function useChat({ }), ), body: { + data: chatRequest.data, ...body, ...options?.body, }, @@ -237,15 +241,20 @@ export function useChat({ const [input, setInput] = createSignal(initialInput); - const handleSubmit = (e: any) => { + const handleSubmit = (e: any, options: ChatRequestOptions = {}) => { e.preventDefault(); const inputValue = input(); if (!inputValue) return; - append({ - content: inputValue, - role: 'user', - createdAt: new Date(), - }); + + append( + { + content: inputValue, + role: 'user', + createdAt: new Date(), + }, + options, + ); + setInput(''); }; diff --git a/packages/core/vue/use-chat.ts b/packages/core/vue/use-chat.ts index ef7996841d5e..457dd51640f2 100644 --- a/packages/core/vue/use-chat.ts +++ b/packages/core/vue/use-chat.ts @@ -5,10 +5,10 @@ import { callChatApi } from '../shared/call-chat-api'; import { processChatStream } from '../shared/process-chat-stream'; import type { ChatRequest, + ChatRequestOptions, CreateMessage, JSONValue, Message, - RequestOptions, UseChatOptions, } from '../shared/types'; import { nanoid } from '../shared/utils'; @@ -26,14 +26,16 @@ export type UseChatHelpers = { */ append: ( message: Message | CreateMessage, - options?: RequestOptions, + chatRequestOptions?: ChatRequestOptions, ) => Promise; /** * Reload the last AI chat response for the given chat history. If the last * message isn't from the assistant, it will request the API to generate a * new response. */ - reload: (options?: RequestOptions) => Promise; + reload: ( + chatRequestOptions?: ChatRequestOptions, + ) => Promise; /** * Abort the current request immediately, keep the generated tokens if any. */ @@ -47,7 +49,7 @@ export type UseChatHelpers = { /** The current value of the input */ input: Ref; /** Form submission handler to automatically reset input and append a user message */ - handleSubmit: (e: any) => void; + handleSubmit: (e: any, chatRequestOptions?: ChatRequestOptions) => void; /** Whether the API request is in progress */ isLoading: Ref; @@ -110,7 +112,7 @@ export function useChat({ let abortController: AbortController | null = null; async function triggerRequest( messagesSnapshot: Message[], - options?: RequestOptions, + { options, data }: ChatRequestOptions = {}, ) { try { error.value = undefined; @@ -126,6 +128,7 @@ export function useChat({ let chatRequest: ChatRequest = { messages: messagesSnapshot, options, + data, }; await processChatStream({ @@ -147,6 +150,7 @@ export function useChat({ }), ), body: { + data: chatRequest.data, ...unref(body), // Use unref to unwrap the ref value ...options?.body, }, @@ -233,14 +237,17 @@ export function useChat({ const input = ref(initialInput); - const handleSubmit = (e: any) => { + const handleSubmit = (e: any, options: ChatRequestOptions = {}) => { e.preventDefault(); const inputValue = input.value; if (!inputValue) return; - append({ - content: inputValue, - role: 'user', - }); + append( + { + content: inputValue, + role: 'user', + }, + options, + ); input.value = ''; };