-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat (ai/core): add streamText sendStart & sendFinish data stream opt…
…ions (#5047) Co-authored-by: Nico Albanese <[email protected]>
- Loading branch information
1 parent
49ee53a
commit 0cb2647
Showing
10 changed files
with
393 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'ai': patch | ||
--- | ||
|
||
feat (ai/core): add streamText sendStart & sendFinish data stream options |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
--- | ||
title: streamText Multi-Step Cookbook | ||
description: Learn how to create several streamText steps with different settings | ||
tags: ['next', 'streaming'] | ||
--- | ||
|
||
# Stream Text Multi-Step | ||
|
||
You may want to have different steps in your stream where each step has different settings, | ||
e.g. models, tools, or system prompts. | ||
|
||
With `createDataStreamResponse` and `sendFinish` / `sendStart` options when merging | ||
into the data stream, you can control when the finish and start events are sent to the client, | ||
allowing you to have different steps in a single assistant UI message. | ||
|
||
## Server | ||
|
||
```typescript filename='app/api/chat/route.ts' | ||
import { openai } from '@ai-sdk/openai'; | ||
import { createDataStreamResponse, streamText, tool } from 'ai'; | ||
import { z } from 'zod'; | ||
|
||
export async function POST(req: Request) { | ||
const { messages } = await req.json(); | ||
|
||
return createDataStreamResponse({ | ||
execute: async dataStream => { | ||
// step 1 example: forced tool call | ||
const result1 = streamText({ | ||
model: openai('gpt-4o-mini', { structuredOutputs: true }), | ||
system: 'Extract the user goal from the conversation.', | ||
messages, | ||
toolChoice: 'required', // force the model to call a tool | ||
tools: { | ||
extractGoal: tool({ | ||
parameters: z.object({ goal: z.string() }), | ||
execute: async ({ goal }) => goal, // no-op extract tool | ||
}), | ||
}, | ||
}); | ||
|
||
// forward the initial result to the client without the finish event: | ||
result1.mergeIntoDataStream(dataStream, { | ||
experimental_sendFinish: false, // omit the finish event | ||
}); | ||
|
||
// note: you can use any programming construct here, e.g. if-else, loops, etc. | ||
// workflow programming is normal programming with this approach. | ||
|
||
// example: continue stream with forced tool call from previous step | ||
const result2 = streamText({ | ||
// different system prompt, different model, no tools: | ||
model: openai('gpt-4o'), | ||
system: | ||
'You are a helpful assistant with a different system prompt. Repeat the extract user goal in your answer.', | ||
// continue the workflow stream with the messages from the previous step: | ||
messages: [...messages, ...(await result1.response).messages], | ||
}); | ||
|
||
// forward the 2nd result to the client (incl. the finish event): | ||
result2.mergeIntoDataStream(dataStream, { | ||
experimental_sendStart: false, // omit the start event | ||
}); | ||
}, | ||
}); | ||
} | ||
``` | ||
|
||
## Client | ||
|
||
```tsx filename="app/page.tsx" | ||
'use client'; | ||
|
||
import { useChat } from '@ai-sdk/react'; | ||
|
||
export default function Chat() { | ||
const { messages, input, handleInputChange, handleSubmit } = useChat(); | ||
|
||
return ( | ||
<div> | ||
{messages?.map(message => ( | ||
<div key={message.id}> | ||
<strong>{`${message.role}: `}</strong> | ||
{message.parts.map((part, index) => { | ||
switch (part.type) { | ||
case 'text': | ||
return <span key={index}>{part.text}</span>; | ||
case 'tool-invocation': { | ||
return ( | ||
<pre key={index}> | ||
{JSON.stringify(part.toolInvocation, null, 2)} | ||
</pre> | ||
); | ||
} | ||
} | ||
})} | ||
</div> | ||
))} | ||
<form onSubmit={handleSubmit}> | ||
<input value={input} onChange={handleInputChange} /> | ||
</form> | ||
</div> | ||
); | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
48 changes: 48 additions & 0 deletions
48
examples/next-openai/app/api/use-chat-streamdata-multistep/route.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { openai } from '@ai-sdk/openai'; | ||
import { createDataStreamResponse, streamText, tool } from 'ai'; | ||
import { z } from 'zod'; | ||
|
||
export async function POST(req: Request) { | ||
const { messages } = await req.json(); | ||
|
||
return createDataStreamResponse({ | ||
execute: async dataStream => { | ||
// step 1 example: forced tool call | ||
const result1 = streamText({ | ||
model: openai('gpt-4o-mini', { structuredOutputs: true }), | ||
system: 'Extract the user goal from the conversation.', | ||
messages, | ||
toolChoice: 'required', // force the model to call a tool | ||
tools: { | ||
extractGoal: tool({ | ||
parameters: z.object({ goal: z.string() }), | ||
execute: async ({ goal }) => goal, // no-op extract tool | ||
}), | ||
}, | ||
}); | ||
|
||
// forward the initial result to the client without the finish event: | ||
result1.mergeIntoDataStream(dataStream, { | ||
experimental_sendFinish: false, // omit the finish event | ||
}); | ||
|
||
// note: you can use any programming construct here, e.g. if-else, loops, etc. | ||
// workflow programming is normal programming with this approach. | ||
|
||
// example: continue stream with forced tool call from previous step | ||
const result2 = streamText({ | ||
// different system prompt, different model, no tools: | ||
model: openai('gpt-4o'), | ||
system: | ||
'You are a helpful assistant with a different system prompt. Repeat the extract user goal in your answer.', | ||
// continue the workflow stream with the messages from the previous step: | ||
messages: [...messages, ...(await result1.response).messages], | ||
}); | ||
|
||
// forward the 2nd result to the client (incl. the finish event): | ||
result2.mergeIntoDataStream(dataStream, { | ||
experimental_sendStart: false, // omit the start event | ||
}); | ||
}, | ||
}); | ||
} |
61 changes: 61 additions & 0 deletions
61
examples/next-openai/app/use-chat-streamdata-multistep/page.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
'use client'; | ||
|
||
import { useChat } from '@ai-sdk/react'; | ||
|
||
export default function Chat() { | ||
const { messages, input, handleInputChange, handleSubmit, data, setData } = | ||
useChat({ api: '/api/use-chat-streamdata-multistep' }); | ||
|
||
return ( | ||
<div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> | ||
{data && ( | ||
<> | ||
<pre className="p-4 text-sm bg-gray-100"> | ||
{JSON.stringify(data, null, 2)} | ||
</pre> | ||
<button | ||
onClick={() => setData(undefined)} | ||
className="px-4 py-2 mt-2 text-white bg-blue-500 rounded hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50" | ||
> | ||
Clear Data | ||
</button> | ||
</> | ||
)} | ||
|
||
{messages?.map(message => ( | ||
<div key={message.id} className="whitespace-pre-wrap"> | ||
<strong>{`${message.role}: `}</strong> | ||
{message.parts.map((part, index) => { | ||
switch (part.type) { | ||
case 'text': | ||
return <span key={index}>{part.text}</span>; | ||
case 'tool-invocation': { | ||
return ( | ||
<pre key={index}> | ||
{JSON.stringify(part.toolInvocation, null, 2)} | ||
</pre> | ||
); | ||
} | ||
} | ||
})} | ||
<br /> | ||
<br /> | ||
</div> | ||
))} | ||
|
||
<form | ||
onSubmit={e => { | ||
setData(undefined); // clear stream data | ||
handleSubmit(e); | ||
}} | ||
> | ||
<input | ||
className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl" | ||
value={input} | ||
placeholder="Say something..." | ||
onChange={handleInputChange} | ||
/> | ||
</form> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.