@ai-sdk/svelte 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,251 @@
1
+ import { isAbortError } from '@ai-sdk/provider-utils';
2
+ import type {
3
+ AssistantStatus,
4
+ CreateMessage,
5
+ Message,
6
+ UseAssistantOptions,
7
+ } from '@ai-sdk/ui-utils';
8
+ import { generateId, readDataStream } from '@ai-sdk/ui-utils';
9
+ import { Readable, Writable, get, writable } from 'svelte/store';
10
+
11
+ let uniqueId = 0;
12
+
13
+ const store: Record<string, any> = {};
14
+
15
+ export type UseAssistantHelpers = {
16
+ /**
17
+ * The current array of chat messages.
18
+ */
19
+ messages: Readable<Message[]>;
20
+
21
+ /**
22
+ * Update the message store with a new array of messages.
23
+ */
24
+ setMessages: (messages: Message[]) => void;
25
+
26
+ /**
27
+ * The current thread ID.
28
+ */
29
+ threadId: Readable<string | undefined>;
30
+
31
+ /**
32
+ * The current value of the input field.
33
+ */
34
+ input: Writable<string>;
35
+
36
+ /**
37
+ * Append a user message to the chat list. This triggers the API call to fetch
38
+ * the assistant's response.
39
+ * @param message The message to append
40
+ * @param requestOptions Additional options to pass to the API call
41
+ */
42
+ append: (
43
+ message: Message | CreateMessage,
44
+ requestOptions?: { data?: Record<string, string> },
45
+ ) => Promise<void>;
46
+
47
+ /**
48
+ Abort the current request immediately, keep the generated tokens if any.
49
+ */
50
+ stop: () => void;
51
+
52
+ /**
53
+ * Form submission handler that automatically resets the input field and appends a user message.
54
+ */
55
+ submitMessage: (
56
+ e: any,
57
+ requestOptions?: { data?: Record<string, string> },
58
+ ) => Promise<void>;
59
+
60
+ /**
61
+ * The current status of the assistant. This can be used to show a loading indicator.
62
+ */
63
+ status: Readable<AssistantStatus>;
64
+
65
+ /**
66
+ * The error thrown during the assistant message processing, if any.
67
+ */
68
+ error: Readable<undefined | Error>;
69
+ };
70
+
71
+ export function useAssistant({
72
+ api,
73
+ threadId: threadIdParam,
74
+ credentials,
75
+ headers,
76
+ body,
77
+ onError,
78
+ }: UseAssistantOptions): UseAssistantHelpers {
79
+ // Generate a unique thread ID
80
+ const threadIdStore = writable<string | undefined>(threadIdParam);
81
+
82
+ // Initialize message, input, status, and error stores
83
+ const key = `${api}|${threadIdParam ?? `completion-${uniqueId++}`}`;
84
+ const messages = writable<Message[]>(store[key] || []);
85
+ const input = writable('');
86
+ const status = writable<AssistantStatus>('awaiting_message');
87
+ const error = writable<undefined | Error>(undefined);
88
+
89
+ // To manage aborting the current fetch request
90
+ let abortController: AbortController | null = null;
91
+
92
+ // Update the message store
93
+ const mutateMessages = (newMessages: Message[]) => {
94
+ store[key] = newMessages;
95
+ messages.set(newMessages);
96
+ };
97
+
98
+ // Function to handle API calls and state management
99
+ async function append(
100
+ message: Message | CreateMessage,
101
+ requestOptions?: { data?: Record<string, string> },
102
+ ) {
103
+ status.set('in_progress');
104
+ abortController = new AbortController(); // Initialize a new AbortController
105
+
106
+ // Add the new message to the existing array
107
+ mutateMessages([
108
+ ...get(messages),
109
+ { ...message, id: message.id ?? generateId() },
110
+ ]);
111
+
112
+ input.set('');
113
+
114
+ try {
115
+ const result = await fetch(api, {
116
+ method: 'POST',
117
+ credentials,
118
+ signal: abortController.signal,
119
+ headers: { 'Content-Type': 'application/json', ...headers },
120
+ body: JSON.stringify({
121
+ ...body,
122
+ // always use user-provided threadId when available:
123
+ threadId: threadIdParam ?? get(threadIdStore) ?? null,
124
+ message: message.content,
125
+
126
+ // optional request data:
127
+ data: requestOptions?.data,
128
+ }),
129
+ });
130
+
131
+ if (result.body == null) {
132
+ throw new Error('The response body is empty.');
133
+ }
134
+
135
+ // Read the streamed response data
136
+ for await (const { type, value } of readDataStream(
137
+ result.body.getReader(),
138
+ )) {
139
+ switch (type) {
140
+ case 'assistant_message': {
141
+ mutateMessages([
142
+ ...get(messages),
143
+ {
144
+ id: value.id,
145
+ role: value.role,
146
+ content: value.content[0].text.value,
147
+ },
148
+ ]);
149
+ break;
150
+ }
151
+
152
+ case 'text': {
153
+ // text delta - add to last message:
154
+ mutateMessages(
155
+ get(messages).map((msg, index, array) => {
156
+ if (index === array.length - 1) {
157
+ return { ...msg, content: msg.content + value };
158
+ }
159
+ return msg;
160
+ }),
161
+ );
162
+ break;
163
+ }
164
+
165
+ case 'data_message': {
166
+ mutateMessages([
167
+ ...get(messages),
168
+ {
169
+ id: value.id ?? generateId(),
170
+ role: 'data',
171
+ content: '',
172
+ data: value.data,
173
+ },
174
+ ]);
175
+ break;
176
+ }
177
+
178
+ case 'assistant_control_data': {
179
+ threadIdStore.set(value.threadId);
180
+
181
+ mutateMessages(
182
+ get(messages).map((msg, index, array) => {
183
+ if (index === array.length - 1) {
184
+ return { ...msg, id: value.messageId };
185
+ }
186
+ return msg;
187
+ }),
188
+ );
189
+
190
+ break;
191
+ }
192
+
193
+ case 'error': {
194
+ error.set(new Error(value));
195
+ break;
196
+ }
197
+ }
198
+ }
199
+ } catch (err) {
200
+ // Ignore abort errors as they are expected when the user cancels the request:
201
+ if (isAbortError(error) && abortController?.signal?.aborted) {
202
+ abortController = null;
203
+ return;
204
+ }
205
+
206
+ if (onError && err instanceof Error) {
207
+ onError(err);
208
+ }
209
+
210
+ error.set(err as Error);
211
+ } finally {
212
+ abortController = null;
213
+ status.set('awaiting_message');
214
+ }
215
+ }
216
+
217
+ function setMessages(messages: Message[]) {
218
+ mutateMessages(messages);
219
+ }
220
+
221
+ function stop() {
222
+ if (abortController) {
223
+ abortController.abort();
224
+ abortController = null;
225
+ }
226
+ }
227
+
228
+ // Function to handle form submission
229
+ async function submitMessage(
230
+ e: any,
231
+ requestOptions?: { data?: Record<string, string> },
232
+ ) {
233
+ e.preventDefault();
234
+ const inputValue = get(input);
235
+ if (!inputValue) return;
236
+
237
+ await append({ role: 'user', content: inputValue }, requestOptions);
238
+ }
239
+
240
+ return {
241
+ messages,
242
+ error,
243
+ threadId: threadIdStore,
244
+ input,
245
+ append,
246
+ submitMessage,
247
+ status,
248
+ setMessages,
249
+ stop,
250
+ };
251
+ }
@@ -0,0 +1,376 @@
1
+ import type {
2
+ ChatRequest,
3
+ ChatRequestOptions,
4
+ CreateMessage,
5
+ IdGenerator,
6
+ JSONValue,
7
+ Message,
8
+ UseChatOptions,
9
+ } from '@ai-sdk/ui-utils';
10
+ import {
11
+ callChatApi,
12
+ generateId as generateIdFunc,
13
+ processChatStream,
14
+ } from '@ai-sdk/ui-utils';
15
+ import { useSWR } from 'sswr';
16
+ import { Readable, Writable, derived, get, writable } from 'svelte/store';
17
+ export type { CreateMessage, Message, UseChatOptions };
18
+
19
+ export type UseChatHelpers = {
20
+ /** Current messages in the chat */
21
+ messages: Readable<Message[]>;
22
+ /** The error object of the API request */
23
+ error: Readable<undefined | Error>;
24
+ /**
25
+ * Append a user message to the chat list. This triggers the API call to fetch
26
+ * the assistant's response.
27
+ * @param message The message to append
28
+ * @param chatRequestOptions Additional options to pass to the API call
29
+ */
30
+ append: (
31
+ message: Message | CreateMessage,
32
+ chatRequestOptions?: ChatRequestOptions,
33
+ ) => Promise<string | null | undefined>;
34
+ /**
35
+ * Reload the last AI chat response for the given chat history. If the last
36
+ * message isn't from the assistant, it will request the API to generate a
37
+ * new response.
38
+ */
39
+ reload: (
40
+ chatRequestOptions?: ChatRequestOptions,
41
+ ) => Promise<string | null | undefined>;
42
+ /**
43
+ * Abort the current request immediately, keep the generated tokens if any.
44
+ */
45
+ stop: () => void;
46
+ /**
47
+ * Update the `messages` state locally. This is useful when you want to
48
+ * edit the messages on the client, and then trigger the `reload` method
49
+ * manually to regenerate the AI response.
50
+ */
51
+ setMessages: (messages: Message[]) => void;
52
+ /** The current value of the input */
53
+ input: Writable<string>;
54
+ /** Form submission handler to automatically reset input and append a user message */
55
+ handleSubmit: (e: any, chatRequestOptions?: ChatRequestOptions) => void;
56
+ metadata?: Object;
57
+ /** Whether the API request is in progress */
58
+ isLoading: Readable<boolean | undefined>;
59
+
60
+ /** Additional data added on the server via StreamData */
61
+ data: Readable<JSONValue[] | undefined>;
62
+ };
63
+ const getStreamedResponse = async (
64
+ api: string,
65
+ chatRequest: ChatRequest,
66
+ mutate: (messages: Message[]) => void,
67
+ mutateStreamData: (data: JSONValue[] | undefined) => void,
68
+ existingData: JSONValue[] | undefined,
69
+ extraMetadata: {
70
+ credentials?: RequestCredentials;
71
+ headers?: Record<string, string> | Headers;
72
+ body?: any;
73
+ },
74
+ previousMessages: Message[],
75
+ abortControllerRef: AbortController | null,
76
+ generateId: IdGenerator,
77
+ streamMode?: 'stream-data' | 'text',
78
+ onFinish?: (message: Message) => void,
79
+ onResponse?: (response: Response) => void | Promise<void>,
80
+ sendExtraMessageFields?: boolean,
81
+ ) => {
82
+ // Do an optimistic update to the chat state to show the updated messages
83
+ // immediately.
84
+ mutate(chatRequest.messages);
85
+
86
+ const constructedMessagesPayload = sendExtraMessageFields
87
+ ? chatRequest.messages
88
+ : chatRequest.messages.map(
89
+ ({
90
+ role,
91
+ content,
92
+ name,
93
+ data,
94
+ annotations,
95
+ function_call,
96
+ tool_calls,
97
+ tool_call_id,
98
+ }) => ({
99
+ role,
100
+ content,
101
+ ...(name !== undefined && { name }),
102
+ ...(data !== undefined && { data }),
103
+ ...(annotations !== undefined && { annotations }),
104
+ // outdated function/tool call handling (TODO deprecate):
105
+ tool_call_id,
106
+ ...(function_call !== undefined && { function_call }),
107
+ ...(tool_calls !== undefined && { tool_calls }),
108
+ }),
109
+ );
110
+
111
+ return await callChatApi({
112
+ api,
113
+ messages: constructedMessagesPayload,
114
+ body: {
115
+ ...extraMetadata.body,
116
+ ...chatRequest.options?.body,
117
+ ...(chatRequest.functions !== undefined && {
118
+ functions: chatRequest.functions,
119
+ }),
120
+ ...(chatRequest.function_call !== undefined && {
121
+ function_call: chatRequest.function_call,
122
+ }),
123
+ ...(chatRequest.tools !== undefined && {
124
+ tools: chatRequest.tools,
125
+ }),
126
+ ...(chatRequest.tool_choice !== undefined && {
127
+ tool_choice: chatRequest.tool_choice,
128
+ }),
129
+ },
130
+ streamMode,
131
+ credentials: extraMetadata.credentials,
132
+ headers: {
133
+ ...extraMetadata.headers,
134
+ ...chatRequest.options?.headers,
135
+ },
136
+ abortController: () => abortControllerRef,
137
+ restoreMessagesOnFailure() {
138
+ mutate(previousMessages);
139
+ },
140
+ onResponse,
141
+ onUpdate(merged, data) {
142
+ mutate([...chatRequest.messages, ...merged]);
143
+ mutateStreamData([...(existingData || []), ...(data || [])]);
144
+ },
145
+ onFinish,
146
+ generateId,
147
+ });
148
+ };
149
+
150
+ let uniqueId = 0;
151
+
152
+ const store: Record<string, Message[] | undefined> = {};
153
+
154
+ export function useChat({
155
+ api = '/api/chat',
156
+ id,
157
+ initialMessages = [],
158
+ initialInput = '',
159
+ sendExtraMessageFields,
160
+ experimental_onFunctionCall,
161
+ experimental_onToolCall,
162
+ streamMode,
163
+ onResponse,
164
+ onFinish,
165
+ onError,
166
+ credentials,
167
+ headers,
168
+ body,
169
+ generateId = generateIdFunc,
170
+ }: UseChatOptions = {}): UseChatHelpers {
171
+ // Generate a unique id for the chat if not provided.
172
+ const chatId = id || `chat-${uniqueId++}`;
173
+
174
+ const key = `${api}|${chatId}`;
175
+ const {
176
+ data,
177
+ mutate: originalMutate,
178
+ isLoading: isSWRLoading,
179
+ } = useSWR<Message[]>(key, {
180
+ fetcher: () => store[key] || initialMessages,
181
+ fallbackData: initialMessages,
182
+ });
183
+
184
+ const streamData = writable<JSONValue[] | undefined>(undefined);
185
+
186
+ const loading = writable<boolean>(false);
187
+
188
+ // Force the `data` to be `initialMessages` if it's `undefined`.
189
+ data.set(initialMessages);
190
+
191
+ const mutate = (data: Message[]) => {
192
+ store[key] = data;
193
+ return originalMutate(data);
194
+ };
195
+
196
+ // Because of the `fallbackData` option, the `data` will never be `undefined`.
197
+ const messages = data as Writable<Message[]>;
198
+
199
+ // Abort controller to cancel the current API call.
200
+ let abortController: AbortController | null = null;
201
+
202
+ const extraMetadata = {
203
+ credentials,
204
+ headers,
205
+ body,
206
+ };
207
+
208
+ const error = writable<undefined | Error>(undefined);
209
+
210
+ // Actual mutation hook to send messages to the API endpoint and update the
211
+ // chat state.
212
+ async function triggerRequest(chatRequest: ChatRequest) {
213
+ try {
214
+ error.set(undefined);
215
+ loading.set(true);
216
+ abortController = new AbortController();
217
+
218
+ await processChatStream({
219
+ getStreamedResponse: () =>
220
+ getStreamedResponse(
221
+ api,
222
+ chatRequest,
223
+ mutate,
224
+ data => {
225
+ streamData.set(data);
226
+ },
227
+ get(streamData),
228
+ extraMetadata,
229
+ get(messages),
230
+ abortController,
231
+ generateId,
232
+ streamMode,
233
+ onFinish,
234
+ onResponse,
235
+ sendExtraMessageFields,
236
+ ),
237
+ experimental_onFunctionCall,
238
+ experimental_onToolCall,
239
+ updateChatRequest: chatRequestParam => {
240
+ chatRequest = chatRequestParam;
241
+ },
242
+ getCurrentMessages: () => get(messages),
243
+ });
244
+
245
+ abortController = null;
246
+
247
+ return null;
248
+ } catch (err) {
249
+ // Ignore abort errors as they are expected.
250
+ if ((err as any).name === 'AbortError') {
251
+ abortController = null;
252
+ return null;
253
+ }
254
+
255
+ if (onError && err instanceof Error) {
256
+ onError(err);
257
+ }
258
+
259
+ error.set(err as Error);
260
+ } finally {
261
+ loading.set(false);
262
+ }
263
+ }
264
+
265
+ const append: UseChatHelpers['append'] = async (
266
+ message: Message | CreateMessage,
267
+ {
268
+ options,
269
+ functions,
270
+ function_call,
271
+ tools,
272
+ tool_choice,
273
+ data,
274
+ }: ChatRequestOptions = {},
275
+ ) => {
276
+ if (!message.id) {
277
+ message.id = generateId();
278
+ }
279
+
280
+ const chatRequest: ChatRequest = {
281
+ messages: get(messages).concat(message as Message),
282
+ options,
283
+ data,
284
+ ...(functions !== undefined && { functions }),
285
+ ...(function_call !== undefined && { function_call }),
286
+ ...(tools !== undefined && { tools }),
287
+ ...(tool_choice !== undefined && { tool_choice }),
288
+ };
289
+ return triggerRequest(chatRequest);
290
+ };
291
+
292
+ const reload: UseChatHelpers['reload'] = async ({
293
+ options,
294
+ functions,
295
+ function_call,
296
+ tools,
297
+ tool_choice,
298
+ }: ChatRequestOptions = {}) => {
299
+ const messagesSnapshot = get(messages);
300
+ if (messagesSnapshot.length === 0) return null;
301
+
302
+ // Remove last assistant message and retry last user message.
303
+ const lastMessage = messagesSnapshot.at(-1);
304
+ if (lastMessage?.role === 'assistant') {
305
+ const chatRequest: ChatRequest = {
306
+ messages: messagesSnapshot.slice(0, -1),
307
+ options,
308
+ ...(functions !== undefined && { functions }),
309
+ ...(function_call !== undefined && { function_call }),
310
+ ...(tools !== undefined && { tools }),
311
+ ...(tool_choice !== undefined && { tool_choice }),
312
+ };
313
+
314
+ return triggerRequest(chatRequest);
315
+ }
316
+ const chatRequest: ChatRequest = {
317
+ messages: messagesSnapshot,
318
+ options,
319
+ ...(functions !== undefined && { functions }),
320
+ ...(function_call !== undefined && { function_call }),
321
+ ...(tools !== undefined && { tools }),
322
+ ...(tool_choice !== undefined && { tool_choice }),
323
+ };
324
+
325
+ return triggerRequest(chatRequest);
326
+ };
327
+
328
+ const stop = () => {
329
+ if (abortController) {
330
+ abortController.abort();
331
+ abortController = null;
332
+ }
333
+ };
334
+
335
+ const setMessages = (messages: Message[]) => {
336
+ mutate(messages);
337
+ };
338
+
339
+ const input = writable(initialInput);
340
+
341
+ const handleSubmit = (e: any, options: ChatRequestOptions = {}) => {
342
+ e.preventDefault();
343
+ const inputValue = get(input);
344
+ if (!inputValue) return;
345
+
346
+ append(
347
+ {
348
+ content: inputValue,
349
+ role: 'user',
350
+ createdAt: new Date(),
351
+ },
352
+ options,
353
+ );
354
+ input.set('');
355
+ };
356
+
357
+ const isLoading = derived(
358
+ [isSWRLoading, loading],
359
+ ([$isSWRLoading, $loading]) => {
360
+ return $isSWRLoading || $loading;
361
+ },
362
+ );
363
+
364
+ return {
365
+ messages,
366
+ error,
367
+ append,
368
+ reload,
369
+ stop,
370
+ setMessages,
371
+ input,
372
+ handleSubmit,
373
+ isLoading,
374
+ data: streamData,
375
+ };
376
+ }