diff --git a/README.md b/README.md index 558ba8a..f7da897 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,26 @@ const { input, messages, isLoading, handleSubmit, onInputChange } = useChat({ }); ``` +If tool doesn't need to do any async operations as you expect the AI model to return all the required data to render the component, you can use a simple function that returns the data and the component: + +```ts +tools: { + joke: { + description: + "Call this tool with an original joke setup and punchline.", + parameters: z.object({ + setup: z.string(), + punchline: z.string(), + }), + // Render component for joke and pass data also back to the model. + render: (data) => ({ + data, + component: , + }), + }, +}, +``` + Tools framework within `useChat` is highly extensible. You can define multiple tools to perform various functions based on your chat application's requirements. ## Reference diff --git a/src/openai/chat-completion.ts b/src/openai/chat-completion.ts index f1d3742..a9fb84a 100644 --- a/src/openai/chat-completion.ts +++ b/src/openai/chat-completion.ts @@ -7,7 +7,12 @@ import { import z from 'zod'; import { OpenAIApi } from './openai-api'; import React, { ReactElement } from 'react'; -import { filterOutReactComponents, sleep, toolsToJsonSchema } from './utils'; +import { + filterOutReactComponents, + isAsyncGeneratorFunction, + sleep, + toolsToJsonSchema, +} from './utils'; import EventSource, { EventSourceEvent } from 'react-native-sse'; // Tool's render function can return either data or a component @@ -46,11 +51,10 @@ export type ChatCompletionCreateParams = Omit< type ToolGeneratorReturn = { component: ReactElement; data: object }; // A generator that will yield some (0 or more) React components and then finish with an object, containing both the data and the component to display. -export type ToolRenderReturnType = AsyncGenerator< - ReactElement, - ToolGeneratorReturn, - unknown ->; +// Allow also to return only a component and data in case the tool does not need to do any async operations. +export type ToolRenderReturnType = + | AsyncGenerator + | ToolGeneratorReturn; // Chat completion callbacks, utilized by the caller export interface ChatCompletionCallbacks { @@ -295,37 +299,51 @@ export class ChatCompletion { return; } - // Call the tool and iterate over results - // Use while to access the last value of the generator (what it returns too rather then only what it yields) - // Only the last returned/yielded value is the one we use - const generator = chosenTool.render(args); - - let next = null; - while (next == null || !next.done) { - // Fetch the next value - next = await generator.next(); - const value = next.value; - - // If the value is contains data and component, save both - if ( - value != null && - Object.keys(value).includes('data') && - Object.keys(value).includes('component') - ) { - const v = value as { data: any; component: ReactElement }; - this.toolRenderResult = v.component; - this.toolCallResult = v.data; - } else if (React.isValidElement(value)) { - this.toolRenderResult = value; + // This is either + // - an async generator (if tool will be fetching data asynchronously) + // - a component and data (if tool does not need to do any async operations) + const generatorOrData = chosenTool.render(args); + + if (isAsyncGeneratorFunction(generatorOrData)) { + // Call the tool and iterate over results + // Use while to access the last value of the generator (what it returns too rather then only what it yields) + // Only the last returned/yielded value is the one we use + const generator = generatorOrData; + let next = null; + while (next == null || !next.done) { + // Fetch the next value + next = await generator.next(); + const value = next.value; + + // If the value is contains data and component, save both + if ( + value != null && + Object.keys(value).includes('data') && + Object.keys(value).includes('component') + ) { + const v = value as { data: any; component: ReactElement }; + this.toolRenderResult = v.component; + this.toolCallResult = v.data; + } else if (React.isValidElement(value)) { + this.toolRenderResult = value; + } + + // Update the parent by calling the callbacks + this.notifyChunksReceived(); + + // Break if the generator is done + if (next.done) { + break; + } } + } else { + // Not a generator, simply call the render function, we received all the data at once. + const data = generatorOrData; + this.toolRenderResult = data.component; + this.toolCallResult = data.data; // Update the parent by calling the callbacks this.notifyChunksReceived(); - - // Break if the generator is done - if (next.done) { - break; - } } // Call recursive streaming diff --git a/src/openai/utils.ts b/src/openai/utils.ts index 1dd0c77..deb6e20 100644 --- a/src/openai/utils.ts +++ b/src/openai/utils.ts @@ -46,3 +46,8 @@ export function isReactElement( ): message is React.ReactElement { return React.isValidElement(message); } + +export function isAsyncGeneratorFunction(fn: unknown): fn is AsyncGenerator { + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator + return fn?.constructor?.name === 'AsyncGenerator'; +}