Andrew commited on
Commit
684165a
·
1 Parent(s): 8293c27

feat(api): update conversation endpoints to handle branching parameters

Browse files
src/routes/conversation/+server.ts CHANGED
@@ -53,7 +53,7 @@ export const POST: RequestHandler = async ({ locals, request }) => {
53
  },
54
  ];
55
 
56
- let rootMessageId: Message["id"] = messages[0].id;
57
 
58
  if (values.fromShare) {
59
  const conversation = await collections.sharedConversations.findOne({
 
53
  },
54
  ];
55
 
56
+ let rootMessageId: Message["id"] | undefined = undefined;
57
 
58
  if (values.fromShare) {
59
  const conversation = await collections.sharedConversations.findOne({
src/routes/conversation/[id]/+server.ts CHANGED
@@ -156,6 +156,8 @@ export async function POST({ request, locals, params, getClientAddress }) {
156
  id: messageId,
157
  is_retry: isRetry,
158
  is_continue: isContinue,
 
 
159
  } = z
160
  .object({
161
  id: z.string().uuid().refine(isMessageId).optional(), // parent message id to append to for a normal message, or the message id for a retry/continue
@@ -168,6 +170,12 @@ export async function POST({ request, locals, params, getClientAddress }) {
168
  is_retry: z.optional(z.boolean()),
169
  is_continue: z.optional(z.boolean()),
170
  persona_id: z.optional(z.string()), // Optional: specific persona to regenerate
 
 
 
 
 
 
171
  files: z.optional(
172
  z.array(
173
  z.object({
@@ -237,15 +245,9 @@ export async function POST({ request, locals, params, getClientAddress }) {
237
  messageToWriteToId = messageId;
238
  messagesForPrompt = buildSubtree(conv, messageId);
239
  } else if (isRetry && messageId) {
240
- // REGENERATION DISABLED - commenting out retry logic
241
- // Returning error when retry is attempted
242
- error(400, "Regeneration is currently disabled");
243
-
244
- /*
245
- // two cases, if we're retrying a user message with a newPrompt set,
246
- // it means we're editing a user message
247
- // if we're retrying on an assistant message, newPrompt cannot be set
248
- // it means we're retrying the last assistant message for a new answer
249
 
250
  const messageToRetry = conv.messages.find((message) => message.id === messageId);
251
 
@@ -253,9 +255,11 @@ export async function POST({ request, locals, params, getClientAddress }) {
253
  error(404, "Message not found");
254
  }
255
 
 
 
 
256
  if (messageToRetry.from === "user" && newPrompt) {
257
- // add a sibling to this message from the user, with the alternative prompt
258
- // add a children to that sibling, where we can write to
259
  const newUserMessageId = addSibling(
260
  conv,
261
  {
@@ -264,9 +268,17 @@ export async function POST({ request, locals, params, getClientAddress }) {
264
  files: uploadedFiles,
265
  createdAt: new Date(),
266
  updatedAt: new Date(),
 
 
 
 
267
  },
268
  messageId
269
  );
 
 
 
 
270
  messageToWriteToId = addChildren(
271
  conv,
272
  {
@@ -274,22 +286,35 @@ export async function POST({ request, locals, params, getClientAddress }) {
274
  content: "",
275
  createdAt: new Date(),
276
  updatedAt: new Date(),
 
 
 
 
277
  },
278
  newUserMessageId
279
  );
280
  messagesForPrompt = buildSubtree(conv, newUserMessageId);
281
  } else if (messageToRetry.from === "assistant") {
282
- // we're retrying an assistant message, to generate a new answer
283
- // just add a sibling to the assistant answer where we can write to
284
  messageToWriteToId = addSibling(
285
  conv,
286
- { from: "assistant", content: "", createdAt: new Date(), updatedAt: new Date() },
 
 
 
 
 
 
 
 
 
287
  messageId
288
  );
289
  messagesForPrompt = buildSubtree(conv, messageId);
290
- messagesForPrompt.pop(); // don't need the latest assistant message in the prompt since we're retrying it
 
 
291
  }
292
- */
293
  } else {
294
  // just a normal linear conversation, so we add the user message
295
  // and the blank assistant message back to back
@@ -301,6 +326,10 @@ export async function POST({ request, locals, params, getClientAddress }) {
301
  files: uploadedFiles,
302
  createdAt: new Date(),
303
  updatedAt: new Date(),
 
 
 
 
304
  },
305
  messageId
306
  );
@@ -312,6 +341,10 @@ export async function POST({ request, locals, params, getClientAddress }) {
312
  content: "",
313
  createdAt: new Date(),
314
  updatedAt: new Date(),
 
 
 
 
315
  },
316
  newUserMessageId
317
  );
@@ -535,53 +568,44 @@ export async function POST({ request, locals, params, getClientAddress }) {
535
  );
536
  const { generateTitleForConversation } = await import("$lib/server/textGeneration/title");
537
 
538
- // REGENERATION DISABLED - commenting out persona retry logic
539
- /*
540
- // Check if this is a retry for a specific persona
541
- const isPersonaRetry = isRetry && personaId;
542
-
543
- // If retrying a specific persona, get the previous message's persona responses
544
- const previousMessage = isPersonaRetry
545
- ? conv.messages.find((msg) => msg.id === messageId)
546
- : null;
547
-
548
- // Initialize persona responses structure
549
- if (isPersonaRetry && previousMessage?.personaResponses) {
550
- // Copy all previous persona responses
551
- messageToWriteTo.personaResponses = previousMessage.personaResponses.map((pr) => {
552
- if (pr.personaId === personaId) {
553
- // For the persona being regenerated, store the old response as a child
554
- const oldResponse = { ...pr };
555
- delete oldResponse.children; // Don't nest children recursively
556
- delete oldResponse.currentChildIndex;
557
-
558
- return {
559
- personaId: pr.personaId,
560
- personaName: pr.personaName,
561
- personaOccupation: pr.personaOccupation,
562
- personaStance: pr.personaStance,
563
- content: "",
564
- updates: [],
565
- children: [oldResponse], // Store the old response
566
- currentChildIndex: 0, // New response will be at index 0
567
- };
568
- } else {
569
- // Keep other personas' responses as-is
570
- return { ...pr };
571
- }
572
- });
573
- } else {
574
- */
575
- // Normal generation: initialize empty responses for all personas
576
- messageToWriteTo.personaResponses = activePersonas.map((p) => ({
577
- personaId: p.id,
578
- personaName: p.name,
579
- personaOccupation: p.jobSector,
580
- personaStance: p.stance,
581
- content: "",
582
- updates: [],
583
- }));
584
- // }
585
 
586
  try {
587
  // Generate title if needed (do this once, not per-persona)
@@ -611,18 +635,16 @@ export async function POST({ request, locals, params, getClientAddress }) {
611
  forceMultimodal: Boolean(userSettings?.multimodalOverrides?.[model.id]),
612
  };
613
 
614
- // REGENERATION DISABLED - always generate for all active personas
615
- /*
616
- // Determine which personas to generate for
617
- const personasToGenerate = isPersonaRetry && personaId
618
- ? (() => {
619
- // Find the specific persona from all user personas, not just active ones
620
  const persona = userSettings?.personas?.find((p) => p.id === personaId);
621
- return persona ? [persona] : [];
622
- })()
623
- : activePersonas;
624
- */
625
- const personasToGenerate = activePersonas;
626
 
627
  // Run multi-persona text generation (preprocessing happens once inside)
628
  for await (const event of multiPersonaTextGeneration(ctx, personasToGenerate)) {
 
156
  id: messageId,
157
  is_retry: isRetry,
158
  is_continue: isContinue,
159
+ persona_id: personaId,
160
+ branched_from: branchedFrom,
161
  } = z
162
  .object({
163
  id: z.string().uuid().refine(isMessageId).optional(), // parent message id to append to for a normal message, or the message id for a retry/continue
 
170
  is_retry: z.optional(z.boolean()),
171
  is_continue: z.optional(z.boolean()),
172
  persona_id: z.optional(z.string()), // Optional: specific persona to regenerate
173
+ branched_from: z.optional(
174
+ z.object({
175
+ messageId: z.string(),
176
+ personaId: z.string(),
177
+ })
178
+ ), // Optional: branch metadata
179
  files: z.optional(
180
  z.array(
181
  z.object({
 
245
  messageToWriteToId = messageId;
246
  messagesForPrompt = buildSubtree(conv, messageId);
247
  } else if (isRetry && messageId) {
248
+ // Two cases:
249
+ // 1. Retrying a user message with newPrompt = editing the user's input
250
+ // 2. Retrying an assistant message = regenerating assistant response(s)
 
 
 
 
 
 
251
 
252
  const messageToRetry = conv.messages.find((message) => message.id === messageId);
253
 
 
255
  error(404, "Message not found");
256
  }
257
 
258
+ // Import addSibling for retry logic
259
+ const { addSibling } = await import("$lib/utils/tree/addSibling.js");
260
+
261
  if (messageToRetry.from === "user" && newPrompt) {
262
+ // Editing user message: create sibling with new content, then new assistant response
 
263
  const newUserMessageId = addSibling(
264
  conv,
265
  {
 
268
  files: uploadedFiles,
269
  createdAt: new Date(),
270
  updatedAt: new Date(),
271
+ // Copy branchedFrom if it exists
272
+ ...(messageToRetry.branchedFrom && {
273
+ branchedFrom: messageToRetry.branchedFrom,
274
+ }),
275
  },
276
  messageId
277
  );
278
+
279
+ // Get the newly created user message to check for branchedFrom
280
+ const newUserMessage = conv.messages.find((m) => m.id === newUserMessageId);
281
+
282
  messageToWriteToId = addChildren(
283
  conv,
284
  {
 
286
  content: "",
287
  createdAt: new Date(),
288
  updatedAt: new Date(),
289
+ // Copy branchedFrom from user message if it exists
290
+ ...(newUserMessage?.branchedFrom && {
291
+ branchedFrom: newUserMessage.branchedFrom,
292
+ }),
293
  },
294
  newUserMessageId
295
  );
296
  messagesForPrompt = buildSubtree(conv, newUserMessageId);
297
  } else if (messageToRetry.from === "assistant") {
298
+ // Regenerating assistant response: create new sibling response
 
299
  messageToWriteToId = addSibling(
300
  conv,
301
+ {
302
+ from: "assistant",
303
+ content: "",
304
+ createdAt: new Date(),
305
+ updatedAt: new Date(),
306
+ // Copy branchedFrom if it exists
307
+ ...(messageToRetry.branchedFrom && {
308
+ branchedFrom: messageToRetry.branchedFrom,
309
+ }),
310
+ },
311
  messageId
312
  );
313
  messagesForPrompt = buildSubtree(conv, messageId);
314
+ messagesForPrompt.pop(); // don't need the old assistant message in the prompt
315
+ } else {
316
+ error(400, "Invalid retry request");
317
  }
 
318
  } else {
319
  // just a normal linear conversation, so we add the user message
320
  // and the blank assistant message back to back
 
326
  files: uploadedFiles,
327
  createdAt: new Date(),
328
  updatedAt: new Date(),
329
+ // Add branchedFrom from request if it exists
330
+ ...(branchedFrom && {
331
+ branchedFrom,
332
+ }),
333
  },
334
  messageId
335
  );
 
341
  content: "",
342
  createdAt: new Date(),
343
  updatedAt: new Date(),
344
+ // Copy branchedFrom from request if it exists
345
+ ...(branchedFrom && {
346
+ branchedFrom,
347
+ }),
348
  },
349
  newUserMessageId
350
  );
 
568
  );
569
  const { generateTitleForConversation } = await import("$lib/server/textGeneration/title");
570
 
571
+ // Check if this is a retry for a specific persona
572
+ const isPersonaRetry = isRetry && personaId;
573
+
574
+ // If retrying a specific persona, get the previous message's persona responses
575
+ const previousMessage = isPersonaRetry
576
+ ? conv.messages.find((msg) => msg.id === messageId)
577
+ : null;
578
+
579
+ // Initialize persona responses structure
580
+ if (isPersonaRetry && previousMessage?.personaResponses) {
581
+ // Copy all previous persona responses
582
+ messageToWriteTo.personaResponses = previousMessage.personaResponses.map((pr) => {
583
+ if (pr.personaId === personaId) {
584
+ // For the persona being regenerated, reset content
585
+ return {
586
+ personaId: pr.personaId,
587
+ personaName: pr.personaName,
588
+ personaOccupation: pr.personaOccupation,
589
+ personaStance: pr.personaStance,
590
+ content: "",
591
+ updates: [],
592
+ };
593
+ } else {
594
+ // Keep other personas' responses as-is
595
+ return { ...pr };
596
+ }
597
+ });
598
+ } else {
599
+ // Normal generation: initialize empty responses for all personas
600
+ messageToWriteTo.personaResponses = activePersonas.map((p) => ({
601
+ personaId: p.id,
602
+ personaName: p.name,
603
+ personaOccupation: p.jobSector,
604
+ personaStance: p.stance,
605
+ content: "",
606
+ updates: [],
607
+ }));
608
+ }
 
 
 
 
 
 
 
 
 
609
 
610
  try {
611
  // Generate title if needed (do this once, not per-persona)
 
635
  forceMultimodal: Boolean(userSettings?.multimodalOverrides?.[model.id]),
636
  };
637
 
638
+ // Determine which personas to generate for
639
+ let personasToGenerate = activePersonas;
640
+
641
+ // Check if this is a retry for a specific persona
642
+ if (isPersonaRetry && personaId) {
 
643
  const persona = userSettings?.personas?.find((p) => p.id === personaId);
644
+ personasToGenerate = persona ? [persona] : [];
645
+ }
646
+ // Note: branchedFrom is used for message filtering/display, not persona restriction
647
+ // Users can switch personas within a branch by using the persona selector
 
648
 
649
  // Run multi-persona text generation (preprocessing happens once inside)
650
  for await (const event of multiPersonaTextGeneration(ctx, personasToGenerate)) {