Andrew commited on
Commit
346fcd8
·
1 Parent(s): 8bba2e0

feat(utils): add message sender utility for handling chat requests

Browse files
Files changed (1) hide show
  1. src/lib/utils/messageSender.ts +420 -0
src/lib/utils/messageSender.ts ADDED
@@ -0,0 +1,420 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { goto, invalidate } from "$app/navigation";
2
+ import { base } from "$app/paths";
3
+ import { tick } from "svelte";
4
+ import { type Message, MessageRole } from "$lib/types/Message";
5
+ import {
6
+ MessageReasoningUpdateType,
7
+ MessageUpdateStatus,
8
+ MessageUpdateType,
9
+ } from "$lib/types/MessageUpdate";
10
+ import { UrlDependency } from "$lib/types/UrlDependency";
11
+ import file2base64 from "$lib/utils/file2base64";
12
+ import { fetchMessageUpdates } from "$lib/utils/messageUpdates";
13
+ import { addChildren } from "$lib/utils/tree/addChildren";
14
+ import { addSibling } from "$lib/utils/tree/addSibling";
15
+ import { updateDebouncer } from "$lib/utils/updates.js";
16
+ import type { v4 } from "uuid";
17
+ import { ERROR_MESSAGES } from "$lib/stores/errors";
18
+
19
+ export interface WriteMessageContext {
20
+ page: { params: { id: string } };
21
+ messages: Message[];
22
+ messagesPath: Message[];
23
+ data: { rootMessageId: string };
24
+ files: File[];
25
+ settings: {
26
+ disableStream: boolean;
27
+ personas?: Array<{ id: string; name: string }>;
28
+ };
29
+ isAborted: () => boolean;
30
+ branchState: {
31
+ messageId: string;
32
+ personaId: string;
33
+ personaName: string;
34
+ } | null;
35
+
36
+ setLoading: (val: boolean) => void;
37
+ setPending: (val: boolean) => void;
38
+ setFiles: (val: File[]) => void;
39
+ setError: (val: string) => void;
40
+ setIsAborted: (val: boolean) => void;
41
+ setTitleUpdate: (val: { title: string; convId: string }) => void;
42
+ onTitleUpdate?: (title: string) => void;
43
+ onMessageCreated?: (id: string) => void;
44
+ updateBranchState: (val: unknown) => void;
45
+ invalidate: typeof invalidate;
46
+ goto: typeof goto;
47
+ }
48
+
49
+ export interface WriteMessageParams {
50
+ prompt?: string;
51
+ messageId?: ReturnType<typeof v4>;
52
+ isRetry?: boolean;
53
+ isContinue?: boolean;
54
+ personaId?: string;
55
+ }
56
+
57
+ export async function writeMessage(
58
+ ctx: WriteMessageContext,
59
+ params: WriteMessageParams
60
+ ): Promise<void> {
61
+ const {
62
+ prompt,
63
+ messageId = ctx.messagesPath.at(-1)?.id ?? undefined,
64
+ isRetry = false,
65
+ isContinue = false,
66
+ personaId,
67
+ } = params;
68
+
69
+ const conversationId = ctx.page.params.id;
70
+ if (!conversationId) {
71
+ console.error("No conversation ID available");
72
+ return;
73
+ }
74
+
75
+ let navigateToMessageId: string | null = null;
76
+
77
+ try {
78
+ ctx.setIsAborted(false);
79
+ ctx.setLoading(true);
80
+ ctx.setPending(true);
81
+
82
+ const base64Files = await Promise.all(
83
+ (ctx.files ?? []).map((file) =>
84
+ file2base64(file).then((value) => ({
85
+ type: "base64" as const,
86
+ value,
87
+ mime: file.type,
88
+ name: file.name,
89
+ }))
90
+ )
91
+ );
92
+
93
+ let messageToWriteToId: Message["id"] | undefined = undefined;
94
+
95
+ if (isContinue && messageId) {
96
+ if ((ctx.messages.find((msg) => msg.id === messageId)?.children?.length ?? 0) > 0) {
97
+ ctx.setError("Can only continue the last message");
98
+ } else {
99
+ messageToWriteToId = messageId;
100
+ }
101
+ } else if (isRetry && messageId) {
102
+ const messageToRetry = ctx.messages.find((message) => message.id === messageId);
103
+
104
+ if (!messageToRetry) {
105
+ ctx.setError("Message not found");
106
+ }
107
+
108
+ if (messageToRetry?.from === MessageRole.User && prompt) {
109
+ const newUserMessageId = addSibling(
110
+ {
111
+ messages: ctx.messages,
112
+ rootMessageId: ctx.data.rootMessageId,
113
+ },
114
+ {
115
+ from: MessageRole.User,
116
+ content: prompt,
117
+ files: messageToRetry.files,
118
+ ...(messageToRetry.branchedFrom && {
119
+ branchedFrom: messageToRetry.branchedFrom,
120
+ }),
121
+ },
122
+ messageId
123
+ );
124
+ messageToWriteToId = addChildren(
125
+ {
126
+ messages: ctx.messages,
127
+ rootMessageId: ctx.data.rootMessageId,
128
+ },
129
+ {
130
+ from: MessageRole.Assistant,
131
+ content: "",
132
+ personaResponses: [],
133
+ ...(messageToRetry.branchedFrom && {
134
+ branchedFrom: messageToRetry.branchedFrom,
135
+ }),
136
+ },
137
+ newUserMessageId
138
+ );
139
+
140
+ if (messageToRetry.branchedFrom) {
141
+ const persona = ctx.settings.personas?.find(
142
+ (p) => p.id === messageToRetry.branchedFrom?.personaId
143
+ );
144
+ ctx.updateBranchState({
145
+ messageId: messageToRetry.branchedFrom.messageId,
146
+ personaId: messageToRetry.branchedFrom.personaId,
147
+ personaName: persona?.name || messageToRetry.branchedFrom.personaId,
148
+ });
149
+ navigateToMessageId = newUserMessageId;
150
+ }
151
+
152
+ ctx.onMessageCreated?.(messageToWriteToId);
153
+ } else if (messageToRetry?.from === MessageRole.Assistant) {
154
+ messageToWriteToId = addSibling(
155
+ {
156
+ messages: ctx.messages,
157
+ rootMessageId: ctx.data.rootMessageId,
158
+ },
159
+ {
160
+ from: MessageRole.Assistant,
161
+ content: "",
162
+ personaResponses: [],
163
+ ...(messageToRetry.branchedFrom && {
164
+ branchedFrom: messageToRetry.branchedFrom,
165
+ }),
166
+ },
167
+ messageId
168
+ );
169
+
170
+ if (messageToRetry.branchedFrom) {
171
+ const persona = ctx.settings.personas?.find(
172
+ (p) => p.id === messageToRetry.branchedFrom?.personaId
173
+ );
174
+ ctx.updateBranchState({
175
+ messageId: messageToRetry.branchedFrom.messageId,
176
+ personaId: messageToRetry.branchedFrom.personaId,
177
+ personaName: persona?.name || messageToRetry.branchedFrom.personaId,
178
+ });
179
+ navigateToMessageId = messageToWriteToId;
180
+ }
181
+ ctx.onMessageCreated?.(messageToWriteToId);
182
+ }
183
+ } else {
184
+ const newUserMessageId = addChildren(
185
+ {
186
+ messages: ctx.messages,
187
+ rootMessageId: ctx.data.rootMessageId,
188
+ },
189
+ {
190
+ from: MessageRole.User,
191
+ content: prompt ?? "",
192
+ files: base64Files,
193
+ ...(ctx.branchState && {
194
+ branchedFrom: {
195
+ messageId: ctx.branchState.messageId,
196
+ personaId: ctx.branchState.personaId,
197
+ },
198
+ }),
199
+ },
200
+ messageId
201
+ );
202
+
203
+ if (!ctx.data.rootMessageId) {
204
+ ctx.data.rootMessageId = newUserMessageId;
205
+ }
206
+
207
+ messageToWriteToId = addChildren(
208
+ {
209
+ messages: ctx.messages,
210
+ rootMessageId: ctx.data.rootMessageId,
211
+ },
212
+ {
213
+ from: MessageRole.Assistant,
214
+ content: "",
215
+ personaResponses: [],
216
+ ...(ctx.branchState && {
217
+ branchedFrom: {
218
+ messageId: ctx.branchState.messageId,
219
+ personaId: ctx.branchState.personaId,
220
+ },
221
+ }),
222
+ },
223
+ newUserMessageId
224
+ );
225
+
226
+ ctx.onMessageCreated?.(messageToWriteToId);
227
+ }
228
+
229
+ const userMessage = ctx.messages.find((message) => message.id === messageId);
230
+ const messageToWriteTo = ctx.messages.find((message) => message.id === messageToWriteToId);
231
+ if (!messageToWriteTo) {
232
+ throw new Error("Message to write to not found");
233
+ }
234
+
235
+ const messageUpdatesAbortController = new AbortController();
236
+
237
+ const messageUpdatesIterator = await fetchMessageUpdates(
238
+ conversationId,
239
+ {
240
+ base,
241
+ inputs: prompt,
242
+ messageId,
243
+ isRetry,
244
+ isContinue,
245
+ files: isRetry ? userMessage?.files : base64Files,
246
+ personaId,
247
+ branchedFrom: ctx.branchState
248
+ ? {
249
+ messageId: ctx.branchState.messageId,
250
+ personaId: ctx.branchState.personaId,
251
+ }
252
+ : undefined,
253
+ },
254
+ messageUpdatesAbortController.signal
255
+ ).catch((err) => {
256
+ ctx.setError(err.message);
257
+ });
258
+ if (messageUpdatesIterator === undefined) return;
259
+
260
+ ctx.setFiles([]);
261
+ let buffer = "";
262
+ let lastUpdateTime = new Date();
263
+
264
+ let reasoningBuffer = "";
265
+ let reasoningLastUpdate = new Date();
266
+
267
+ const personaBuffers = new Map<string, string>();
268
+ const personaLastUpdateTimes = new Map<string, Date>();
269
+
270
+ for await (const update of messageUpdatesIterator) {
271
+ if (ctx.isAborted()) {
272
+ messageUpdatesAbortController.abort();
273
+ return;
274
+ }
275
+
276
+ if (update.type === MessageUpdateType.Stream) {
277
+ update.token = update.token.replaceAll("\0", "");
278
+ }
279
+
280
+ const isHighFrequencyUpdate =
281
+ (update.type === MessageUpdateType.Reasoning &&
282
+ update.subtype === MessageReasoningUpdateType.Stream) ||
283
+ update.type === MessageUpdateType.Stream ||
284
+ update.type === MessageUpdateType.Persona ||
285
+ (update.type === MessageUpdateType.Status &&
286
+ update.status === MessageUpdateStatus.KeepAlive);
287
+
288
+ if (!isHighFrequencyUpdate) {
289
+ messageToWriteTo.updates = [...(messageToWriteTo.updates ?? []), update];
290
+ }
291
+ const currentTime = new Date();
292
+
293
+ if (update.type === MessageUpdateType.PersonaInit) {
294
+ messageToWriteTo.personaResponses = update.personas.map((p) => ({
295
+ personaId: p.personaId,
296
+ personaName: p.personaName,
297
+ personaOccupation: p.personaOccupation,
298
+ personaStance: p.personaStance,
299
+ content: "",
300
+ }));
301
+ } else if (update.type === MessageUpdateType.Persona) {
302
+ if (!messageToWriteTo.personaResponses) {
303
+ messageToWriteTo.personaResponses = [];
304
+ }
305
+
306
+ let personaResponse = messageToWriteTo.personaResponses.find(
307
+ (pr) => pr.personaId === update.personaId
308
+ );
309
+ if (!personaResponse) {
310
+ personaResponse = {
311
+ personaId: update.personaId,
312
+ personaName: update.personaName,
313
+ personaOccupation: update.personaOccupation,
314
+ personaStance: update.personaStance,
315
+ content: "",
316
+ };
317
+ messageToWriteTo.personaResponses.push(personaResponse);
318
+ }
319
+
320
+ if (update.updateType === "stream" && update.token && !ctx.settings.disableStream) {
321
+ const personaBuffer = personaBuffers.get(update.personaId) || "";
322
+ const newBuffer = personaBuffer + update.token;
323
+ personaBuffers.set(update.personaId, newBuffer);
324
+
325
+ const lastUpdate = personaLastUpdateTimes.get(update.personaId) || new Date(0);
326
+ if (currentTime.getTime() - lastUpdate.getTime() > updateDebouncer.maxUpdateTime) {
327
+ personaResponse.content += newBuffer;
328
+ personaBuffers.set(update.personaId, "");
329
+ personaLastUpdateTimes.set(update.personaId, currentTime);
330
+ }
331
+ ctx.setPending(false);
332
+ } else if (update.updateType === "finalAnswer" && update.text) {
333
+ personaResponse.content = update.text;
334
+ personaResponse.interrupted = update.interrupted;
335
+ } else if (update.updateType === "routerMetadata" && update.route && update.model) {
336
+ personaResponse.routerMetadata = {
337
+ route: update.route,
338
+ model: update.model,
339
+ };
340
+ } else if (update.updateType === "status" && update.error) {
341
+ personaResponse.interrupted = true;
342
+ personaResponse.content = personaResponse.content || `Error: ${update.error}`;
343
+ }
344
+ } else if (update.type === MessageUpdateType.Stream && !ctx.settings.disableStream) {
345
+ buffer += update.token;
346
+ if (currentTime.getTime() - lastUpdateTime.getTime() > updateDebouncer.maxUpdateTime) {
347
+ messageToWriteTo.content += buffer;
348
+ buffer = "";
349
+ lastUpdateTime = currentTime;
350
+ }
351
+ ctx.setPending(false);
352
+ } else if (
353
+ update.type === MessageUpdateType.Status &&
354
+ update.status === MessageUpdateStatus.Error
355
+ ) {
356
+ ctx.setError(update.message ?? "An error has occurred");
357
+ } else if (update.type === MessageUpdateType.Title) {
358
+ ctx.setTitleUpdate({
359
+ title: update.title,
360
+ convId: conversationId,
361
+ });
362
+ ctx.onTitleUpdate?.(update.title);
363
+ } else if (update.type === MessageUpdateType.File) {
364
+ messageToWriteTo.files = [
365
+ ...(messageToWriteTo.files ?? []),
366
+ { type: "hash", value: update.sha, mime: update.mime, name: update.name },
367
+ ];
368
+ } else if (update.type === MessageUpdateType.Reasoning) {
369
+ if (!messageToWriteTo.reasoning) {
370
+ messageToWriteTo.reasoning = "";
371
+ }
372
+ if (update.subtype === MessageReasoningUpdateType.Stream) {
373
+ reasoningBuffer += update.token;
374
+ if (
375
+ currentTime.getTime() - reasoningLastUpdate.getTime() >
376
+ updateDebouncer.maxUpdateTime
377
+ ) {
378
+ messageToWriteTo.reasoning += reasoningBuffer;
379
+ reasoningBuffer = "";
380
+ reasoningLastUpdate = currentTime;
381
+ }
382
+ }
383
+ } else if (update.type === MessageUpdateType.RouterMetadata) {
384
+ messageToWriteTo.routerMetadata = {
385
+ route: update.route,
386
+ model: update.model,
387
+ };
388
+ } else if (update.type === MessageUpdateType.FinalAnswer) {
389
+ messageToWriteTo.content = update.text;
390
+ messageToWriteTo.interrupted = update.interrupted;
391
+ ctx.setPending(false);
392
+ }
393
+ }
394
+ } catch (err) {
395
+ if (err instanceof Error && err.message.includes("overloaded")) {
396
+ ctx.setError("Too much traffic, please try again.");
397
+ } else if (err instanceof Error && err.message.includes("429")) {
398
+ ctx.setError(ERROR_MESSAGES.rateLimited);
399
+ } else if (err instanceof Error) {
400
+ ctx.setError(err.message);
401
+ } else {
402
+ ctx.setError(ERROR_MESSAGES.default);
403
+ }
404
+ console.error(err);
405
+ } finally {
406
+ await ctx.invalidate(UrlDependency.Conversation);
407
+ await ctx.invalidate(UrlDependency.ConversationList);
408
+
409
+ ctx.setLoading(false);
410
+ ctx.setPending(false);
411
+
412
+ if (navigateToMessageId) {
413
+ await tick();
414
+ const url = new URL(window.location.href);
415
+ url.searchParams.set("msgId", navigateToMessageId);
416
+ url.searchParams.set("scrollTo", "true");
417
+ await ctx.goto(url.toString(), { replaceState: false, noScroll: true });
418
+ }
419
+ }
420
+ }