Skip to content

Commit 7de628e

Browse files
authored
feat (ui): add onToolCall callback to useChat (#1729)
1 parent a5c633c commit 7de628e

File tree

14 files changed

+410
-306
lines changed

14 files changed

+410
-306
lines changed

.changeset/swift-lamps-juggle.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'ai': patch
3+
---
4+
5+
chore (ui): deprecate old function/tool call handling

.changeset/three-foxes-dream.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'ai': patch
3+
---
4+
5+
feat (ui): add onToolCall handler to useChat
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,37 @@
11
---
2-
title: Chatbot with Tool Calling
3-
description: Learn how to use tool calling with the useChat hook.
2+
title: Chatbot with Tools
3+
description: Learn how to use tools with the useChat hook.
44
---
55

6-
# Chatbot with Tool Calling
6+
# Chatbot with Tools
77

88
<Note type="warning">
9-
The tool calling functionality described here is experimental. It is currently
10-
only available for React.
9+
The tool calling functionality described here currently only available for
10+
React.
1111
</Note>
1212

1313
With `useChat` and `streamText`, you can use tools in your chatbot application.
14-
The Vercel AI SDK supports both client and server side tool execution.
14+
The Vercel AI SDK supports three types of tools in this context:
15+
16+
1. Automatically executed server-side tools
17+
2. Automatically executed client-side tools
18+
3. Tools that require user interaction, such as confirmation dialogs
1519

1620
The flow is as follows:
1721

22+
1. The user enters a message in the chat UI.
23+
1. The message is sent to the API route.
24+
1. The messages from the client are converted to AI SDK Core messages using `convertToCoreMessages`.
1825
1. In your server side route, the language model generates tool calls during the `streamText` call.
19-
The messages from the client are converted to AI SDK Core messages using `convertToCoreMessages`.
20-
2. All tool calls are forwarded to the client.
21-
3. Server-side tools are executed using their `execute` method and their results are sent back to the client.
22-
4. The client-side tools are executed on the client.
23-
The results of client side tool calls can be appended to last assigned message using `experimental_addToolResult`.
24-
5. When all tool calls are resolved, the client sends the updated messages with the tool results back to the server, triggering another `streamText` call.
26+
1. All tool calls are forwarded to the client.
27+
1. Server-side tools are executed using their `execute` method and their results are forwarded to the client.
28+
1. Client-side tools that should be automatically executed are handled with the `onToolCall` callback.
29+
You can return the tool result from the callback.
30+
1. Client-side tool that require user interactions can be displayed in the UI.
31+
The tool calls and results are available in the `toolInvocations` property of the last assistant message.
32+
1. When the user interaction is done, `experimental_addToolResult` can be used to add the tool result to the chat.
33+
1. When there are tool calls in the last assistant message and all tool results are available, the client sends the updated messages back to the server.
34+
This triggers another iteration of this flow.
2535

2636
The tool call and tool executions are integrated into the assistant message as `toolInvocations`.
2737
A tool invocation is at first a tool call, and then it becomes a tool result when the tool is executed.
@@ -34,16 +44,15 @@ The tool result contains all information about the tool call as well as the resu
3444
for backward compatibility.
3545
</Note>
3646

37-
## Example: Client-Side Tool Execution
47+
## Example
3848

39-
In this example, we'll define two tools.
40-
The client-side tool is a confirmation dialog that asks the user to confirm the execution of the server-side tool.
41-
The server-side tool is a simple fake tool that restarts an engine.
49+
In this example, we'll use three tools:
4250

43-
### Server-side route
51+
- `getWeatherInformation`: An automatically executed server-side tool that returns the weather in a given city.
52+
- `askForConfirmation`: A user-interaction client-side tool that asks the user for confirmation.
53+
- `getLocation`: An automatically executed client-side tool that returns a random city.
4454

45-
Please note that only the `restartEngine` tool has an `execute` method and is executed on the server side.
46-
The `askForConfirmation` tool is executed on the client side.
55+
### API route
4756

4857
```tsx filename='app/api/chat/route.ts'
4958
import { openai } from '@ai-sdk/openai';
@@ -60,19 +69,30 @@ export async function POST(req: Request) {
6069
model: openai('gpt-4-turbo'),
6170
messages: convertToCoreMessages(messages),
6271
tools: {
63-
restartEngine: {
64-
description:
65-
'Restarts the engine. ' +
66-
'Always ask for confirmation before using this tool.',
67-
parameters: z.object({}),
68-
execute: async () => 'Engine restarted.',
72+
// server-side tool with execute function:
73+
getWeatherInformation: {
74+
description: 'show the weather in a given city to the user',
75+
parameters: z.object({ city: z.string() }),
76+
execute: async ({}: { city: string }) => {
77+
const weatherOptions = ['sunny', 'cloudy', 'rainy', 'snowy', 'windy'];
78+
return weatherOptions[
79+
Math.floor(Math.random() * weatherOptions.length)
80+
];
81+
},
6982
},
83+
// client-side tool that starts user interaction:
7084
askForConfirmation: {
7185
description: 'Ask the user for confirmation.',
7286
parameters: z.object({
7387
message: z.string().describe('The message to ask for confirmation.'),
7488
}),
7589
},
90+
// client-side tool that is automatically executed on the client:
91+
getLocation: {
92+
description:
93+
'Get the user location. Always ask for confirmation before using this tool.',
94+
parameters: z.object({}),
95+
},
7696
},
7797
});
7898

@@ -85,24 +105,43 @@ export async function POST(req: Request) {
85105
The client-side page uses the `useChat` hook to create a chatbot application with real-time message streaming.
86106
Tool invocations are displayed in the chat UI.
87107

88-
If the tool is a confirmation dialog, the user can confirm or deny the execution of the server-side tool.
89-
Once the user confirms the execution, the tool result is appended to the assistant message using `experimental_addToolResult`,
90-
and the server route is called again to execute the server-side tool.
108+
There are three things worth mentioning:
109+
110+
1. The `onToolCall` callback is used to handle client-side tools that should be automatically executed.
111+
In this example, the `getLocation` tool is a client-side tool that returns a random city.
112+
113+
1. The `toolInvocations` property of the last assistant message contains all tool calls and results.
114+
The client-side tool `askForConfirmation` is displayed in the UI.
115+
It asks the user for confirmation and displays the result once the user confirms or denies the execution.
116+
The result is added to the chat using `experimental_addToolResult`.
117+
118+
1. The `experimental_maxAutomaticRoundtrips` option is set to 5.
119+
This enables several tool use iterations between the client and the server.
91120

92121
```tsx filename='app/page.tsx'
93-
'use client';
122+
'use client'
94123

95-
import { ToolInvocation } from 'ai';
96-
import { Message, useChat } from 'ai/react';
124+
import { ToolInvocation } from 'ai'
125+
import { Message, useChat } from 'ai/react'
97126

98127
export default function Chat() {
99128
const {
100129
messages,
101130
input,
102131
handleInputChange,
103132
handleSubmit,
104-
experimental_addToolResult,
105-
} = useChat();
133+
experimental_addToolResult
134+
} = useChat({
135+
experimental_maxAutomaticRoundtrips: 5,
136+
137+
// run client-side tools that are automatically executed:
138+
async onToolCall({ toolCall }) {
139+
if (toolCall.toolName === 'getLocation') {
140+
const cities = ['New York', 'Los Angeles', 'Chicago', 'San Francisco']
141+
return cities[Math.floor(Math.random() * cities.length)]
142+
}
143+
}
144+
})
106145

107146
return (
108147
<div>
@@ -111,9 +150,9 @@ export default function Chat() {
111150
<strong>{`${m.role}: `}</strong>
112151
{m.content}
113152
{m.toolInvocations?.map((toolInvocation: ToolInvocation) => {
114-
const toolCallId = toolInvocation.toolCallId;
153+
const toolCallId = toolInvocation.toolCallId
115154

116-
// render confirmation tool
155+
// render confirmation tool (client-side tool with user interaction)
117156
if (toolInvocation.toolName === 'askForConfirmation') {
118157
return (
119158
<div key={toolCallId}>
@@ -127,7 +166,7 @@ export default function Chat() {
127166
onClick={() =>
128167
experimental_addToolResult({
129168
toolCallId,
130-
result: 'Yes, confirmed.',
169+
result: 'Yes, confirmed.'
131170
})
132171
}
133172
>
@@ -137,7 +176,7 @@ export default function Chat() {
137176
onClick={() =>
138177
experimental_addToolResult({
139178
toolCallId,
140-
result: 'No, denied',
179+
result: 'No, denied'
141180
})
142181
}
143182
>
@@ -147,123 +186,34 @@ export default function Chat() {
147186
)}
148187
</div>
149188
</div>
150-
);
189+
)
151190
}
152191

153192
// other tools:
154193
return 'result' in toolInvocation ? (
155-
<div key={toolCallId}>
156-
<strong>{`${toolInvocation.toolName}:`}</strong>
194+
<div key={toolCallId}
195+
Tool call {`${toolInvocation.toolName}: `}
157196
{toolInvocation.result}
158197
</div>
159198
) : (
160-
<div key={toolCallId}>Calling {toolInvocation.toolName}...</div>
161-
);
199+
<div key={toolCallId}
200+
Calling {toolInvocation.toolName}...
201+
</div>
202+
)
162203
})}
163204
<br />
164205
<br />
165206
</div>
166207
))}
167208

168209
<form onSubmit={handleSubmit}>
169-
<input value={input} onChange={handleInputChange} />
170-
</form>
171-
</div>
172-
);
173-
}
174-
```
175-
176-
## Example: Server-Side Tool Execution with Roundtrips
177-
178-
In this example, we'll define a weather tool that shows the weather in a given city.
179-
180-
When asked about the weather, the assistant will call the weather tool to get the weather information.
181-
The server will then respond with the weather information tool results.
182-
183-
Once the client receives all tool results, it will send the updated messages back to the server for another roundtrip.
184-
The server will then generate a streaming text response that uses the information from the weather tool results.
185-
186-
### Server-side route
187-
188-
The server-side route defines a weather tool that returns the weather in a given city.
189-
190-
```tsx filename='app/api/chat/route.ts'
191-
import { openai } from '@ai-sdk/openai';
192-
import { convertToCoreMessages, streamText } from 'ai';
193-
import { z } from 'zod';
194-
195-
export const dynamic = 'force-dynamic';
196-
export const maxDuration = 60;
197-
198-
export async function POST(req: Request) {
199-
const { messages } = await req.json();
200-
201-
const result = await streamText({
202-
model: openai('gpt-4-turbo'),
203-
system:
204-
'You are a weather bot that can use the weather tool ' +
205-
'to get the weather in a given city. ' +
206-
'Respond to the user with weather information in a friendly ' +
207-
'and helpful manner.',
208-
messages: convertToCoreMessages(messages),
209-
tools: {
210-
weather: {
211-
description: 'show the weather in a given city to the user',
212-
parameters: z.object({ city: z.string() }),
213-
execute: async ({}: { city: string }) => {
214-
// Random weather:
215-
const weatherOptions = ['sunny', 'cloudy', 'rainy', 'snowy', 'windy'];
216-
return weatherOptions[
217-
Math.floor(Math.random() * weatherOptions.length)
218-
];
219-
},
220-
},
221-
},
222-
});
223-
224-
return result.toAIStreamResponse();
225-
}
226-
```
227-
228-
### Client-side page
229-
230-
The page uses the `useChat` hook to create a chatbot application with real-time message streaming.
231-
We set `experimental_maxAutomaticRoundtrips` to 2 to automatically
232-
send another request to the server when all server-side tool results are received.
233-
234-
<Note>
235-
The `experimental_maxAutomaticRoundtrips` option is disabled by default for
236-
backward compatibility. It also limits the number of automatic roundtrips to
237-
prevent infinite client-server call loops.
238-
</Note>
239-
240-
```tsx filename='app/page.tsx'
241-
'use client';
242-
243-
import { useChat } from 'ai/react';
244-
245-
export default function Chat() {
246-
const { messages, input, handleInputChange, handleSubmit } = useChat({
247-
experimental_maxAutomaticRoundtrips: 2,
248-
});
249-
250-
return (
251-
<div>
252-
{messages
253-
.filter(m => m.content) // filter out empty messages
254-
.map(m => (
255-
<div key={m.id}>
256-
<strong>{`${m.role}: `}</strong>
257-
{m.content}
258-
<br />
259-
<br />
260-
</div>
261-
))}
262-
263-
<form onSubmit={handleSubmit}>
264-
<input value={input} onChange={handleInputChange} />
210+
<input
211+
value={input}
212+
placeholder="Say something..."
213+
onChange={handleInputChange}
214+
/>
265215
</form>
266216
</div>
267-
);
217+
)
268218
}
269219
```

0 commit comments

Comments
 (0)