Skip to content

Commit f617b97

Browse files
authored
client/server tool invocations with useChat and streamText (#1514)
1 parent fcf24fc commit f617b97

File tree

16 files changed

+743
-102
lines changed

16 files changed

+743
-102
lines changed

‎.changeset/good-feet-accept.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'ai': patch
3+
---
4+
5+
feat (ai): support client/server tool calls with useChat and streamText
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { openai } from '@ai-sdk/openai';
2+
import { convertToCoreMessages, streamText } from 'ai';
3+
import { z } from 'zod';
4+
5+
export const dynamic = 'force-dynamic';
6+
export const maxDuration = 60;
7+
8+
export async function POST(req: Request) {
9+
const { messages } = await req.json();
10+
11+
const result = await streamText({
12+
model: openai('gpt-4-turbo'),
13+
messages: convertToCoreMessages(messages),
14+
tools: {
15+
restartEngine: {
16+
description:
17+
'Restarts the engine. Always ask for confirmation before using this tool.',
18+
parameters: z.object({}),
19+
execute: async () => 'Engine restarted.',
20+
},
21+
askForConfirmation: {
22+
description: 'Ask the user for confirmation.',
23+
parameters: z.object({
24+
message: z.string().describe('The message to ask for confirmation.'),
25+
}),
26+
},
27+
},
28+
});
29+
30+
return result.toAIStreamResponse();
31+
}

‎examples/next-openai/app/api/use-chat-tool-call-ui/route.ts‎

Lines changed: 0 additions & 48 deletions
This file was deleted.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { openai } from '@ai-sdk/openai';
2+
import { streamText } from 'ai';
3+
import { z } from 'zod';
4+
5+
export const dynamic = 'force-dynamic';
6+
export const maxDuration = 60;
7+
8+
export async function POST(req: Request) {
9+
const { messages } = await req.json();
10+
11+
const result = await streamText({
12+
model: openai('gpt-4-turbo'),
13+
messages,
14+
tools: {
15+
weather: {
16+
description: 'show the weather in a given city to the user',
17+
parameters: z.object({ city: z.string() }),
18+
execute: async ({}: { city: string }) => {
19+
// Random delay between 1000ms (1s) and 3000ms (3s):
20+
const delay = Math.floor(Math.random() * (3000 - 1000 + 1)) + 1000;
21+
await new Promise(resolve => setTimeout(resolve, delay));
22+
23+
// Random weather:
24+
const weatherOptions = ['sunny', 'cloudy', 'rainy', 'snowy', 'windy'];
25+
return weatherOptions[
26+
Math.floor(Math.random() * weatherOptions.length)
27+
];
28+
},
29+
},
30+
},
31+
});
32+
33+
return result.toAIStreamResponse();
34+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
'use client';
2+
3+
import { ToolInvocation } from 'ai';
4+
import { Message, useChat } from 'ai/react';
5+
6+
export default function Chat() {
7+
const {
8+
messages,
9+
input,
10+
handleInputChange,
11+
handleSubmit,
12+
experimental_addToolResult,
13+
} = useChat({ api: '/api/use-chat-client-tool' });
14+
15+
return (
16+
<div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
17+
{messages?.map((m: Message) => (
18+
<div key={m.id} className="whitespace-pre-wrap">
19+
<strong>{`${m.role}: `}</strong>
20+
{m.content}
21+
{m.toolInvocations?.map((toolInvocation: ToolInvocation) => {
22+
const toolCallId = toolInvocation.toolCallId;
23+
24+
// render confirmation tool
25+
if (toolInvocation.toolName === 'askForConfirmation') {
26+
return (
27+
<div key={toolCallId} className="text-gray-500">
28+
{toolInvocation.args.message}
29+
<div className="flex gap-2">
30+
{'result' in toolInvocation ? (
31+
<b>{toolInvocation.result}</b>
32+
) : (
33+
<>
34+
<button
35+
className="px-4 py-2 font-bold text-white bg-blue-500 rounded hover:bg-blue-700"
36+
onClick={() =>
37+
experimental_addToolResult({
38+
toolCallId,
39+
result: 'Yes, confirmed.',
40+
})
41+
}
42+
>
43+
Yes
44+
</button>
45+
<button
46+
className="px-4 py-2 font-bold text-white bg-red-500 rounded hover:bg-red-700"
47+
onClick={() =>
48+
experimental_addToolResult({
49+
toolCallId,
50+
result: 'No, denied',
51+
})
52+
}
53+
>
54+
No
55+
</button>
56+
</>
57+
)}
58+
</div>
59+
</div>
60+
);
61+
}
62+
63+
// other tools:
64+
return 'result' in toolInvocation ? (
65+
<div key={toolCallId} className="text-gray-500">
66+
<strong>{`${toolInvocation.toolName}: `}</strong>
67+
{toolInvocation.result}
68+
</div>
69+
) : (
70+
<div key={toolCallId} className="text-gray-500">
71+
Calling {toolInvocation.toolName}...
72+
</div>
73+
);
74+
})}
75+
<br />
76+
<br />
77+
</div>
78+
))}
79+
80+
<form onSubmit={handleSubmit}>
81+
<input
82+
className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl"
83+
value={input}
84+
placeholder="Say something..."
85+
onChange={handleInputChange}
86+
/>
87+
</form>
88+
</div>
89+
);
90+
}

‎examples/next-openai/app/use-chat-tool-call-ui/page.tsx‎

Lines changed: 0 additions & 35 deletions
This file was deleted.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
'use client';
2+
3+
import { ToolInvocation } from 'ai';
4+
import { useChat } from 'ai/react';
5+
6+
export default function Chat() {
7+
const { messages, input, handleInputChange, handleSubmit } = useChat({
8+
api: '/api/use-chat-tool-result',
9+
});
10+
11+
return (
12+
<div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
13+
{messages.map(m => (
14+
<div key={m.id} className="whitespace-pre-wrap">
15+
<strong>{`${m.role}: `}</strong>
16+
{m.content}
17+
{m.toolInvocations?.map((toolInvocation: ToolInvocation) => {
18+
if (toolInvocation.toolName === 'weather') {
19+
const { city } = toolInvocation.args;
20+
return (
21+
<i key={toolInvocation.toolCallId} className="before:block">
22+
{'result' in toolInvocation
23+
? `Weather in ${city}: ${toolInvocation.result}`
24+
: `Calling weather tool for ${city}...`}
25+
</i>
26+
);
27+
}
28+
})}
29+
<br />
30+
<br />
31+
</div>
32+
))}
33+
34+
<form onSubmit={handleSubmit}>
35+
<input
36+
className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl"
37+
value={input}
38+
placeholder="Say something..."
39+
onChange={handleInputChange}
40+
/>
41+
</form>
42+
</div>
43+
);
44+
}

0 commit comments

Comments
 (0)