Skip to content

Commit ceb44bc

Browse files
authoredMay 10, 2024··
feat (ai/ui): add stop() helper to useAssistant (#1524)
1 parent 1b9e2fb commit ceb44bc

File tree

8 files changed

+90
-29
lines changed

8 files changed

+90
-29
lines changed
 

‎.changeset/light-chairs-clap.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'ai': patch
3+
---
4+
5+
feat (ai/ui): add stop() helper to useAssistant (important: AssistantResponse now requires OpenAI SDK 4.42+)

‎examples/next-openai/app/api/assistant/route.ts

+20-11
Original file line numberDiff line numberDiff line change
@@ -27,22 +27,30 @@ export async function POST(req: Request) {
2727
const threadId = input.threadId ?? (await openai.beta.threads.create({})).id;
2828

2929
// Add a message to the thread
30-
const createdMessage = await openai.beta.threads.messages.create(threadId, {
31-
role: 'user',
32-
content: input.message,
33-
});
30+
const createdMessage = await openai.beta.threads.messages.create(
31+
threadId,
32+
{
33+
role: 'user',
34+
content: input.message,
35+
},
36+
{ signal: req.signal },
37+
);
3438

3539
return AssistantResponse(
3640
{ threadId, messageId: createdMessage.id },
3741
async ({ forwardStream, sendDataMessage }) => {
3842
// Run the assistant on the thread
39-
const runStream = openai.beta.threads.runs.createAndStream(threadId, {
40-
assistant_id:
41-
process.env.ASSISTANT_ID ??
42-
(() => {
43-
throw new Error('ASSISTANT_ID is not set');
44-
})(),
45-
});
43+
const runStream = openai.beta.threads.runs.stream(
44+
threadId,
45+
{
46+
assistant_id:
47+
process.env.ASSISTANT_ID ??
48+
(() => {
49+
throw new Error('ASSISTANT_ID is not set');
50+
})(),
51+
},
52+
{ signal: req.signal },
53+
);
4654

4755
// forward run status would stream message deltas
4856
let runResult = await forwardStream(runStream);
@@ -108,6 +116,7 @@ export async function POST(req: Request) {
108116
threadId,
109117
runResult.id,
110118
{ tool_outputs },
119+
{ signal: req.signal },
111120
),
112121
);
113122
}

‎examples/next-openai/app/assistant/page.tsx

+17-3
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,15 @@ const roleToColorMap: Record<Message['role'], string> = {
1313
};
1414

1515
export default function Chat() {
16-
const { status, messages, input, submitMessage, handleInputChange, error } =
17-
useAssistant({ api: '/api/assistant' });
16+
const {
17+
status,
18+
messages,
19+
input,
20+
submitMessage,
21+
handleInputChange,
22+
error,
23+
stop,
24+
} = useAssistant({ api: '/api/assistant' });
1825

1926
// When status changes to accepting messages, focus the input:
2027
const inputRef = useRef<HTMLInputElement>(null);
@@ -64,12 +71,19 @@ export default function Chat() {
6471
<input
6572
ref={inputRef}
6673
disabled={status !== 'awaiting_message'}
67-
className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl"
74+
className="fixed w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl bottom-14 ax-w-md"
6875
value={input}
6976
placeholder="What is the temperature in the living room?"
7077
onChange={handleInputChange}
7178
/>
7279
</form>
80+
81+
<button
82+
className="fixed bottom-0 w-full max-w-md p-2 mb-8 text-white bg-red-500 rounded-lg"
83+
onClick={stop}
84+
>
85+
Stop
86+
</button>
7387
</div>
7488
);
7589
}

‎examples/next-openai/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"@ai-sdk/openai": "latest",
1313
"ai": "latest",
1414
"next": "latest",
15-
"openai": "4.29.0",
15+
"openai": "4.42.0",
1616
"react": "18.2.0",
1717
"react-dom": "^18.2.0",
1818
"zod": "3.23.4"

‎packages/core/package.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@
117117
"jsdom": "^23.0.0",
118118
"langchain": "0.0.196",
119119
"msw": "2.0.9",
120-
"openai": "4.29.0",
120+
"openai": "4.42.0",
121121
"react-dom": "^18.2.0",
122122
"react-server-dom-webpack": "18.3.0-canary-eb33bd747-20240312",
123123
"solid-js": "^1.8.7",
@@ -127,6 +127,7 @@
127127
"zod": "3.22.4"
128128
},
129129
"peerDependencies": {
130+
"openai": "^4.42.0",
130131
"react": "^18.2.0",
131132
"solid-js": "^1.7.7",
132133
"svelte": "^3.0.0 || ^4.0.0",
@@ -148,6 +149,9 @@
148149
},
149150
"zod": {
150151
"optional": true
152+
},
153+
"openai": {
154+
"optional": true
151155
}
152156
},
153157
"engines": {

‎packages/core/react/use-assistant.ts

+33-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
/* eslint-disable react-hooks/rules-of-hooks */
22

3-
import { useState } from 'react';
4-
3+
import { isAbortError } from '@ai-sdk/provider-utils';
4+
import { useCallback, useRef, useState } from 'react';
55
import { generateId } from '../shared/generate-id';
66
import { readDataStream } from '../shared/read-data-stream';
77
import { CreateMessage, Message } from '../shared/types';
8+
import { abort } from 'node:process';
89

910
export type AssistantStatus = 'in_progress' | 'awaiting_message';
1011

@@ -42,6 +43,11 @@ export type UseAssistantHelpers = {
4243
},
4344
) => Promise<void>;
4445

46+
/**
47+
Abort the current request immediately, keep the generated tokens if any.
48+
*/
49+
stop: () => void;
50+
4551
/**
4652
* setState-powered method to update the input value.
4753
*/
@@ -135,6 +141,16 @@ export function useAssistant({
135141
setInput(event.target.value);
136142
};
137143

144+
// Abort controller to cancel the current API call.
145+
const abortControllerRef = useRef<AbortController | null>(null);
146+
147+
const stop = useCallback(() => {
148+
if (abortControllerRef.current) {
149+
abortControllerRef.current.abort();
150+
abortControllerRef.current = null;
151+
}
152+
}, []);
153+
138154
const append = async (
139155
message: Message | CreateMessage,
140156
requestOptions?: {
@@ -153,10 +169,15 @@ export function useAssistant({
153169

154170
setInput('');
155171

172+
const abortController = new AbortController();
173+
156174
try {
175+
abortControllerRef.current = abortController;
176+
157177
const result = await fetch(api, {
158178
method: 'POST',
159179
credentials,
180+
signal: abortController.signal,
160181
headers: { 'Content-Type': 'application/json', ...headers },
161182
body: JSON.stringify({
162183
...body,
@@ -240,14 +261,21 @@ export function useAssistant({
240261
}
241262
}
242263
} catch (error) {
264+
// Ignore abort errors as they are expected when the user cancels the request:
265+
if (isAbortError(error) && abortController.signal.aborted) {
266+
abortControllerRef.current = null;
267+
return;
268+
}
269+
243270
if (onError && error instanceof Error) {
244271
onError(error);
245272
}
246273

247274
setError(error as Error);
275+
} finally {
276+
abortControllerRef.current = null;
277+
setStatus('awaiting_message');
248278
}
249-
250-
setStatus('awaiting_message');
251279
};
252280

253281
const submitMessage = async (
@@ -276,6 +304,7 @@ export function useAssistant({
276304
submitMessage,
277305
status,
278306
error,
307+
stop,
279308
};
280309
}
281310

‎packages/core/streams/assistant-response.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AssistantStream } from 'openai/lib/AssistantStream';
1+
import { type AssistantStream } from 'openai/lib/AssistantStream';
22
import { Run } from 'openai/resources/beta/threads/runs/runs';
33
import { formatStreamPart } from '../shared/stream-parts';
44
import { AssistantMessage, DataMessage } from '../shared/types';

‎pnpm-lock.yaml

+8-8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
Please sign in to comment.