Conversation trees (#223) (#807)
Browse files* work on branching
* branching
* work on input field
* wip
* wip
* helper stuff
* pass tests
* add type guards and clean up type requirements
* fixed shared conv type
* more helpers stuff
* clean up code
* addChildren helper
* addSibling
* add test for addSibling & refacto addChildren tests
* backend work pt. 1
* backend done
* add children property to messages for easier frontend rendering
* fix message id type rail
* front-end work on simple linear conversation
* fix title generation
* convert conversations on post
* server side retry works with branching
* clean up buttons
* fix retry feature backend
* Send new messages in any subtree
* make edit previous prompts feature work
* fix padding
* revert unneeded changes
* bring back pending message
* fix front-end streaming
* fix initial message
* Revert "fix initial message"
This reverts commit 6257fe8789c52a20e7506aa453efda55c16e95bb.
* Fix bug subtree state refresh
* fix continue feature on shared conversations
* fix websearch updates
* Fix first message streaming
* fix fornt-end websearch updates
* fix loading icon
* fix bottom padding
* move children nav to below the message
* Show current message in continue & retry
* children nav styling
* you can now edit the first message
* bottom padding on assistant message
* fix test
* lint
* use <form>
* tree navigation
* misc
* mobile: hide download link
* forgot to implem continue feature lol
* fix continue feature on llama 70b
* fix edit mode
* disable submit button & nav when loading
* fix bug when interrupting
* hide arrows in edit mode
* forgot to reset edit mode when submitting retry
* reset editing when switching conversations
---------
Co-authored-by: Victor Mustar <victor.mustar@gmail.com>
- .env.template +1 -5
- src/lib/buildPrompt.ts +19 -82
- src/lib/components/chat/ChatMessage.svelte +196 -45
- src/lib/components/chat/ChatMessages.svelte +0 -106
- src/lib/components/chat/ChatWindow.svelte +115 -40
- src/lib/server/database.ts +6 -0
- src/lib/server/endpoints/aws/endpointAws.ts +4 -4
- src/lib/server/endpoints/endpoints.ts +4 -7
- src/lib/server/endpoints/llamacpp/endpointLlamacpp.ts +4 -4
- src/lib/server/endpoints/ollama/endpointOllama.ts +4 -4
- src/lib/server/endpoints/openai/endpointOai.ts +16 -43
- src/lib/server/endpoints/tgi/endpointTgi.ts +5 -16
- src/lib/server/generateFromDefaultEndpoint.ts +1 -1
- src/lib/server/preprocessMessages.ts +57 -0
- src/lib/server/websearch/runWebSearch.ts +2 -5
- src/lib/stores/convTree.ts +25 -0
- src/lib/types/Conversation.ts +1 -0
- src/lib/types/Message.ts +7 -1
- src/lib/types/SharedConversation.ts +14 -14
- src/lib/utils/tree/addChildren.spec.ts +102 -0
- src/lib/utils/tree/addChildren.ts +52 -0
- src/lib/utils/tree/addSibling.spec.ts +80 -0
- src/lib/utils/tree/addSibling.ts +45 -0
- src/lib/utils/tree/buildSubtree.spec.ts +110 -0
- src/lib/utils/tree/buildSubtree.ts +28 -0
- src/lib/utils/tree/convertLegacyConversation.spec.ts +31 -0
- src/lib/utils/tree/convertLegacyConversation.ts +35 -0
- src/lib/utils/tree/isMessageId.spec.ts +14 -0
- src/lib/utils/tree/isMessageId.ts +5 -0
- src/lib/utils/tree/treeHelpers.spec.ts +164 -0
- src/routes/conversation/+server.ts +15 -1
- src/routes/conversation/[id]/+page.server.ts +10 -6
- src/routes/conversation/[id]/+page.svelte +118 -67
- src/routes/conversation/[id]/+server.ts +154 -112
- src/routes/conversation/[id]/message/[messageId]/prompt/+server.ts +4 -3
- src/routes/conversation/[id]/share/+server.ts +3 -2
@@ -33,10 +33,6 @@ MODELS=`[
|
|
33 |
"name": "meta-llama/Llama-2-70b-chat-hf",
|
34 |
"description": "The latest and biggest model from Meta, fine-tuned for chat.",
|
35 |
"websiteUrl": "https://ai.meta.com/llama/",
|
36 |
-
"userMessageToken": "",
|
37 |
-
"userMessageEndToken": " [/INST] ",
|
38 |
-
"assistantMessageToken": "",
|
39 |
-
"assistantMessageEndToken": " </s><s>[INST] ",
|
40 |
"preprompt": " ",
|
41 |
"chatPromptTemplate" : "<s>[INST] <<SYS>>\n{{preprompt}}\n<</SYS>>\n\n{{#each messages}}{{#ifUser}}{{content}} [/INST] {{/ifUser}}{{#ifAssistant}}{{content}} </s><s>[INST] {{/ifAssistant}}{{/each}}",
|
42 |
"promptExamples": [
|
@@ -58,7 +54,7 @@ MODELS=`[
|
|
58 |
"top_k": 50,
|
59 |
"truncate": 3072,
|
60 |
"max_new_tokens": 1024,
|
61 |
-
"stop" : ["</s>", "
|
62 |
}
|
63 |
},
|
64 |
{
|
|
|
33 |
"name": "meta-llama/Llama-2-70b-chat-hf",
|
34 |
"description": "The latest and biggest model from Meta, fine-tuned for chat.",
|
35 |
"websiteUrl": "https://ai.meta.com/llama/",
|
|
|
|
|
|
|
|
|
36 |
"preprompt": " ",
|
37 |
"chatPromptTemplate" : "<s>[INST] <<SYS>>\n{{preprompt}}\n<</SYS>>\n\n{{#each messages}}{{#ifUser}}{{content}} [/INST] {{/ifUser}}{{#ifAssistant}}{{content}} </s><s>[INST] {{/ifAssistant}}{{/each}}",
|
38 |
"promptExamples": [
|
|
|
54 |
"top_k": 50,
|
55 |
"truncate": 3072,
|
56 |
"max_new_tokens": 1024,
|
57 |
+
"stop" : ["</s>", "</s><s>[INST]"]
|
58 |
}
|
59 |
},
|
60 |
{
|
@@ -1,94 +1,31 @@
|
|
|
|
1 |
import type { BackendModel } from "./server/models";
|
2 |
-
import type { Message } from "./types/Message";
|
3 |
-
import { format } from "date-fns";
|
4 |
-
import type { WebSearch } from "./types/WebSearch";
|
5 |
-
import { downloadFile } from "./server/files/downloadFile";
|
6 |
-
import type { Conversation } from "./types/Conversation";
|
7 |
|
8 |
-
|
9 |
-
messages: Pick<Message, "from" | "content" | "files">[];
|
10 |
-
id?: Conversation["_id"];
|
11 |
model: BackendModel;
|
12 |
-
|
13 |
-
webSearch?: WebSearch;
|
14 |
-
preprompt?: string;
|
15 |
-
files?: File[];
|
16 |
-
continue?: boolean;
|
17 |
-
}
|
18 |
|
19 |
export async function buildPrompt({
|
20 |
messages,
|
21 |
model,
|
22 |
-
webSearch,
|
23 |
preprompt,
|
24 |
-
|
25 |
}: buildPromptOptions): Promise<string> {
|
26 |
-
let
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
const currentDate = format(new Date(), "MMMM d, yyyy");
|
42 |
-
|
43 |
-
// update the last user message directly (that way if the last message is an assistant partial answer, we keep the beginning of that answer)
|
44 |
-
modifiedMessages[lastUsrMsgIndex] = {
|
45 |
-
from: "user",
|
46 |
-
content: `I searched the web using the query: ${webSearch.searchQuery}. Today is ${currentDate} and here are the results:
|
47 |
-
=====================
|
48 |
-
${webSearch.context}
|
49 |
-
=====================
|
50 |
-
${previousQuestions}
|
51 |
-
Answer the question: ${messages[lastUsrMsgIndex].content}`,
|
52 |
-
};
|
53 |
-
}
|
54 |
-
// section to handle potential files input
|
55 |
-
if (model.multimodal) {
|
56 |
-
modifiedMessages = await Promise.all(
|
57 |
-
modifiedMessages.map(async (el) => {
|
58 |
-
let content = el.content;
|
59 |
-
|
60 |
-
if (el.from === "user") {
|
61 |
-
if (el?.files && el.files.length > 0 && id) {
|
62 |
-
const markdowns = await Promise.all(
|
63 |
-
el.files.map(async (hash) => {
|
64 |
-
try {
|
65 |
-
const { content: image, mime } = await downloadFile(hash, id);
|
66 |
-
const b64 = image.toString("base64");
|
67 |
-
return `![](data:${mime};base64,${b64})})`;
|
68 |
-
} catch (e) {
|
69 |
-
console.error(e);
|
70 |
-
}
|
71 |
-
})
|
72 |
-
);
|
73 |
-
content += markdowns.join("\n ");
|
74 |
-
} else {
|
75 |
-
// if no image, append an empty white image
|
76 |
-
content +=
|
77 |
-
"\n![](data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAAQABADAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD+/igAoAKACgD/2Q==)";
|
78 |
-
}
|
79 |
-
}
|
80 |
-
|
81 |
-
return { ...el, content };
|
82 |
-
})
|
83 |
-
);
|
84 |
}
|
85 |
|
86 |
-
return
|
87 |
-
model
|
88 |
-
.chatPromptRender({ messages: modifiedMessages, preprompt })
|
89 |
-
// Not super precise, but it's truncated in the model's backend anyway
|
90 |
-
.split(" ")
|
91 |
-
.slice(-(model.parameters?.truncate ?? 0))
|
92 |
-
.join(" ")
|
93 |
-
);
|
94 |
}
|
|
|
1 |
+
import type { EndpointParameters } from "./server/endpoints/endpoints";
|
2 |
import type { BackendModel } from "./server/models";
|
|
|
|
|
|
|
|
|
|
|
3 |
|
4 |
+
type buildPromptOptions = Pick<EndpointParameters, "messages" | "preprompt" | "continueMessage"> & {
|
|
|
|
|
5 |
model: BackendModel;
|
6 |
+
};
|
|
|
|
|
|
|
|
|
|
|
7 |
|
8 |
export async function buildPrompt({
|
9 |
messages,
|
10 |
model,
|
|
|
11 |
preprompt,
|
12 |
+
continueMessage,
|
13 |
}: buildPromptOptions): Promise<string> {
|
14 |
+
let prompt = model
|
15 |
+
.chatPromptRender({ messages, preprompt })
|
16 |
+
// Not super precise, but it's truncated in the model's backend anyway
|
17 |
+
.split(" ")
|
18 |
+
.slice(-(model.parameters?.truncate ?? 0))
|
19 |
+
.join(" ");
|
20 |
+
|
21 |
+
if (continueMessage && model.parameters?.stop) {
|
22 |
+
prompt = model.parameters.stop.reduce((acc: string, curr: string) => {
|
23 |
+
if (acc.endsWith(curr)) {
|
24 |
+
return acc.slice(0, acc.length - curr.length);
|
25 |
+
}
|
26 |
+
return acc;
|
27 |
+
}, prompt.trimEnd());
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
28 |
}
|
29 |
|
30 |
+
return prompt;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
31 |
}
|
@@ -2,7 +2,7 @@
|
|
2 |
import { marked } from "marked";
|
3 |
import markedKatex from "marked-katex-extension";
|
4 |
import type { Message } from "$lib/types/Message";
|
5 |
-
import { afterUpdate, createEventDispatcher } from "svelte";
|
6 |
import { deepestChild } from "$lib/utils/deepestChild";
|
7 |
import { page } from "$app/stores";
|
8 |
|
@@ -13,6 +13,9 @@
|
|
13 |
import CarbonDownload from "~icons/carbon/download";
|
14 |
import CarbonThumbsUp from "~icons/carbon/thumbs-up";
|
15 |
import CarbonThumbsDown from "~icons/carbon/thumbs-down";
|
|
|
|
|
|
|
16 |
|
17 |
import { PUBLIC_SEP_TOKEN } from "$lib/constants/publicSepToken";
|
18 |
import type { Model } from "$lib/types/Model";
|
@@ -20,6 +23,7 @@
|
|
20 |
import OpenWebSearchResults from "../OpenWebSearchResults.svelte";
|
21 |
import type { WebSearchUpdate } from "$lib/types/MessageUpdate";
|
22 |
import { base } from "$app/paths";
|
|
|
23 |
|
24 |
function sanitizeMd(md: string) {
|
25 |
let ret = md
|
@@ -45,16 +49,17 @@
|
|
45 |
}
|
46 |
|
47 |
export let model: Model;
|
48 |
-
export let
|
|
|
49 |
export let loading = false;
|
50 |
export let isAuthor = true;
|
51 |
export let readOnly = false;
|
52 |
export let isTapped = false;
|
53 |
|
54 |
-
|
55 |
|
56 |
const dispatch = createEventDispatcher<{
|
57 |
-
retry: { content
|
58 |
vote: { score: Message["score"]; id: Message["id"] };
|
59 |
}>();
|
60 |
|
@@ -63,6 +68,8 @@
|
|
63 |
let pendingTimeout: ReturnType<typeof setTimeout>;
|
64 |
let isCopied = false;
|
65 |
|
|
|
|
|
66 |
const renderer = new marked.Renderer();
|
67 |
// For code blocks with simple backticks
|
68 |
renderer.codespan = (code) => {
|
@@ -91,12 +98,15 @@
|
|
91 |
|
92 |
$: tokens = marked.lexer(sanitizeMd(message.content));
|
93 |
|
|
|
|
|
|
|
94 |
afterUpdate(() => {
|
95 |
loadingEl?.$destroy();
|
96 |
clearTimeout(pendingTimeout);
|
97 |
|
98 |
// Add loading animation to the last message if update takes more than 600ms
|
99 |
-
if (loading) {
|
100 |
pendingTimeout = setTimeout(() => {
|
101 |
if (contentEl) {
|
102 |
loadingEl = new IconLoading({
|
@@ -108,11 +118,14 @@
|
|
108 |
}
|
109 |
});
|
110 |
|
111 |
-
|
|
|
|
|
|
|
|
|
112 |
|
113 |
-
$: searchUpdates = ((
|
114 |
-
|
115 |
-
: message.updates?.filter(({ type }) => type === "webSearch")) ?? []) as WebSearchUpdate[];
|
116 |
|
117 |
$: downloadLink =
|
118 |
message.from === "user" ? `${$page.url.pathname}/message/${message.id}/prompt` : undefined;
|
@@ -131,11 +144,40 @@
|
|
131 |
isCopied = false;
|
132 |
}, 1000);
|
133 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
134 |
</script>
|
135 |
|
136 |
{#if message.from === "assistant"}
|
137 |
<div
|
138 |
-
class="group relative -mb-
|
139 |
role="presentation"
|
140 |
on:click={() => (isTapped = !isTapped)}
|
141 |
on:keydown={() => (isTapped = !isTapped)}
|
@@ -162,9 +204,6 @@
|
|
162 |
webSearchMessages={searchUpdates}
|
163 |
/>
|
164 |
{/if}
|
165 |
-
{#if !message.content && (webSearchIsDone || (webSearchMessages && webSearchMessages.length === 0))}
|
166 |
-
<IconLoading />
|
167 |
-
{/if}
|
168 |
|
169 |
<div
|
170 |
class="prose max-w-none max-sm:prose-sm dark:prose-invert prose-headings:font-semibold prose-h1:text-lg prose-h2:text-base prose-h3:text-base prose-pre:bg-gray-800 dark:prose-pre:bg-gray-900"
|
@@ -179,6 +218,7 @@
|
|
179 |
{/if}
|
180 |
{/each}
|
181 |
</div>
|
|
|
182 |
<!-- Web Search sources -->
|
183 |
{#if webSearchSources?.length}
|
184 |
<div class="mt-4 flex flex-wrap items-center gap-x-2 gap-y-1.5 text-sm">
|
@@ -202,7 +242,7 @@
|
|
202 |
</div>
|
203 |
{#if isAuthor && !loading && message.content}
|
204 |
<div
|
205 |
-
class="absolute bottom-1 right-0 flex max-md:transition-all md:bottom-0 md:group-hover:visible md:group-hover:opacity-100
|
206 |
{message.score ? 'visible opacity-100' : 'invisible max-md:-translate-y-4 max-md:opacity-0'}
|
207 |
{isTapped || isCopied ? 'max-md:visible max-md:translate-y-0 max-md:opacity-100' : ''}
|
208 |
"
|
@@ -230,6 +270,14 @@
|
|
230 |
>
|
231 |
<CarbonThumbsDown class="h-[1.14em] w-[1.14em]" />
|
232 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
233 |
<CopyToClipBoardBtn
|
234 |
on:click={() => {
|
235 |
isCopied = true;
|
@@ -240,10 +288,16 @@
|
|
240 |
</div>
|
241 |
{/if}
|
242 |
</div>
|
|
|
243 |
{/if}
|
244 |
{#if message.from === "user"}
|
245 |
-
<div
|
246 |
-
|
|
|
|
|
|
|
|
|
|
|
247 |
{#if message.files && message.files.length > 0}
|
248 |
<div class="mx-auto grid w-fit grid-cols-2 gap-5 px-5">
|
249 |
{#each message.files as file}
|
@@ -266,36 +320,133 @@
|
|
266 |
</div>
|
267 |
{/if}
|
268 |
|
269 |
-
<div
|
270 |
-
|
271 |
-
|
272 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
273 |
</div>
|
274 |
-
|
275 |
-
<div class="absolute right-0 top-3.5 flex gap-2 lg:-right-2">
|
276 |
-
{#if downloadLink}
|
277 |
-
<a
|
278 |
-
class="rounded-lg border border-gray-100 p-1 text-xs text-gray-400 group-hover:block hover:text-gray-500 md:hidden dark:border-gray-800 dark:text-gray-400 dark:hover:text-gray-300"
|
279 |
-
title="Download prompt and parameters"
|
280 |
-
type="button"
|
281 |
-
target="_blank"
|
282 |
-
href={downloadLink}
|
283 |
-
>
|
284 |
-
<CarbonDownload />
|
285 |
-
</a>
|
286 |
-
{/if}
|
287 |
-
{#if !readOnly}
|
288 |
-
<button
|
289 |
-
class="cursor-pointer rounded-lg border border-gray-100 p-1 text-xs text-gray-400 group-hover:block hover:text-gray-500 md:hidden lg:-right-2 dark:border-gray-800 dark:text-gray-400 dark:hover:text-gray-300"
|
290 |
-
title="Retry"
|
291 |
-
type="button"
|
292 |
-
on:click={() => dispatch("retry", { content: message.content, id: message.id })}
|
293 |
-
>
|
294 |
-
<CarbonRotate360 />
|
295 |
-
</button>
|
296 |
-
{/if}
|
297 |
-
</div>
|
298 |
-
{/if}
|
299 |
</div>
|
300 |
</div>
|
301 |
{/if}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
import { marked } from "marked";
|
3 |
import markedKatex from "marked-katex-extension";
|
4 |
import type { Message } from "$lib/types/Message";
|
5 |
+
import { afterUpdate, createEventDispatcher, tick } from "svelte";
|
6 |
import { deepestChild } from "$lib/utils/deepestChild";
|
7 |
import { page } from "$app/stores";
|
8 |
|
|
|
13 |
import CarbonDownload from "~icons/carbon/download";
|
14 |
import CarbonThumbsUp from "~icons/carbon/thumbs-up";
|
15 |
import CarbonThumbsDown from "~icons/carbon/thumbs-down";
|
16 |
+
import CarbonPen from "~icons/carbon/pen";
|
17 |
+
import CarbonChevronLeft from "~icons/carbon/chevron-left";
|
18 |
+
import CarbonChevronRight from "~icons/carbon/chevron-right";
|
19 |
|
20 |
import { PUBLIC_SEP_TOKEN } from "$lib/constants/publicSepToken";
|
21 |
import type { Model } from "$lib/types/Model";
|
|
|
23 |
import OpenWebSearchResults from "../OpenWebSearchResults.svelte";
|
24 |
import type { WebSearchUpdate } from "$lib/types/MessageUpdate";
|
25 |
import { base } from "$app/paths";
|
26 |
+
import { useConvTreeStore } from "$lib/stores/convTree";
|
27 |
|
28 |
function sanitizeMd(md: string) {
|
29 |
let ret = md
|
|
|
49 |
}
|
50 |
|
51 |
export let model: Model;
|
52 |
+
export let id: Message["id"];
|
53 |
+
export let messages: Message[];
|
54 |
export let loading = false;
|
55 |
export let isAuthor = true;
|
56 |
export let readOnly = false;
|
57 |
export let isTapped = false;
|
58 |
|
59 |
+
$: message = messages.find((m) => m.id === id) ?? ({} as Message);
|
60 |
|
61 |
const dispatch = createEventDispatcher<{
|
62 |
+
retry: { content?: string; id: Message["id"] };
|
63 |
vote: { score: Message["score"]; id: Message["id"] };
|
64 |
}>();
|
65 |
|
|
|
68 |
let pendingTimeout: ReturnType<typeof setTimeout>;
|
69 |
let isCopied = false;
|
70 |
|
71 |
+
let initialized = false;
|
72 |
+
|
73 |
const renderer = new marked.Renderer();
|
74 |
// For code blocks with simple backticks
|
75 |
renderer.codespan = (code) => {
|
|
|
98 |
|
99 |
$: tokens = marked.lexer(sanitizeMd(message.content));
|
100 |
|
101 |
+
$: emptyLoad =
|
102 |
+
!message.content && (webSearchIsDone || (searchUpdates && searchUpdates.length === 0));
|
103 |
+
|
104 |
afterUpdate(() => {
|
105 |
loadingEl?.$destroy();
|
106 |
clearTimeout(pendingTimeout);
|
107 |
|
108 |
// Add loading animation to the last message if update takes more than 600ms
|
109 |
+
if ((loading && isLast) || emptyLoad) {
|
110 |
pendingTimeout = setTimeout(() => {
|
111 |
if (contentEl) {
|
112 |
loadingEl = new IconLoading({
|
|
|
118 |
}
|
119 |
});
|
120 |
|
121 |
+
function handleKeyDown(e: KeyboardEvent) {
|
122 |
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
123 |
+
editFormEl.requestSubmit();
|
124 |
+
}
|
125 |
+
}
|
126 |
|
127 |
+
$: searchUpdates = (message.updates?.filter(({ type }) => type === "webSearch") ??
|
128 |
+
[]) as WebSearchUpdate[];
|
|
|
129 |
|
130 |
$: downloadLink =
|
131 |
message.from === "user" ? `${$page.url.pathname}/message/${message.id}/prompt` : undefined;
|
|
|
144 |
isCopied = false;
|
145 |
}, 1000);
|
146 |
}
|
147 |
+
|
148 |
+
$: editMode = $convTreeStore.editing === message.id;
|
149 |
+
let editContentEl: HTMLTextAreaElement;
|
150 |
+
let editFormEl: HTMLFormElement;
|
151 |
+
|
152 |
+
$: if (editMode) {
|
153 |
+
tick();
|
154 |
+
if (editContentEl) {
|
155 |
+
editContentEl.value = message.content;
|
156 |
+
editContentEl?.focus();
|
157 |
+
}
|
158 |
+
}
|
159 |
+
|
160 |
+
$: isLast = (message && message.children?.length === 0) ?? false;
|
161 |
+
|
162 |
+
$: childrenToRender = 0;
|
163 |
+
$: nChildren = message?.children?.length ?? 0;
|
164 |
+
|
165 |
+
$: {
|
166 |
+
if (initialized) {
|
167 |
+
childrenToRender = Math.max(0, nChildren - 1);
|
168 |
+
} else {
|
169 |
+
childrenToRender = 0;
|
170 |
+
initialized = true;
|
171 |
+
}
|
172 |
+
}
|
173 |
+
const convTreeStore = useConvTreeStore();
|
174 |
+
|
175 |
+
$: if (message.children?.length === 0) $convTreeStore.leaf = message.id;
|
176 |
</script>
|
177 |
|
178 |
{#if message.from === "assistant"}
|
179 |
<div
|
180 |
+
class="group relative -mb-6 flex items-start justify-start gap-4 pb-4 leading-relaxed"
|
181 |
role="presentation"
|
182 |
on:click={() => (isTapped = !isTapped)}
|
183 |
on:keydown={() => (isTapped = !isTapped)}
|
|
|
204 |
webSearchMessages={searchUpdates}
|
205 |
/>
|
206 |
{/if}
|
|
|
|
|
|
|
207 |
|
208 |
<div
|
209 |
class="prose max-w-none max-sm:prose-sm dark:prose-invert prose-headings:font-semibold prose-h1:text-lg prose-h2:text-base prose-h3:text-base prose-pre:bg-gray-800 dark:prose-pre:bg-gray-900"
|
|
|
218 |
{/if}
|
219 |
{/each}
|
220 |
</div>
|
221 |
+
|
222 |
<!-- Web Search sources -->
|
223 |
{#if webSearchSources?.length}
|
224 |
<div class="mt-4 flex flex-wrap items-center gap-x-2 gap-y-1.5 text-sm">
|
|
|
242 |
</div>
|
243 |
{#if isAuthor && !loading && message.content}
|
244 |
<div
|
245 |
+
class="absolute bottom-1 right-0 -mb-4 flex max-md:transition-all md:bottom-0 md:group-hover:visible md:group-hover:opacity-100
|
246 |
{message.score ? 'visible opacity-100' : 'invisible max-md:-translate-y-4 max-md:opacity-0'}
|
247 |
{isTapped || isCopied ? 'max-md:visible max-md:translate-y-0 max-md:opacity-100' : ''}
|
248 |
"
|
|
|
270 |
>
|
271 |
<CarbonThumbsDown class="h-[1.14em] w-[1.14em]" />
|
272 |
</button>
|
273 |
+
<button
|
274 |
+
class="btn rounded-sm p-1 text-sm text-gray-400 focus:ring-0 hover:text-gray-500 dark:text-gray-400 dark:hover:text-gray-300"
|
275 |
+
title="Retry"
|
276 |
+
type="button"
|
277 |
+
on:click={() => dispatch("retry", { id: message.id })}
|
278 |
+
>
|
279 |
+
<CarbonRotate360 />
|
280 |
+
</button>
|
281 |
<CopyToClipBoardBtn
|
282 |
on:click={() => {
|
283 |
isCopied = true;
|
|
|
288 |
</div>
|
289 |
{/if}
|
290 |
</div>
|
291 |
+
<slot name="childrenNav" />
|
292 |
{/if}
|
293 |
{#if message.from === "user"}
|
294 |
+
<div
|
295 |
+
class="group relative w-full items-start justify-start gap-4 max-sm:text-sm"
|
296 |
+
role="presentation"
|
297 |
+
on:click={() => (isTapped = !isTapped)}
|
298 |
+
on:keydown={() => (isTapped = !isTapped)}
|
299 |
+
>
|
300 |
+
<div class="flex w-full flex-col">
|
301 |
{#if message.files && message.files.length > 0}
|
302 |
<div class="mx-auto grid w-fit grid-cols-2 gap-5 px-5">
|
303 |
{#each message.files as file}
|
|
|
320 |
</div>
|
321 |
{/if}
|
322 |
|
323 |
+
<div class="flex w-full flex-row flex-nowrap">
|
324 |
+
{#if !editMode}
|
325 |
+
<p
|
326 |
+
class="disabled w-full appearance-none whitespace-break-spaces text-wrap break-words bg-inherit px-5 py-3.5 text-gray-500 dark:text-gray-400"
|
327 |
+
>
|
328 |
+
{message.content.trim()}
|
329 |
+
</p>
|
330 |
+
{:else}
|
331 |
+
<form
|
332 |
+
class="flex w-full flex-col"
|
333 |
+
bind:this={editFormEl}
|
334 |
+
on:submit|preventDefault={() => {
|
335 |
+
dispatch("retry", { content: editContentEl.value, id: message.id });
|
336 |
+
$convTreeStore.editing = null;
|
337 |
+
}}
|
338 |
+
>
|
339 |
+
<textarea
|
340 |
+
class="w-full whitespace-break-spaces break-words rounded-lg bg-gray-100 px-5 py-3.5 text-gray-500 *:h-max dark:bg-gray-800 dark:text-gray-400"
|
341 |
+
bind:this={editContentEl}
|
342 |
+
value={message.content.trim()}
|
343 |
+
on:keydown={handleKeyDown}
|
344 |
+
required
|
345 |
+
/>
|
346 |
+
<div class="flex w-full flex-row flex-nowrap items-center justify-center gap-2 pt-2">
|
347 |
+
<button
|
348 |
+
type="submit"
|
349 |
+
class="btn rounded-lg px-3 py-1.5 text-sm
|
350 |
+
{loading
|
351 |
+
? 'bg-gray-300 text-gray-400 dark:bg-gray-700 dark:text-gray-600'
|
352 |
+
: 'bg-gray-200 text-gray-600 focus:ring-0 hover:text-gray-800 dark:bg-gray-800 dark:text-gray-300 dark:hover:text-gray-200'}
|
353 |
+
"
|
354 |
+
disabled={loading}
|
355 |
+
>
|
356 |
+
Submit
|
357 |
+
</button>
|
358 |
+
<button
|
359 |
+
type="button"
|
360 |
+
class="btn rounded-sm p-2 text-sm text-gray-400 focus:ring-0 hover:text-gray-500 dark:text-gray-400 dark:hover:text-gray-300"
|
361 |
+
on:click={() => {
|
362 |
+
$convTreeStore.editing = null;
|
363 |
+
}}
|
364 |
+
>
|
365 |
+
Cancel
|
366 |
+
</button>
|
367 |
+
</div>
|
368 |
+
</form>
|
369 |
+
{/if}
|
370 |
+
{#if !loading && !editMode}
|
371 |
+
<div
|
372 |
+
class="
|
373 |
+
max-md:opacity-0' invisible absolute
|
374 |
+
right-0 top-3.5 z-10 h-max max-md:-translate-y-4 max-md:transition-all md:bottom-0 md:group-hover:visible md:group-hover:opacity-100 {isTapped ||
|
375 |
+
isCopied
|
376 |
+
? 'max-md:visible max-md:translate-y-0 max-md:opacity-100'
|
377 |
+
: ''}"
|
378 |
+
>
|
379 |
+
<div class="mx-auto flex flex-row flex-nowrap gap-2">
|
380 |
+
{#if downloadLink}
|
381 |
+
<a
|
382 |
+
class="rounded-lg border border-gray-100 bg-gray-100 p-1 text-xs text-gray-400 group-hover:block hover:text-gray-500 max-sm:!hidden md:hidden dark:border-gray-800 dark:bg-gray-800 dark:text-gray-400 dark:hover:text-gray-300"
|
383 |
+
title="Download prompt and parameters"
|
384 |
+
type="button"
|
385 |
+
target="_blank"
|
386 |
+
href={downloadLink}
|
387 |
+
>
|
388 |
+
<CarbonDownload />
|
389 |
+
</a>
|
390 |
+
{/if}
|
391 |
+
{#if !readOnly}
|
392 |
+
<button
|
393 |
+
class="cursor-pointer rounded-lg border border-gray-100 bg-gray-100 p-1 text-xs text-gray-400 group-hover:block hover:text-gray-500 md:hidden lg:-right-2 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-400 dark:hover:text-gray-300"
|
394 |
+
title="Branch"
|
395 |
+
type="button"
|
396 |
+
on:click={() => ($convTreeStore.editing = message.id)}
|
397 |
+
>
|
398 |
+
<CarbonPen />
|
399 |
+
</button>
|
400 |
+
{/if}
|
401 |
+
</div>
|
402 |
+
</div>
|
403 |
+
{/if}
|
404 |
</div>
|
405 |
+
<slot name="childrenNav" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
406 |
</div>
|
407 |
</div>
|
408 |
{/if}
|
409 |
+
|
410 |
+
{#if nChildren > 0}
|
411 |
+
<svelte:self
|
412 |
+
{loading}
|
413 |
+
{messages}
|
414 |
+
{isAuthor}
|
415 |
+
{readOnly}
|
416 |
+
{model}
|
417 |
+
id={messages.find((m) => m.id === id)?.children?.[childrenToRender]}
|
418 |
+
on:retry
|
419 |
+
on:vote
|
420 |
+
on:continue
|
421 |
+
>
|
422 |
+
<svelte:fragment slot="childrenNav">
|
423 |
+
{#if nChildren > 1 && $convTreeStore.editing === null}
|
424 |
+
<div
|
425 |
+
class="font-white z-10 -mt-1 ml-3.5 mr-auto flex h-6 w-fit select-none flex-row items-center justify-center gap-1 text-sm"
|
426 |
+
>
|
427 |
+
<button
|
428 |
+
class="inline text-lg font-thin text-gray-400 disabled:pointer-events-none disabled:opacity-25 hover:text-gray-800 dark:text-gray-500 dark:hover:text-gray-200"
|
429 |
+
on:click={() => (childrenToRender = Math.max(0, childrenToRender - 1))}
|
430 |
+
disabled={childrenToRender === 0 || loading}
|
431 |
+
>
|
432 |
+
<CarbonChevronLeft class="text-sm" />
|
433 |
+
</button>
|
434 |
+
<span class=" text-gray-400 dark:text-gray-500">
|
435 |
+
{childrenToRender + 1} / {nChildren}
|
436 |
+
</span>
|
437 |
+
<button
|
438 |
+
class="inline text-lg font-thin text-gray-400 disabled:pointer-events-none disabled:opacity-25 hover:text-gray-800 dark:text-gray-500 dark:hover:text-gray-200"
|
439 |
+
on:click={() =>
|
440 |
+
(childrenToRender = Math.min(
|
441 |
+
message?.children?.length ?? 1 - 1,
|
442 |
+
childrenToRender + 1
|
443 |
+
))}
|
444 |
+
disabled={childrenToRender === nChildren - 1 || loading}
|
445 |
+
>
|
446 |
+
<CarbonChevronRight class="text-sm" />
|
447 |
+
</button>
|
448 |
+
</div>
|
449 |
+
{/if}
|
450 |
+
</svelte:fragment>
|
451 |
+
</svelte:self>
|
452 |
+
{/if}
|
@@ -1,106 +0,0 @@
|
|
1 |
-
<script lang="ts">
|
2 |
-
import type { Message } from "$lib/types/Message";
|
3 |
-
import { snapScrollToBottom } from "$lib/actions/snapScrollToBottom";
|
4 |
-
import ScrollToBottomBtn from "$lib/components/ScrollToBottomBtn.svelte";
|
5 |
-
import { tick } from "svelte";
|
6 |
-
import { randomUUID } from "$lib/utils/randomUuid";
|
7 |
-
import type { Model } from "$lib/types/Model";
|
8 |
-
import ChatIntroduction from "./ChatIntroduction.svelte";
|
9 |
-
import ChatMessage from "./ChatMessage.svelte";
|
10 |
-
import type { WebSearchUpdate } from "$lib/types/MessageUpdate";
|
11 |
-
import { browser } from "$app/environment";
|
12 |
-
import SystemPromptModal from "../SystemPromptModal.svelte";
|
13 |
-
import type { Assistant } from "$lib/types/Assistant";
|
14 |
-
import AssistantIntroduction from "./AssistantIntroduction.svelte";
|
15 |
-
import { page } from "$app/stores";
|
16 |
-
import { base } from "$app/paths";
|
17 |
-
|
18 |
-
export let messages: Message[];
|
19 |
-
export let loading: boolean;
|
20 |
-
export let pending: boolean;
|
21 |
-
export let isAuthor: boolean;
|
22 |
-
export let currentModel: Model;
|
23 |
-
export let assistant: Assistant | undefined;
|
24 |
-
export let models: Model[];
|
25 |
-
export let preprompt: string | undefined;
|
26 |
-
export let readOnly: boolean;
|
27 |
-
|
28 |
-
let chatContainer: HTMLElement;
|
29 |
-
|
30 |
-
export let webSearchMessages: WebSearchUpdate[] = [];
|
31 |
-
|
32 |
-
async function scrollToBottom() {
|
33 |
-
await tick();
|
34 |
-
chatContainer.scrollTop = chatContainer.scrollHeight;
|
35 |
-
}
|
36 |
-
|
37 |
-
// If last message is from user, scroll to bottom
|
38 |
-
$: if (browser && messages[messages.length - 1]?.from === "user") {
|
39 |
-
scrollToBottom();
|
40 |
-
}
|
41 |
-
</script>
|
42 |
-
|
43 |
-
<div
|
44 |
-
class="scrollbar-custom mr-1 h-full overflow-y-auto"
|
45 |
-
use:snapScrollToBottom={messages.length ? [...messages, ...webSearchMessages] : false}
|
46 |
-
bind:this={chatContainer}
|
47 |
-
>
|
48 |
-
<div class="mx-auto flex h-full max-w-3xl flex-col gap-6 px-5 pt-6 sm:gap-8 xl:max-w-4xl">
|
49 |
-
{#each messages as message, i}
|
50 |
-
{#if i === 0 && $page.data?.assistant}
|
51 |
-
<a
|
52 |
-
class="mx-auto flex items-center gap-1.5 rounded-full border border-gray-100 bg-gray-50 py-1 pl-1 pr-3 text-sm text-gray-800 hover:bg-gray-100 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700"
|
53 |
-
href="{base}/settings/assistants/{$page.data.assistant._id}"
|
54 |
-
>
|
55 |
-
{#if $page.data?.assistant.avatar}
|
56 |
-
<img
|
57 |
-
src="{base}/settings/assistants/{$page.data?.assistant._id.toString()}/avatar.jpg?hash=${$page
|
58 |
-
.data.assistant.avatar}"
|
59 |
-
alt="Avatar"
|
60 |
-
class="size-5 rounded-full object-cover"
|
61 |
-
/>
|
62 |
-
{:else}
|
63 |
-
<div
|
64 |
-
class="flex size-6 items-center justify-center rounded-full bg-gray-300 font-bold uppercase text-gray-500"
|
65 |
-
>
|
66 |
-
{$page.data?.assistant.name[0]}
|
67 |
-
</div>
|
68 |
-
{/if}
|
69 |
-
|
70 |
-
{$page.data.assistant.name}
|
71 |
-
</a>
|
72 |
-
{:else if i === 0 && preprompt && preprompt != currentModel.preprompt}
|
73 |
-
<SystemPromptModal preprompt={preprompt ?? ""} />
|
74 |
-
{/if}
|
75 |
-
<ChatMessage
|
76 |
-
loading={loading && i === messages.length - 1}
|
77 |
-
{message}
|
78 |
-
{isAuthor}
|
79 |
-
{readOnly}
|
80 |
-
model={currentModel}
|
81 |
-
webSearchMessages={i === messages.length - 1 ? webSearchMessages : []}
|
82 |
-
on:retry
|
83 |
-
on:vote
|
84 |
-
on:continue
|
85 |
-
/>
|
86 |
-
{:else}
|
87 |
-
{#if !assistant}
|
88 |
-
<ChatIntroduction {models} {currentModel} on:message />
|
89 |
-
{:else}
|
90 |
-
<AssistantIntroduction {assistant} on:message />
|
91 |
-
{/if}
|
92 |
-
{/each}
|
93 |
-
{#if pending && messages[messages.length - 1]?.from === "user"}
|
94 |
-
<ChatMessage
|
95 |
-
message={{ from: "assistant", content: "", id: randomUUID() }}
|
96 |
-
model={currentModel}
|
97 |
-
{webSearchMessages}
|
98 |
-
/>
|
99 |
-
{/if}
|
100 |
-
<div class="h-44 flex-none" />
|
101 |
-
</div>
|
102 |
-
<ScrollToBottomBtn
|
103 |
-
class="bottom-36 right-4 max-md:hidden lg:right-10"
|
104 |
-
scrollNode={chatContainer}
|
105 |
-
/>
|
106 |
-
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,6 +1,6 @@
|
|
1 |
<script lang="ts">
|
2 |
import type { Message } from "$lib/types/Message";
|
3 |
-
import { createEventDispatcher, onDestroy } from "svelte";
|
4 |
|
5 |
import CarbonSendAltFilled from "~icons/carbon/send-alt-filled";
|
6 |
import CarbonExport from "~icons/carbon/export";
|
@@ -11,13 +11,11 @@
|
|
11 |
|
12 |
import EosIconsLoading from "~icons/eos-icons/loading";
|
13 |
|
14 |
-
import ChatMessages from "./ChatMessages.svelte";
|
15 |
import ChatInput from "./ChatInput.svelte";
|
16 |
import StopGeneratingBtn from "../StopGeneratingBtn.svelte";
|
17 |
import type { Model } from "$lib/types/Model";
|
18 |
import WebSearchToggle from "../WebSearchToggle.svelte";
|
19 |
import LoginModal from "../LoginModal.svelte";
|
20 |
-
import type { WebSearchUpdate } from "$lib/types/MessageUpdate";
|
21 |
import { page } from "$app/stores";
|
22 |
import FileDropzone from "./FileDropzone.svelte";
|
23 |
import RetryBtn from "../RetryBtn.svelte";
|
@@ -26,15 +24,23 @@
|
|
26 |
import type { Assistant } from "$lib/types/Assistant";
|
27 |
import { base } from "$app/paths";
|
28 |
import ContinueBtn from "../ContinueBtn.svelte";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
29 |
|
30 |
export let messages: Message[] = [];
|
31 |
export let loading = false;
|
32 |
export let pending = false;
|
|
|
33 |
export let shared = false;
|
34 |
export let currentModel: Model;
|
35 |
export let models: Model[];
|
36 |
export let assistant: Assistant | undefined = undefined;
|
37 |
-
export let webSearchMessages: WebSearchUpdate[] = [];
|
38 |
export let preprompt: string | undefined = undefined;
|
39 |
export let files: File[] = [];
|
40 |
|
@@ -50,7 +56,7 @@
|
|
50 |
message: string;
|
51 |
share: void;
|
52 |
stop: void;
|
53 |
-
retry: { id: Message["id"]; content
|
54 |
continue: { id: Message["id"] };
|
55 |
}>();
|
56 |
|
@@ -76,7 +82,11 @@
|
|
76 |
const onDragOver = (e: DragEvent) => {
|
77 |
e.preventDefault();
|
78 |
};
|
79 |
-
|
|
|
|
|
|
|
|
|
80 |
|
81 |
$: sources = files.map((file) => file2base64(file));
|
82 |
|
@@ -96,6 +106,18 @@
|
|
96 |
clearTimeout(timeout);
|
97 |
}
|
98 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
99 |
</script>
|
100 |
|
101 |
<div class="relative min-h-0 min-w-0">
|
@@ -106,31 +128,79 @@
|
|
106 |
}}
|
107 |
/>
|
108 |
{/if}
|
109 |
-
<
|
110 |
-
|
111 |
-
{
|
112 |
-
{
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
133 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
134 |
<div
|
135 |
class="dark:via-gray-80 pointer-events-none absolute inset-x-0 bottom-0 z-0 mx-auto flex w-full max-w-3xl flex-col items-center justify-center bg-gradient-to-t from-white via-white/80 to-white/0 px-3.5 py-4 max-md:border-t max-md:bg-white sm:px-5 md:py-8 xl:max-w-4xl dark:border-gray-800 dark:from-gray-900 dark:to-gray-900/0 max-md:dark:bg-gray-900 [&>*]:pointer-events-auto"
|
136 |
>
|
@@ -169,23 +239,28 @@
|
|
169 |
{:else if lastIsError}
|
170 |
<RetryBtn
|
171 |
classNames="ml-auto"
|
172 |
-
on:click={() =>
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
|
|
|
|
177 |
/>
|
178 |
{:else}
|
179 |
<div class="ml-auto gap-2">
|
180 |
{#if currentModel.multimodal}
|
181 |
<UploadBtn bind:files classNames="ml-auto" />
|
182 |
{/if}
|
183 |
-
{#if messages &&
|
184 |
<ContinueBtn
|
185 |
-
on:click={() =>
|
186 |
-
|
187 |
-
|
188 |
-
|
|
|
|
|
|
|
189 |
/>
|
190 |
{/if}
|
191 |
</div>
|
|
|
1 |
<script lang="ts">
|
2 |
import type { Message } from "$lib/types/Message";
|
3 |
+
import { createEventDispatcher, onDestroy, tick } from "svelte";
|
4 |
|
5 |
import CarbonSendAltFilled from "~icons/carbon/send-alt-filled";
|
6 |
import CarbonExport from "~icons/carbon/export";
|
|
|
11 |
|
12 |
import EosIconsLoading from "~icons/eos-icons/loading";
|
13 |
|
|
|
14 |
import ChatInput from "./ChatInput.svelte";
|
15 |
import StopGeneratingBtn from "../StopGeneratingBtn.svelte";
|
16 |
import type { Model } from "$lib/types/Model";
|
17 |
import WebSearchToggle from "../WebSearchToggle.svelte";
|
18 |
import LoginModal from "../LoginModal.svelte";
|
|
|
19 |
import { page } from "$app/stores";
|
20 |
import FileDropzone from "./FileDropzone.svelte";
|
21 |
import RetryBtn from "../RetryBtn.svelte";
|
|
|
24 |
import type { Assistant } from "$lib/types/Assistant";
|
25 |
import { base } from "$app/paths";
|
26 |
import ContinueBtn from "../ContinueBtn.svelte";
|
27 |
+
import AssistantIntroduction from "./AssistantIntroduction.svelte";
|
28 |
+
import ChatMessage from "./ChatMessage.svelte";
|
29 |
+
import ScrollToBottomBtn from "../ScrollToBottomBtn.svelte";
|
30 |
+
import { browser } from "$app/environment";
|
31 |
+
import { snapScrollToBottom } from "$lib/actions/snapScrollToBottom";
|
32 |
+
import SystemPromptModal from "../SystemPromptModal.svelte";
|
33 |
+
import ChatIntroduction from "./ChatIntroduction.svelte";
|
34 |
+
import { useConvTreeStore } from "$lib/stores/convTree";
|
35 |
|
36 |
export let messages: Message[] = [];
|
37 |
export let loading = false;
|
38 |
export let pending = false;
|
39 |
+
|
40 |
export let shared = false;
|
41 |
export let currentModel: Model;
|
42 |
export let models: Model[];
|
43 |
export let assistant: Assistant | undefined = undefined;
|
|
|
44 |
export let preprompt: string | undefined = undefined;
|
45 |
export let files: File[] = [];
|
46 |
|
|
|
56 |
message: string;
|
57 |
share: void;
|
58 |
stop: void;
|
59 |
+
retry: { id: Message["id"]; content?: string };
|
60 |
continue: { id: Message["id"] };
|
61 |
}>();
|
62 |
|
|
|
82 |
const onDragOver = (e: DragEvent) => {
|
83 |
e.preventDefault();
|
84 |
};
|
85 |
+
|
86 |
+
const convTreeStore = useConvTreeStore();
|
87 |
+
|
88 |
+
$: lastMessage = browser && (messages.find((m) => m.id == $convTreeStore.leaf) as Message);
|
89 |
+
$: lastIsError = lastMessage && lastMessage.from === "user" && !loading;
|
90 |
|
91 |
$: sources = files.map((file) => file2base64(file));
|
92 |
|
|
|
106 |
clearTimeout(timeout);
|
107 |
}
|
108 |
});
|
109 |
+
|
110 |
+
let chatContainer: HTMLElement;
|
111 |
+
|
112 |
+
async function scrollToBottom() {
|
113 |
+
await tick();
|
114 |
+
chatContainer.scrollTop = chatContainer.scrollHeight;
|
115 |
+
}
|
116 |
+
|
117 |
+
// If last message is from user, scroll to bottom
|
118 |
+
$: if (lastMessage && lastMessage.from === "user") {
|
119 |
+
scrollToBottom();
|
120 |
+
}
|
121 |
</script>
|
122 |
|
123 |
<div class="relative min-h-0 min-w-0">
|
|
|
128 |
}}
|
129 |
/>
|
130 |
{/if}
|
131 |
+
<div
|
132 |
+
class="scrollbar-custom mr-1 h-full overflow-y-auto"
|
133 |
+
use:snapScrollToBottom={messages.length ? [...messages] : false}
|
134 |
+
bind:this={chatContainer}
|
135 |
+
>
|
136 |
+
<div class="mx-auto flex h-full max-w-3xl flex-col gap-6 px-5 pt-6 sm:gap-8 xl:max-w-4xl">
|
137 |
+
{#if $page.data?.assistant}
|
138 |
+
<a
|
139 |
+
class="mx-auto flex items-center gap-1.5 rounded-full border border-gray-100 bg-gray-50 py-1 pl-1 pr-3 text-sm text-gray-800 hover:bg-gray-100 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700"
|
140 |
+
href="{base}/settings/assistants/{$page.data.assistant._id}"
|
141 |
+
>
|
142 |
+
{#if $page.data?.assistant.avatar}
|
143 |
+
<img
|
144 |
+
src="{base}/settings/assistants/{$page.data?.assistant._id.toString()}/avatar.jpg?hash=${$page
|
145 |
+
.data.assistant.avatar}"
|
146 |
+
alt="Avatar"
|
147 |
+
class="size-5 rounded-full object-cover"
|
148 |
+
/>
|
149 |
+
{:else}
|
150 |
+
<div
|
151 |
+
class="flex size-6 items-center justify-center rounded-full bg-gray-300 font-bold uppercase text-gray-500"
|
152 |
+
>
|
153 |
+
{$page.data?.assistant.name[0]}
|
154 |
+
</div>
|
155 |
+
{/if}
|
156 |
+
|
157 |
+
{$page.data.assistant.name}
|
158 |
+
</a>
|
159 |
+
{:else if preprompt && preprompt != currentModel.preprompt}
|
160 |
+
<SystemPromptModal preprompt={preprompt ?? ""} />
|
161 |
+
{/if}
|
162 |
|
163 |
+
{#if messages.length > 0}
|
164 |
+
<div class="flex h-max flex-col gap-6 pb-52">
|
165 |
+
<ChatMessage
|
166 |
+
{loading}
|
167 |
+
{messages}
|
168 |
+
id={messages[0].id}
|
169 |
+
isAuthor={!shared}
|
170 |
+
readOnly={isReadOnly}
|
171 |
+
model={currentModel}
|
172 |
+
on:retry
|
173 |
+
on:vote
|
174 |
+
on:continue
|
175 |
+
/>
|
176 |
+
</div>
|
177 |
+
{:else if pending}
|
178 |
+
<ChatMessage
|
179 |
+
loading={true}
|
180 |
+
messages={[
|
181 |
+
{
|
182 |
+
id: "0-0-0-0-0",
|
183 |
+
content: "",
|
184 |
+
from: "assistant",
|
185 |
+
children: [],
|
186 |
+
},
|
187 |
+
]}
|
188 |
+
id={"0-0-0-0-0"}
|
189 |
+
isAuthor={!shared}
|
190 |
+
readOnly={isReadOnly}
|
191 |
+
model={currentModel}
|
192 |
+
/>
|
193 |
+
{:else if !assistant}
|
194 |
+
<ChatIntroduction {models} {currentModel} on:message />
|
195 |
+
{:else}
|
196 |
+
<AssistantIntroduction {assistant} on:message />
|
197 |
+
{/if}
|
198 |
+
</div>
|
199 |
+
<ScrollToBottomBtn
|
200 |
+
class="bottom-36 right-4 max-md:hidden lg:right-10"
|
201 |
+
scrollNode={chatContainer}
|
202 |
+
/>
|
203 |
+
</div>
|
204 |
<div
|
205 |
class="dark:via-gray-80 pointer-events-none absolute inset-x-0 bottom-0 z-0 mx-auto flex w-full max-w-3xl flex-col items-center justify-center bg-gradient-to-t from-white via-white/80 to-white/0 px-3.5 py-4 max-md:border-t max-md:bg-white sm:px-5 md:py-8 xl:max-w-4xl dark:border-gray-800 dark:from-gray-900 dark:to-gray-900/0 max-md:dark:bg-gray-900 [&>*]:pointer-events-auto"
|
206 |
>
|
|
|
239 |
{:else if lastIsError}
|
240 |
<RetryBtn
|
241 |
classNames="ml-auto"
|
242 |
+
on:click={() => {
|
243 |
+
if (lastMessage && lastMessage.ancestors) {
|
244 |
+
dispatch("retry", {
|
245 |
+
id: lastMessage.ancestors[lastMessage.ancestors.length - 1],
|
246 |
+
});
|
247 |
+
}
|
248 |
+
}}
|
249 |
/>
|
250 |
{:else}
|
251 |
<div class="ml-auto gap-2">
|
252 |
{#if currentModel.multimodal}
|
253 |
<UploadBtn bind:files classNames="ml-auto" />
|
254 |
{/if}
|
255 |
+
{#if messages && lastMessage && lastMessage.interrupted && !isReadOnly}
|
256 |
<ContinueBtn
|
257 |
+
on:click={() => {
|
258 |
+
if (lastMessage && lastMessage.ancestors) {
|
259 |
+
dispatch("continue", {
|
260 |
+
id: lastMessage?.id,
|
261 |
+
});
|
262 |
+
}
|
263 |
+
}}
|
264 |
/>
|
265 |
{/if}
|
266 |
</div>
|
@@ -62,6 +62,12 @@ client.on("open", () => {
|
|
62 |
{ partialFilterExpression: { userId: { $exists: true } } }
|
63 |
)
|
64 |
.catch(console.error);
|
|
|
|
|
|
|
|
|
|
|
|
|
65 |
abortedGenerations.createIndex({ updatedAt: 1 }, { expireAfterSeconds: 30 }).catch(console.error);
|
66 |
abortedGenerations.createIndex({ conversationId: 1 }, { unique: true }).catch(console.error);
|
67 |
sharedConversations.createIndex({ hash: 1 }, { unique: true }).catch(console.error);
|
|
|
62 |
{ partialFilterExpression: { userId: { $exists: true } } }
|
63 |
)
|
64 |
.catch(console.error);
|
65 |
+
conversations
|
66 |
+
.createIndex(
|
67 |
+
{ "message.id": 1, "message.ancestors": 1 },
|
68 |
+
{ partialFilterExpression: { userId: { $exists: true } } }
|
69 |
+
)
|
70 |
+
.catch(console.error);
|
71 |
abortedGenerations.createIndex({ updatedAt: 1 }, { expireAfterSeconds: 30 }).catch(console.error);
|
72 |
abortedGenerations.createIndex({ conversationId: 1 }, { unique: true }).catch(console.error);
|
73 |
sharedConversations.createIndex({ hash: 1 }, { unique: true }).catch(console.error);
|
@@ -36,11 +36,11 @@ export async function endpointAws(
|
|
36 |
region,
|
37 |
});
|
38 |
|
39 |
-
return async ({
|
40 |
const prompt = await buildPrompt({
|
41 |
-
messages
|
42 |
-
|
43 |
-
preprompt
|
44 |
model,
|
45 |
});
|
46 |
|
|
|
36 |
region,
|
37 |
});
|
38 |
|
39 |
+
return async ({ messages, preprompt, continueMessage }) => {
|
40 |
const prompt = await buildPrompt({
|
41 |
+
messages,
|
42 |
+
continueMessage,
|
43 |
+
preprompt,
|
44 |
model,
|
45 |
});
|
46 |
|
@@ -8,13 +8,10 @@ import endpointLlamacpp, { endpointLlamacppParametersSchema } from "./llamacpp/e
|
|
8 |
import endpointOllama, { endpointOllamaParametersSchema } from "./ollama/endpointOllama";
|
9 |
|
10 |
// parameters passed when generating text
|
11 |
-
interface EndpointParameters {
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
_id?: Conversation["_id"];
|
16 |
-
};
|
17 |
-
continue?: boolean;
|
18 |
}
|
19 |
|
20 |
interface CommonEndpoint {
|
|
|
8 |
import endpointOllama, { endpointOllamaParametersSchema } from "./ollama/endpointOllama";
|
9 |
|
10 |
// parameters passed when generating text
|
11 |
+
export interface EndpointParameters {
|
12 |
+
messages: Omit<Conversation["messages"][0], "id">[];
|
13 |
+
preprompt?: Conversation["preprompt"];
|
14 |
+
continueMessage?: boolean; // used to signal that the last message will be extended
|
|
|
|
|
|
|
15 |
}
|
16 |
|
17 |
interface CommonEndpoint {
|
@@ -19,11 +19,11 @@ export function endpointLlamacpp(
|
|
19 |
input: z.input<typeof endpointLlamacppParametersSchema>
|
20 |
): Endpoint {
|
21 |
const { url, model } = endpointLlamacppParametersSchema.parse(input);
|
22 |
-
return async ({
|
23 |
const prompt = await buildPrompt({
|
24 |
-
messages
|
25 |
-
|
26 |
-
preprompt
|
27 |
model,
|
28 |
});
|
29 |
|
|
|
19 |
input: z.input<typeof endpointLlamacppParametersSchema>
|
20 |
): Endpoint {
|
21 |
const { url, model } = endpointLlamacppParametersSchema.parse(input);
|
22 |
+
return async ({ messages, preprompt, continueMessage }) => {
|
23 |
const prompt = await buildPrompt({
|
24 |
+
messages,
|
25 |
+
continueMessage,
|
26 |
+
preprompt,
|
27 |
model,
|
28 |
});
|
29 |
|
@@ -14,11 +14,11 @@ export const endpointOllamaParametersSchema = z.object({
|
|
14 |
export function endpointOllama(input: z.input<typeof endpointOllamaParametersSchema>): Endpoint {
|
15 |
const { url, model, ollamaName } = endpointOllamaParametersSchema.parse(input);
|
16 |
|
17 |
-
return async ({
|
18 |
const prompt = await buildPrompt({
|
19 |
-
messages
|
20 |
-
|
21 |
-
preprompt
|
22 |
model,
|
23 |
});
|
24 |
|
|
|
14 |
export function endpointOllama(input: z.input<typeof endpointOllamaParametersSchema>): Endpoint {
|
15 |
const { url, model, ollamaName } = endpointOllamaParametersSchema.parse(input);
|
16 |
|
17 |
+
return async ({ messages, preprompt, continueMessage }) => {
|
18 |
const prompt = await buildPrompt({
|
19 |
+
messages,
|
20 |
+
continueMessage,
|
21 |
+
preprompt,
|
22 |
model,
|
23 |
});
|
24 |
|
@@ -4,7 +4,6 @@ import { openAIChatToTextGenerationStream } from "./openAIChatToTextGenerationSt
|
|
4 |
import { buildPrompt } from "$lib/buildPrompt";
|
5 |
import { OPENAI_API_KEY } from "$env/static/private";
|
6 |
import type { Endpoint } from "../endpoints";
|
7 |
-
import { format } from "date-fns";
|
8 |
|
9 |
export const endpointOAIParametersSchema = z.object({
|
10 |
weight: z.number().int().positive().default(1),
|
@@ -37,16 +36,18 @@ export async function endpointOai(
|
|
37 |
});
|
38 |
|
39 |
if (completion === "completions") {
|
40 |
-
return async ({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
41 |
return openAICompletionToTextGenerationStream(
|
42 |
await openai.completions.create({
|
43 |
model: model.id ?? model.name,
|
44 |
-
prompt
|
45 |
-
messages: conversation.messages,
|
46 |
-
webSearch: conversation.messages[conversation.messages.length - 1].webSearch,
|
47 |
-
preprompt: conversation.preprompt,
|
48 |
-
model,
|
49 |
-
}),
|
50 |
stream: true,
|
51 |
max_tokens: model.parameters?.max_new_tokens,
|
52 |
stop: model.parameters?.stop,
|
@@ -57,48 +58,20 @@ export async function endpointOai(
|
|
57 |
);
|
58 |
};
|
59 |
} else if (completion === "chat_completions") {
|
60 |
-
return async ({
|
61 |
-
let
|
62 |
-
const webSearch = conversation.messages[conversation.messages.length - 1].webSearch;
|
63 |
-
|
64 |
-
if (webSearch && webSearch.context) {
|
65 |
-
const lastMsg = messages.slice(-1)[0];
|
66 |
-
const messagesWithoutLastUsrMsg = messages.slice(0, -1);
|
67 |
-
const previousUserMessages = messages.filter((el) => el.from === "user").slice(0, -1);
|
68 |
-
|
69 |
-
const previousQuestions =
|
70 |
-
previousUserMessages.length > 0
|
71 |
-
? `Previous questions: \n${previousUserMessages
|
72 |
-
.map(({ content }) => `- ${content}`)
|
73 |
-
.join("\n")}`
|
74 |
-
: "";
|
75 |
-
const currentDate = format(new Date(), "MMMM d, yyyy");
|
76 |
-
messages = [
|
77 |
-
...messagesWithoutLastUsrMsg,
|
78 |
-
{
|
79 |
-
from: "user",
|
80 |
-
content: `I searched the web using the query: ${webSearch.searchQuery}. Today is ${currentDate} and here are the results:
|
81 |
-
=====================
|
82 |
-
${webSearch.context}
|
83 |
-
=====================
|
84 |
-
${previousQuestions}
|
85 |
-
Answer the question: ${lastMsg.content}
|
86 |
-
`,
|
87 |
-
},
|
88 |
-
];
|
89 |
-
}
|
90 |
-
|
91 |
-
const messagesOpenAI = messages.map((message) => ({
|
92 |
role: message.from,
|
93 |
content: message.content,
|
94 |
}));
|
95 |
|
|
|
|
|
|
|
|
|
96 |
return openAIChatToTextGenerationStream(
|
97 |
await openai.chat.completions.create({
|
98 |
model: model.id ?? model.name,
|
99 |
-
messages:
|
100 |
-
? [{ role: "system", content: conversation.preprompt }, ...messagesOpenAI]
|
101 |
-
: messagesOpenAI,
|
102 |
stream: true,
|
103 |
max_tokens: model.parameters?.max_new_tokens,
|
104 |
stop: model.parameters?.stop,
|
|
|
4 |
import { buildPrompt } from "$lib/buildPrompt";
|
5 |
import { OPENAI_API_KEY } from "$env/static/private";
|
6 |
import type { Endpoint } from "../endpoints";
|
|
|
7 |
|
8 |
export const endpointOAIParametersSchema = z.object({
|
9 |
weight: z.number().int().positive().default(1),
|
|
|
36 |
});
|
37 |
|
38 |
if (completion === "completions") {
|
39 |
+
return async ({ messages, preprompt, continueMessage }) => {
|
40 |
+
const prompt = await buildPrompt({
|
41 |
+
messages,
|
42 |
+
continueMessage,
|
43 |
+
preprompt,
|
44 |
+
model,
|
45 |
+
});
|
46 |
+
|
47 |
return openAICompletionToTextGenerationStream(
|
48 |
await openai.completions.create({
|
49 |
model: model.id ?? model.name,
|
50 |
+
prompt,
|
|
|
|
|
|
|
|
|
|
|
51 |
stream: true,
|
52 |
max_tokens: model.parameters?.max_new_tokens,
|
53 |
stop: model.parameters?.stop,
|
|
|
58 |
);
|
59 |
};
|
60 |
} else if (completion === "chat_completions") {
|
61 |
+
return async ({ messages, preprompt }) => {
|
62 |
+
let messagesOpenAI = messages.map((message) => ({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
63 |
role: message.from,
|
64 |
content: message.content,
|
65 |
}));
|
66 |
|
67 |
+
if (messagesOpenAI?.[0]?.role !== "system") {
|
68 |
+
messagesOpenAI = [{ role: "system", content: preprompt ?? "" }, ...messagesOpenAI];
|
69 |
+
}
|
70 |
+
|
71 |
return openAIChatToTextGenerationStream(
|
72 |
await openai.chat.completions.create({
|
73 |
model: model.id ?? model.name,
|
74 |
+
messages: messagesOpenAI,
|
|
|
|
|
75 |
stream: true,
|
76 |
max_tokens: model.parameters?.max_new_tokens,
|
77 |
stop: model.parameters?.stop,
|
@@ -16,25 +16,14 @@ export const endpointTgiParametersSchema = z.object({
|
|
16 |
export function endpointTgi(input: z.input<typeof endpointTgiParametersSchema>): Endpoint {
|
17 |
const { url, accessToken, model, authorization } = endpointTgiParametersSchema.parse(input);
|
18 |
|
19 |
-
return async ({
|
20 |
-
|
21 |
-
messages
|
22 |
-
|
23 |
-
preprompt: conversation.preprompt,
|
24 |
model,
|
25 |
-
|
26 |
});
|
27 |
|
28 |
-
if (messageContinue) {
|
29 |
-
// start with the full prompt, and for each stop token, try to remove it from the end of the prompt
|
30 |
-
prompt = model.parameters.stop.reduce((acc: string, curr: string) => {
|
31 |
-
if (acc.endsWith(curr)) {
|
32 |
-
return acc.slice(0, acc.length - curr.length);
|
33 |
-
}
|
34 |
-
return acc;
|
35 |
-
}, prompt.trimEnd());
|
36 |
-
}
|
37 |
-
|
38 |
return textGenerationStream(
|
39 |
{
|
40 |
parameters: { ...model.parameters, return_full_text: false },
|
|
|
16 |
export function endpointTgi(input: z.input<typeof endpointTgiParametersSchema>): Endpoint {
|
17 |
const { url, accessToken, model, authorization } = endpointTgiParametersSchema.parse(input);
|
18 |
|
19 |
+
return async ({ messages, preprompt, continueMessage }) => {
|
20 |
+
const prompt = await buildPrompt({
|
21 |
+
messages,
|
22 |
+
preprompt,
|
|
|
23 |
model,
|
24 |
+
continueMessage,
|
25 |
});
|
26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
return textGenerationStream(
|
28 |
{
|
29 |
parameters: { ...model.parameters, return_full_text: false },
|
@@ -10,7 +10,7 @@ export async function generateFromDefaultEndpoint({
|
|
10 |
}): Promise<string> {
|
11 |
const endpoint = await smallModel.getEndpoint();
|
12 |
|
13 |
-
const tokenStream = await endpoint({
|
14 |
|
15 |
for await (const output of tokenStream) {
|
16 |
// if not generated_text is here it means the generation is not done
|
|
|
10 |
}): Promise<string> {
|
11 |
const endpoint = await smallModel.getEndpoint();
|
12 |
|
13 |
+
const tokenStream = await endpoint({ messages, preprompt });
|
14 |
|
15 |
for await (const output of tokenStream) {
|
16 |
// if not generated_text is here it means the generation is not done
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type { Conversation } from "$lib/types/Conversation";
|
2 |
+
import type { Message } from "$lib/types/Message";
|
3 |
+
import { format } from "date-fns";
|
4 |
+
import { downloadFile } from "./files/downloadFile";
|
5 |
+
|
6 |
+
export async function preprocessMessages(
|
7 |
+
messages: Message[],
|
8 |
+
multimodal: boolean,
|
9 |
+
id: Conversation["_id"]
|
10 |
+
): Promise<Message[]> {
|
11 |
+
return await Promise.all(
|
12 |
+
messages.map(async (message, idx) => {
|
13 |
+
// start by adding websearch to the last message
|
14 |
+
if (idx === messages.length - 1 && message.webSearch && message.webSearch.context) {
|
15 |
+
const lastUsrMsgIndex = messages.map((el) => el.from).lastIndexOf("user");
|
16 |
+
const previousUserMessages = messages.filter((el) => el.from === "user").slice(0, -1);
|
17 |
+
const previousQuestions =
|
18 |
+
previousUserMessages.length > 0
|
19 |
+
? `Previous questions: \n${previousUserMessages
|
20 |
+
.map(({ content }) => `- ${content}`)
|
21 |
+
.join("\n")}`
|
22 |
+
: "";
|
23 |
+
const currentDate = format(new Date(), "MMMM d, yyyy");
|
24 |
+
|
25 |
+
message.content = `I searched the web using the query: ${message.webSearch.searchQuery}. Today is ${currentDate} and here are the results:
|
26 |
+
=====================
|
27 |
+
${message.webSearch.context}
|
28 |
+
=====================
|
29 |
+
${previousQuestions}
|
30 |
+
Answer the question: ${messages[lastUsrMsgIndex].content}`;
|
31 |
+
}
|
32 |
+
// handle files if model is multimodal
|
33 |
+
if (multimodal) {
|
34 |
+
if (message.files && message.files.length > 0) {
|
35 |
+
const markdowns = await Promise.all(
|
36 |
+
message.files.map(async (hash) => {
|
37 |
+
try {
|
38 |
+
const { content: image, mime } = await downloadFile(hash, id);
|
39 |
+
const b64 = image.toString("base64");
|
40 |
+
return `![](data:${mime};base64,${b64})})`;
|
41 |
+
} catch (e) {
|
42 |
+
console.error(e);
|
43 |
+
}
|
44 |
+
})
|
45 |
+
);
|
46 |
+
message.content += markdowns.join("\n ");
|
47 |
+
} else {
|
48 |
+
// if no image, append an empty white image
|
49 |
+
message.content +=
|
50 |
+
"\n![](data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAAQABADAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD+/igAoAKACgD/2Q==)";
|
51 |
+
}
|
52 |
+
}
|
53 |
+
|
54 |
+
return message;
|
55 |
+
})
|
56 |
+
);
|
57 |
+
}
|
@@ -17,13 +17,10 @@ const DOMAIN_BLOCKLIST = ["youtube.com", "twitter.com"];
|
|
17 |
|
18 |
export async function runWebSearch(
|
19 |
conv: Conversation,
|
20 |
-
|
21 |
updatePad: (upd: MessageUpdate) => void
|
22 |
) {
|
23 |
-
const
|
24 |
-
return [...conv.messages, { content: prompt, from: "user", id: crypto.randomUUID() }];
|
25 |
-
})() satisfies Message[];
|
26 |
-
|
27 |
const webSearch: WebSearch = {
|
28 |
prompt,
|
29 |
searchQuery: "",
|
|
|
17 |
|
18 |
export async function runWebSearch(
|
19 |
conv: Conversation,
|
20 |
+
messages: Message[],
|
21 |
updatePad: (upd: MessageUpdate) => void
|
22 |
) {
|
23 |
+
const prompt = messages[messages.length - 1].content;
|
|
|
|
|
|
|
24 |
const webSearch: WebSearch = {
|
25 |
prompt,
|
26 |
searchQuery: "",
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type { Message } from "$lib/types/Message";
|
2 |
+
import { getContext, setContext } from "svelte";
|
3 |
+
import { writable, type Writable } from "svelte/store";
|
4 |
+
|
5 |
+
// used to store the id of the message that is the currently displayed leaf of the conversation tree
|
6 |
+
// (that is the last message in the current branch of the conversation tree)
|
7 |
+
|
8 |
+
interface ConvTreeStore {
|
9 |
+
leaf: Message["id"] | null;
|
10 |
+
editing: Message["id"] | null;
|
11 |
+
}
|
12 |
+
|
13 |
+
export function useConvTreeStore() {
|
14 |
+
return getContext<Writable<ConvTreeStore>>("convTreeStore");
|
15 |
+
}
|
16 |
+
|
17 |
+
export function createConvTreeStore() {
|
18 |
+
const convTreeStore = writable<ConvTreeStore>({
|
19 |
+
leaf: null,
|
20 |
+
editing: null,
|
21 |
+
});
|
22 |
+
setContext("convTreeStore", convTreeStore);
|
23 |
+
|
24 |
+
return convTreeStore;
|
25 |
+
}
|
@@ -14,6 +14,7 @@ export interface Conversation extends Timestamps {
|
|
14 |
embeddingModel: string;
|
15 |
|
16 |
title: string;
|
|
|
17 |
messages: Message[];
|
18 |
|
19 |
meta?: {
|
|
|
14 |
embeddingModel: string;
|
15 |
|
16 |
title: string;
|
17 |
+
rootMessageId?: Message["id"];
|
18 |
messages: Message[];
|
19 |
|
20 |
meta?: {
|
@@ -3,7 +3,7 @@ import type { Timestamps } from "./Timestamps";
|
|
3 |
import type { WebSearch } from "./WebSearch";
|
4 |
|
5 |
export type Message = Partial<Timestamps> & {
|
6 |
-
from: "user" | "assistant";
|
7 |
id: ReturnType<typeof crypto.randomUUID>;
|
8 |
content: string;
|
9 |
updates?: MessageUpdate[];
|
@@ -12,4 +12,10 @@ export type Message = Partial<Timestamps> & {
|
|
12 |
score?: -1 | 0 | 1;
|
13 |
files?: string[]; // can contain either the hash of the file or the b64 encoded image data on the client side when uploading
|
14 |
interrupted?: boolean;
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
};
|
|
|
3 |
import type { WebSearch } from "./WebSearch";
|
4 |
|
5 |
export type Message = Partial<Timestamps> & {
|
6 |
+
from: "user" | "assistant" | "system";
|
7 |
id: ReturnType<typeof crypto.randomUUID>;
|
8 |
content: string;
|
9 |
updates?: MessageUpdate[];
|
|
|
12 |
score?: -1 | 0 | 1;
|
13 |
files?: string[]; // can contain either the hash of the file or the b64 encoded image data on the client side when uploading
|
14 |
interrupted?: boolean;
|
15 |
+
|
16 |
+
// needed for conversation trees
|
17 |
+
ancestors?: Message["id"][];
|
18 |
+
|
19 |
+
// goes one level deep
|
20 |
+
children?: Message["id"][];
|
21 |
};
|
@@ -1,17 +1,17 @@
|
|
1 |
-
import type {
|
2 |
-
import type { Message } from "./Message";
|
3 |
-
import type { Timestamps } from "./Timestamps";
|
4 |
|
5 |
-
export
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
_id: string;
|
7 |
-
|
8 |
hash: string;
|
9 |
-
|
10 |
-
model: string;
|
11 |
-
embeddingModel: string;
|
12 |
-
|
13 |
-
title: string;
|
14 |
-
messages: Message[];
|
15 |
-
preprompt?: string;
|
16 |
-
assistantId?: Assistant["_id"];
|
17 |
-
}
|
|
|
1 |
+
import type { Conversation } from "./Conversation";
|
|
|
|
|
2 |
|
3 |
+
export type SharedConversation = Pick<
|
4 |
+
Conversation,
|
5 |
+
| "model"
|
6 |
+
| "embeddingModel"
|
7 |
+
| "title"
|
8 |
+
| "rootMessageId"
|
9 |
+
| "messages"
|
10 |
+
| "preprompt"
|
11 |
+
| "assistantId"
|
12 |
+
| "createdAt"
|
13 |
+
| "updatedAt"
|
14 |
+
> & {
|
15 |
_id: string;
|
|
|
16 |
hash: string;
|
17 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { collections } from "$lib/server/database";
|
2 |
+
import { ObjectId } from "mongodb";
|
3 |
+
import { describe, expect, it } from "vitest";
|
4 |
+
|
5 |
+
import { insertLegacyConversation, insertSideBranchesConversation } from "./treeHelpers.spec";
|
6 |
+
import { addChildren } from "./addChildren";
|
7 |
+
import type { Message } from "$lib/types/Message";
|
8 |
+
|
9 |
+
const newMessage: Omit<Message, "id"> = {
|
10 |
+
content: "new message",
|
11 |
+
from: "user",
|
12 |
+
};
|
13 |
+
|
14 |
+
Object.freeze(newMessage);
|
15 |
+
|
16 |
+
describe("addChildren", async () => {
|
17 |
+
it("should let you append on legacy conversations", async () => {
|
18 |
+
const convId = await insertLegacyConversation();
|
19 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
20 |
+
if (!conv) throw new Error("Conversation not found");
|
21 |
+
|
22 |
+
const convLength = conv.messages.length;
|
23 |
+
|
24 |
+
addChildren(conv, newMessage, conv.messages[conv.messages.length - 1].id);
|
25 |
+
expect(conv.messages.length).toEqual(convLength + 1);
|
26 |
+
});
|
27 |
+
it("should not let you create branches on legacy conversations", async () => {
|
28 |
+
const convId = await insertLegacyConversation();
|
29 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
30 |
+
if (!conv) throw new Error("Conversation not found");
|
31 |
+
|
32 |
+
expect(() => addChildren(conv, newMessage, conv.messages[0].id)).toThrow();
|
33 |
+
});
|
34 |
+
it("should not let you create a message that already exists", async () => {
|
35 |
+
const convId = await insertLegacyConversation();
|
36 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
37 |
+
if (!conv) throw new Error("Conversation not found");
|
38 |
+
|
39 |
+
const messageThatAlreadyExists: Message = {
|
40 |
+
id: conv.messages[0].id,
|
41 |
+
content: "new message",
|
42 |
+
from: "user",
|
43 |
+
};
|
44 |
+
|
45 |
+
expect(() => addChildren(conv, messageThatAlreadyExists, conv.messages[0].id)).toThrow();
|
46 |
+
});
|
47 |
+
it("should let you create branches on conversations with subtrees", async () => {
|
48 |
+
const convId = await insertSideBranchesConversation();
|
49 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
50 |
+
if (!conv) throw new Error("Conversation not found");
|
51 |
+
|
52 |
+
const nChildren = conv.messages[0].children?.length;
|
53 |
+
if (!nChildren) throw new Error("No children found");
|
54 |
+
addChildren(conv, newMessage, conv.messages[0].id);
|
55 |
+
expect(conv.messages[0].children?.length).toEqual(nChildren + 1);
|
56 |
+
});
|
57 |
+
|
58 |
+
it("should let you create a new leaf", async () => {
|
59 |
+
const convId = await insertSideBranchesConversation();
|
60 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
61 |
+
if (!conv) throw new Error("Conversation not found");
|
62 |
+
|
63 |
+
const parentId = conv.messages[conv.messages.length - 1].id;
|
64 |
+
const nChildren = conv.messages[conv.messages.length - 1].children?.length;
|
65 |
+
|
66 |
+
if (nChildren === undefined) throw new Error("No children found");
|
67 |
+
expect(nChildren).toEqual(0);
|
68 |
+
|
69 |
+
addChildren(conv, newMessage, parentId);
|
70 |
+
expect(conv.messages[conv.messages.length - 2].children?.length).toEqual(nChildren + 1);
|
71 |
+
});
|
72 |
+
|
73 |
+
it("should let you append to an empty conversation without specifying a parentId", async () => {
|
74 |
+
const conv = {
|
75 |
+
_id: new ObjectId(),
|
76 |
+
rootMessageId: undefined,
|
77 |
+
messages: [] as Message[],
|
78 |
+
};
|
79 |
+
|
80 |
+
addChildren(conv, newMessage);
|
81 |
+
expect(conv.messages.length).toEqual(1);
|
82 |
+
expect(conv.rootMessageId).toEqual(conv.messages[0].id);
|
83 |
+
});
|
84 |
+
|
85 |
+
it("should throw if you don't specify a parentId in a conversation with messages", async () => {
|
86 |
+
const convId = await insertLegacyConversation();
|
87 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
88 |
+
if (!conv) throw new Error("Conversation not found");
|
89 |
+
|
90 |
+
expect(() => addChildren(conv, newMessage)).toThrow();
|
91 |
+
});
|
92 |
+
|
93 |
+
it("should return the id of the new message", async () => {
|
94 |
+
const convId = await insertLegacyConversation();
|
95 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
96 |
+
if (!conv) throw new Error("Conversation not found");
|
97 |
+
|
98 |
+
expect(addChildren(conv, newMessage, conv.messages[conv.messages.length - 1].id)).toEqual(
|
99 |
+
conv.messages[conv.messages.length - 1].id
|
100 |
+
);
|
101 |
+
});
|
102 |
+
});
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type { Conversation } from "$lib/types/Conversation";
|
2 |
+
import type { Message } from "$lib/types/Message";
|
3 |
+
|
4 |
+
export function addChildren(
|
5 |
+
conv: Pick<Conversation, "messages" | "rootMessageId">,
|
6 |
+
message: Omit<Message, "id">,
|
7 |
+
parentId?: Message["id"]
|
8 |
+
): Message["id"] {
|
9 |
+
// if this is the first message we just push it
|
10 |
+
if (conv.messages.length === 0) {
|
11 |
+
const messageId = crypto.randomUUID();
|
12 |
+
conv.rootMessageId = messageId;
|
13 |
+
conv.messages.push({
|
14 |
+
...message,
|
15 |
+
ancestors: [],
|
16 |
+
id: messageId,
|
17 |
+
});
|
18 |
+
return messageId;
|
19 |
+
}
|
20 |
+
|
21 |
+
if (!parentId) {
|
22 |
+
throw new Error("You need to specify a parentId if this is not the first message");
|
23 |
+
}
|
24 |
+
|
25 |
+
const messageId = crypto.randomUUID();
|
26 |
+
if (!conv.rootMessageId) {
|
27 |
+
// if there is no parentId we just push the message
|
28 |
+
if (!!parentId && parentId !== conv.messages[conv.messages.length - 1].id) {
|
29 |
+
throw new Error("This is a legacy conversation, you can only append to the last message");
|
30 |
+
}
|
31 |
+
conv.messages.push({ ...message, id: messageId });
|
32 |
+
return messageId;
|
33 |
+
}
|
34 |
+
|
35 |
+
const ancestors = [...(conv.messages.find((m) => m.id === parentId)?.ancestors ?? []), parentId];
|
36 |
+
conv.messages.push({
|
37 |
+
...message,
|
38 |
+
ancestors,
|
39 |
+
id: messageId,
|
40 |
+
children: [],
|
41 |
+
});
|
42 |
+
|
43 |
+
const parent = conv.messages.find((m) => m.id === parentId);
|
44 |
+
|
45 |
+
if (parent) {
|
46 |
+
if (parent.children) {
|
47 |
+
parent.children.push(messageId);
|
48 |
+
} else parent.children = [messageId];
|
49 |
+
}
|
50 |
+
|
51 |
+
return messageId;
|
52 |
+
}
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { collections } from "$lib/server/database";
|
2 |
+
import { ObjectId } from "mongodb";
|
3 |
+
import { describe, expect, it } from "vitest";
|
4 |
+
|
5 |
+
import { insertLegacyConversation, insertSideBranchesConversation } from "./treeHelpers.spec";
|
6 |
+
import type { Message } from "$lib/types/Message";
|
7 |
+
import { addSibling } from "./addSibling";
|
8 |
+
|
9 |
+
const newMessage: Omit<Message, "id"> = {
|
10 |
+
content: "new message",
|
11 |
+
from: "user",
|
12 |
+
};
|
13 |
+
|
14 |
+
Object.freeze(newMessage);
|
15 |
+
|
16 |
+
describe("addSibling", async () => {
|
17 |
+
it("should fail on empty conversations", () => {
|
18 |
+
const conv = {
|
19 |
+
_id: new ObjectId(),
|
20 |
+
rootMessageId: undefined,
|
21 |
+
messages: [],
|
22 |
+
};
|
23 |
+
|
24 |
+
expect(() => addSibling(conv, newMessage, "not-a-real-id-test")).toThrow(
|
25 |
+
"Cannot add a sibling to an empty conversation"
|
26 |
+
);
|
27 |
+
});
|
28 |
+
|
29 |
+
it("should fail on legacy conversations", async () => {
|
30 |
+
const convId = await insertLegacyConversation();
|
31 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
32 |
+
if (!conv) throw new Error("Conversation not found");
|
33 |
+
|
34 |
+
expect(() => addSibling(conv, newMessage, conv.messages[0].id)).toThrow(
|
35 |
+
"Cannot add a sibling to a legacy conversation"
|
36 |
+
);
|
37 |
+
});
|
38 |
+
|
39 |
+
it("should fail if the sibling message doesn't exist", async () => {
|
40 |
+
const convId = await insertSideBranchesConversation();
|
41 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
42 |
+
if (!conv) throw new Error("Conversation not found");
|
43 |
+
|
44 |
+
expect(() => addSibling(conv, newMessage, "not-a-real-id-test")).toThrow(
|
45 |
+
"The sibling message doesn't exist"
|
46 |
+
);
|
47 |
+
});
|
48 |
+
|
49 |
+
// TODO: This behaviour should be fixed, we do not need to fail on the root message.
|
50 |
+
it("should fail if the sibling message is the root message", async () => {
|
51 |
+
const convId = await insertSideBranchesConversation();
|
52 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
53 |
+
if (!conv) throw new Error("Conversation not found");
|
54 |
+
if (!conv.rootMessageId) throw new Error("Root message not found");
|
55 |
+
|
56 |
+
expect(() => addSibling(conv, newMessage, conv.rootMessageId as Message["id"])).toThrow(
|
57 |
+
"The sibling message is the root message, therefore we can't add a sibling"
|
58 |
+
);
|
59 |
+
});
|
60 |
+
|
61 |
+
it("should add a sibling to a message", async () => {
|
62 |
+
const convId = await insertSideBranchesConversation();
|
63 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
64 |
+
if (!conv) throw new Error("Conversation not found");
|
65 |
+
|
66 |
+
// add sibling and check children count for parnets
|
67 |
+
|
68 |
+
const nChildren = conv.messages[1].children?.length;
|
69 |
+
const siblingId = addSibling(conv, newMessage, conv.messages[2].id);
|
70 |
+
const nChildrenNew = conv.messages[1].children?.length;
|
71 |
+
|
72 |
+
if (!nChildren) throw new Error("No children found");
|
73 |
+
|
74 |
+
expect(nChildrenNew).toBe(nChildren + 1);
|
75 |
+
|
76 |
+
// make sure siblings have the same ancestors
|
77 |
+
const sibling = conv.messages.find((m) => m.id === siblingId);
|
78 |
+
expect(sibling?.ancestors).toEqual(conv.messages[2].ancestors);
|
79 |
+
});
|
80 |
+
});
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type { Conversation } from "$lib/types/Conversation";
|
2 |
+
import type { Message } from "$lib/types/Message";
|
3 |
+
|
4 |
+
export function addSibling(
|
5 |
+
conv: Pick<Conversation, "messages" | "rootMessageId">,
|
6 |
+
message: Omit<Message, "id">,
|
7 |
+
siblingId: Message["id"]
|
8 |
+
): Message["id"] {
|
9 |
+
if (conv.messages.length === 0) {
|
10 |
+
throw new Error("Cannot add a sibling to an empty conversation");
|
11 |
+
}
|
12 |
+
if (!conv.rootMessageId) {
|
13 |
+
throw new Error("Cannot add a sibling to a legacy conversation");
|
14 |
+
}
|
15 |
+
|
16 |
+
const sibling = conv.messages.find((m) => m.id === siblingId);
|
17 |
+
|
18 |
+
if (!sibling) {
|
19 |
+
throw new Error("The sibling message doesn't exist");
|
20 |
+
}
|
21 |
+
|
22 |
+
if (!sibling.ancestors || sibling.ancestors?.length === 0) {
|
23 |
+
throw new Error("The sibling message is the root message, therefore we can't add a sibling");
|
24 |
+
}
|
25 |
+
|
26 |
+
const messageId = crypto.randomUUID();
|
27 |
+
|
28 |
+
conv.messages.push({
|
29 |
+
...message,
|
30 |
+
id: messageId,
|
31 |
+
ancestors: sibling.ancestors,
|
32 |
+
children: [],
|
33 |
+
});
|
34 |
+
|
35 |
+
const nearestAncestorId = sibling.ancestors[sibling.ancestors.length - 1];
|
36 |
+
const nearestAncestor = conv.messages.find((m) => m.id === nearestAncestorId);
|
37 |
+
|
38 |
+
if (nearestAncestor) {
|
39 |
+
if (nearestAncestor.children) {
|
40 |
+
nearestAncestor.children.push(messageId);
|
41 |
+
} else nearestAncestor.children = [messageId];
|
42 |
+
}
|
43 |
+
|
44 |
+
return messageId;
|
45 |
+
}
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { collections } from "$lib/server/database";
|
2 |
+
import { ObjectId } from "mongodb";
|
3 |
+
import { describe, expect, it } from "vitest";
|
4 |
+
|
5 |
+
import {
|
6 |
+
insertLegacyConversation,
|
7 |
+
insertLinearBranchConversation,
|
8 |
+
insertSideBranchesConversation,
|
9 |
+
} from "./treeHelpers.spec";
|
10 |
+
import { buildSubtree } from "./buildSubtree";
|
11 |
+
|
12 |
+
describe("buildSubtree", () => {
|
13 |
+
it("a subtree in a legacy conversation should be just a slice", async () => {
|
14 |
+
const convId = await insertLegacyConversation();
|
15 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
16 |
+
if (!conv) throw new Error("Conversation not found");
|
17 |
+
|
18 |
+
// check middle
|
19 |
+
const id = conv.messages[2].id;
|
20 |
+
const subtree = buildSubtree(conv, id);
|
21 |
+
expect(subtree).toEqual(conv.messages.slice(0, 3));
|
22 |
+
|
23 |
+
// check zero
|
24 |
+
const id2 = conv.messages[0].id;
|
25 |
+
const subtree2 = buildSubtree(conv, id2);
|
26 |
+
expect(subtree2).toEqual(conv.messages.slice(0, 1));
|
27 |
+
|
28 |
+
//check full length
|
29 |
+
const id3 = conv.messages[conv.messages.length - 1].id;
|
30 |
+
const subtree3 = buildSubtree(conv, id3);
|
31 |
+
expect(subtree3).toEqual(conv.messages);
|
32 |
+
});
|
33 |
+
|
34 |
+
it("a subtree in a linear branch conversation should be the ancestors and the message", async () => {
|
35 |
+
const convId = await insertLinearBranchConversation();
|
36 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
37 |
+
if (!conv) throw new Error("Conversation not found");
|
38 |
+
|
39 |
+
// check middle
|
40 |
+
const id = conv.messages[1].id;
|
41 |
+
const subtree = buildSubtree(conv, id);
|
42 |
+
expect(subtree).toEqual([conv.messages[0], conv.messages[1]]);
|
43 |
+
|
44 |
+
// check zero
|
45 |
+
const id2 = conv.messages[0].id;
|
46 |
+
const subtree2 = buildSubtree(conv, id2);
|
47 |
+
expect(subtree2).toEqual([conv.messages[0]]);
|
48 |
+
|
49 |
+
//check full length
|
50 |
+
const id3 = conv.messages[conv.messages.length - 1].id;
|
51 |
+
const subtree3 = buildSubtree(conv, id3);
|
52 |
+
expect(subtree3).toEqual(conv.messages);
|
53 |
+
});
|
54 |
+
|
55 |
+
it("should throw an error if the message is not found", async () => {
|
56 |
+
const convId = await insertLinearBranchConversation();
|
57 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
58 |
+
if (!conv) throw new Error("Conversation not found");
|
59 |
+
|
60 |
+
const id = "not-a-real-id-test";
|
61 |
+
|
62 |
+
expect(() => buildSubtree(conv, id)).toThrow("Message not found");
|
63 |
+
});
|
64 |
+
|
65 |
+
it("should throw an error if the ancestor is not found", async () => {
|
66 |
+
const convId = await insertLinearBranchConversation();
|
67 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
68 |
+
if (!conv) throw new Error("Conversation not found");
|
69 |
+
|
70 |
+
const id = "1-1-1-1-2";
|
71 |
+
|
72 |
+
conv.messages[1].ancestors = ["not-a-real-id-test"];
|
73 |
+
|
74 |
+
expect(() => buildSubtree(conv, id)).toThrow("Ancestor not found");
|
75 |
+
});
|
76 |
+
|
77 |
+
it("should work on empty conversations", () => {
|
78 |
+
const conv = {
|
79 |
+
_id: new ObjectId(),
|
80 |
+
rootMessageId: undefined,
|
81 |
+
messages: [],
|
82 |
+
};
|
83 |
+
|
84 |
+
const subtree = buildSubtree(conv, "not-a-real-id-test");
|
85 |
+
expect(subtree).toEqual([]);
|
86 |
+
});
|
87 |
+
|
88 |
+
it("should work for conversation with subtrees", async () => {
|
89 |
+
const convId = await insertSideBranchesConversation();
|
90 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
91 |
+
if (!conv) throw new Error("Conversation not found");
|
92 |
+
|
93 |
+
const subtree = buildSubtree(conv, "1-1-1-1-2");
|
94 |
+
expect(subtree).toEqual([conv.messages[0], conv.messages[1]]);
|
95 |
+
|
96 |
+
const subtree2 = buildSubtree(conv, "1-1-1-1-4");
|
97 |
+
expect(subtree2).toEqual([
|
98 |
+
conv.messages[0],
|
99 |
+
conv.messages[1],
|
100 |
+
conv.messages[2],
|
101 |
+
conv.messages[3],
|
102 |
+
]);
|
103 |
+
|
104 |
+
const subtree3 = buildSubtree(conv, "1-1-1-1-6");
|
105 |
+
expect(subtree3).toEqual([conv.messages[0], conv.messages[4], conv.messages[5]]);
|
106 |
+
|
107 |
+
const subtree4 = buildSubtree(conv, "1-1-1-1-7");
|
108 |
+
expect(subtree4).toEqual([conv.messages[0], conv.messages[4], conv.messages[6]]);
|
109 |
+
});
|
110 |
+
});
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type { Conversation } from "$lib/types/Conversation";
|
2 |
+
import type { Message } from "$lib/types/Message";
|
3 |
+
|
4 |
+
export function buildSubtree(
|
5 |
+
conv: Pick<Conversation, "messages" | "rootMessageId">,
|
6 |
+
id: Message["id"]
|
7 |
+
): Message[] {
|
8 |
+
if (!conv.rootMessageId) {
|
9 |
+
if (conv.messages.length === 0) return [];
|
10 |
+
// legacy conversation slice up to id
|
11 |
+
const index = conv.messages.findIndex((m) => m.id === id);
|
12 |
+
if (index === -1) throw new Error("Message not found");
|
13 |
+
return conv.messages.slice(0, index + 1);
|
14 |
+
} else {
|
15 |
+
// find the message with the right id then create the ancestor tree
|
16 |
+
const message = conv.messages.find((m) => m.id === id);
|
17 |
+
if (!message) throw new Error("Message not found");
|
18 |
+
|
19 |
+
return [
|
20 |
+
...(message.ancestors?.map((ancestorId) => {
|
21 |
+
const ancestor = conv.messages.find((m) => m.id === ancestorId);
|
22 |
+
if (!ancestor) throw new Error("Ancestor not found");
|
23 |
+
return ancestor;
|
24 |
+
}) ?? []),
|
25 |
+
message,
|
26 |
+
];
|
27 |
+
}
|
28 |
+
}
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { collections } from "$lib/server/database";
|
2 |
+
import { ObjectId } from "mongodb";
|
3 |
+
import { describe, expect, it } from "vitest";
|
4 |
+
|
5 |
+
import { convertLegacyConversation } from "./convertLegacyConversation";
|
6 |
+
import { insertLegacyConversation } from "./treeHelpers.spec";
|
7 |
+
|
8 |
+
describe("convertLegacyConversation", () => {
|
9 |
+
it("should convert a legacy conversation", async () => {
|
10 |
+
const convId = await insertLegacyConversation();
|
11 |
+
const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) });
|
12 |
+
if (!conv) throw new Error("Conversation not found");
|
13 |
+
|
14 |
+
const newConv = convertLegacyConversation(conv);
|
15 |
+
|
16 |
+
expect(newConv.rootMessageId).toBe(newConv.messages[0].id);
|
17 |
+
expect(newConv.messages[0].ancestors).toEqual([]);
|
18 |
+
expect(newConv.messages[1].ancestors).toEqual([newConv.messages[0].id]);
|
19 |
+
expect(newConv.messages[0].children).toEqual([newConv.messages[1].id]);
|
20 |
+
});
|
21 |
+
it("should work on empty conversations", async () => {
|
22 |
+
const conv = {
|
23 |
+
_id: new ObjectId(),
|
24 |
+
rootMessageId: undefined,
|
25 |
+
messages: [],
|
26 |
+
};
|
27 |
+
const newConv = convertLegacyConversation(conv);
|
28 |
+
expect(newConv.rootMessageId).toBe(undefined);
|
29 |
+
expect(newConv.messages).toEqual([]);
|
30 |
+
});
|
31 |
+
});
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type { Conversation } from "$lib/types/Conversation";
|
2 |
+
import type { Message } from "$lib/types/Message";
|
3 |
+
|
4 |
+
export function convertLegacyConversation(
|
5 |
+
conv: Pick<Conversation, "messages" | "rootMessageId" | "preprompt">
|
6 |
+
): Pick<Conversation, "messages" | "rootMessageId" | "preprompt"> {
|
7 |
+
if (conv.rootMessageId) return conv; // not a legacy conversation
|
8 |
+
if (conv.messages.length === 0) return conv; // empty conversation
|
9 |
+
const messages = [
|
10 |
+
{
|
11 |
+
from: "system",
|
12 |
+
content: conv.preprompt ?? "",
|
13 |
+
createdAt: new Date(),
|
14 |
+
updatedAt: new Date(),
|
15 |
+
id: crypto.randomUUID(),
|
16 |
+
} satisfies Message,
|
17 |
+
...conv.messages,
|
18 |
+
];
|
19 |
+
|
20 |
+
const rootMessageId = messages[0].id;
|
21 |
+
|
22 |
+
const newMessages = messages.map((message, index) => {
|
23 |
+
return {
|
24 |
+
...message,
|
25 |
+
ancestors: messages.slice(0, index).map((m) => m.id),
|
26 |
+
children: index < messages.length - 1 ? [messages[index + 1].id] : [],
|
27 |
+
};
|
28 |
+
});
|
29 |
+
|
30 |
+
return {
|
31 |
+
...conv,
|
32 |
+
rootMessageId,
|
33 |
+
messages: newMessages,
|
34 |
+
};
|
35 |
+
}
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { describe, expect, it } from "vitest";
|
2 |
+
import { isMessageId } from "./isMessageId";
|
3 |
+
|
4 |
+
describe("isMessageId", () => {
|
5 |
+
it("should return true for a valid message id", () => {
|
6 |
+
expect(isMessageId(crypto.randomUUID())).toBe(true);
|
7 |
+
});
|
8 |
+
it("should return false for an invalid message id", () => {
|
9 |
+
expect(isMessageId("1-2-3-4")).toBe(false);
|
10 |
+
});
|
11 |
+
it("should return false for an empty string", () => {
|
12 |
+
expect(isMessageId("")).toBe(false);
|
13 |
+
});
|
14 |
+
});
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type { Message } from "$lib/types/Message";
|
2 |
+
|
3 |
+
export function isMessageId(id: string): id is Message["id"] {
|
4 |
+
return id.split("-").length === 5;
|
5 |
+
}
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { collections } from "$lib/server/database";
|
2 |
+
import { ObjectId } from "mongodb";
|
3 |
+
import { describe, expect, it } from "vitest";
|
4 |
+
|
5 |
+
// function used to insert conversations used for testing
|
6 |
+
|
7 |
+
export const insertLegacyConversation = async () => {
|
8 |
+
const res = await collections.conversations.insertOne({
|
9 |
+
_id: new ObjectId(),
|
10 |
+
createdAt: new Date(),
|
11 |
+
updatedAt: new Date(),
|
12 |
+
title: "legacy conversation",
|
13 |
+
model: "",
|
14 |
+
embeddingModel: "",
|
15 |
+
messages: [
|
16 |
+
{
|
17 |
+
id: "1-1-1-1-1",
|
18 |
+
from: "user",
|
19 |
+
content: "Hello, world! I am a user",
|
20 |
+
},
|
21 |
+
{
|
22 |
+
id: "1-1-1-1-2",
|
23 |
+
from: "assistant",
|
24 |
+
content: "Hello, world! I am an assistant.",
|
25 |
+
},
|
26 |
+
{
|
27 |
+
id: "1-1-1-1-3",
|
28 |
+
from: "user",
|
29 |
+
content: "Hello, world! I am a user.",
|
30 |
+
},
|
31 |
+
{
|
32 |
+
id: "1-1-1-1-4",
|
33 |
+
from: "assistant",
|
34 |
+
content: "Hello, world! I am an assistant.",
|
35 |
+
},
|
36 |
+
],
|
37 |
+
});
|
38 |
+
return res.insertedId;
|
39 |
+
};
|
40 |
+
|
41 |
+
export const insertLinearBranchConversation = async () => {
|
42 |
+
const res = await collections.conversations.insertOne({
|
43 |
+
_id: new ObjectId(),
|
44 |
+
createdAt: new Date(),
|
45 |
+
updatedAt: new Date(),
|
46 |
+
title: "linear branch conversation",
|
47 |
+
model: "",
|
48 |
+
embeddingModel: "",
|
49 |
+
|
50 |
+
rootMessageId: "1-1-1-1-1",
|
51 |
+
messages: [
|
52 |
+
{
|
53 |
+
id: "1-1-1-1-1",
|
54 |
+
from: "user",
|
55 |
+
content: "Hello, world! I am a user",
|
56 |
+
ancestors: [],
|
57 |
+
children: ["1-1-1-1-2"],
|
58 |
+
},
|
59 |
+
{
|
60 |
+
id: "1-1-1-1-2",
|
61 |
+
from: "assistant",
|
62 |
+
content: "Hello, world! I am an assistant.",
|
63 |
+
ancestors: ["1-1-1-1-1"],
|
64 |
+
children: ["1-1-1-1-3"],
|
65 |
+
},
|
66 |
+
{
|
67 |
+
id: "1-1-1-1-3",
|
68 |
+
from: "user",
|
69 |
+
content: "Hello, world! I am a user.",
|
70 |
+
ancestors: ["1-1-1-1-1", "1-1-1-1-2"],
|
71 |
+
children: ["1-1-1-1-4"],
|
72 |
+
},
|
73 |
+
{
|
74 |
+
id: "1-1-1-1-4",
|
75 |
+
from: "assistant",
|
76 |
+
content: "Hello, world! I am an assistant.",
|
77 |
+
ancestors: ["1-1-1-1-1", "1-1-1-1-2", "1-1-1-1-3"],
|
78 |
+
children: [],
|
79 |
+
},
|
80 |
+
],
|
81 |
+
});
|
82 |
+
return res.insertedId;
|
83 |
+
};
|
84 |
+
|
85 |
+
export const insertSideBranchesConversation = async () => {
|
86 |
+
const res = await collections.conversations.insertOne({
|
87 |
+
_id: new ObjectId(),
|
88 |
+
createdAt: new Date(),
|
89 |
+
updatedAt: new Date(),
|
90 |
+
title: "side branches conversation",
|
91 |
+
model: "",
|
92 |
+
embeddingModel: "",
|
93 |
+
rootMessageId: "1-1-1-1-1",
|
94 |
+
messages: [
|
95 |
+
{
|
96 |
+
id: "1-1-1-1-1",
|
97 |
+
from: "user",
|
98 |
+
content: "Hello, world, root message!",
|
99 |
+
ancestors: [],
|
100 |
+
children: ["1-1-1-1-2", "1-1-1-1-5"],
|
101 |
+
},
|
102 |
+
{
|
103 |
+
id: "1-1-1-1-2",
|
104 |
+
from: "assistant",
|
105 |
+
content: "Hello, response to root message!",
|
106 |
+
ancestors: ["1-1-1-1-1"],
|
107 |
+
children: ["1-1-1-1-3"],
|
108 |
+
},
|
109 |
+
{
|
110 |
+
id: "1-1-1-1-3",
|
111 |
+
from: "user",
|
112 |
+
content: "Hello, follow up question!",
|
113 |
+
ancestors: ["1-1-1-1-1", "1-1-1-1-2"],
|
114 |
+
children: ["1-1-1-1-4"],
|
115 |
+
},
|
116 |
+
{
|
117 |
+
id: "1-1-1-1-4",
|
118 |
+
from: "assistant",
|
119 |
+
content: "Hello, response from follow up question!",
|
120 |
+
ancestors: ["1-1-1-1-1", "1-1-1-1-2", "1-1-1-1-3"],
|
121 |
+
children: [],
|
122 |
+
},
|
123 |
+
{
|
124 |
+
id: "1-1-1-1-5",
|
125 |
+
from: "assistant",
|
126 |
+
content: "Hello, alternative assistant answer!",
|
127 |
+
ancestors: ["1-1-1-1-1"],
|
128 |
+
children: ["1-1-1-1-6", "1-1-1-1-7"],
|
129 |
+
},
|
130 |
+
{
|
131 |
+
id: "1-1-1-1-6",
|
132 |
+
from: "user",
|
133 |
+
content: "Hello, follow up question to alternative answer!",
|
134 |
+
ancestors: ["1-1-1-1-1", "1-1-1-1-5"],
|
135 |
+
children: [],
|
136 |
+
},
|
137 |
+
{
|
138 |
+
id: "1-1-1-1-7",
|
139 |
+
from: "user",
|
140 |
+
content: "Hello, alternative follow up question to alternative answer!",
|
141 |
+
ancestors: ["1-1-1-1-1", "1-1-1-1-5"],
|
142 |
+
children: [],
|
143 |
+
},
|
144 |
+
],
|
145 |
+
});
|
146 |
+
return res.insertedId;
|
147 |
+
};
|
148 |
+
|
149 |
+
describe("inserting conversations", () => {
|
150 |
+
it("should insert a legacy conversation", async () => {
|
151 |
+
const id = await insertLegacyConversation();
|
152 |
+
expect(id).toBeDefined();
|
153 |
+
});
|
154 |
+
|
155 |
+
it("should insert a linear branch conversation", async () => {
|
156 |
+
const id = await insertLinearBranchConversation();
|
157 |
+
expect(id).toBeDefined();
|
158 |
+
});
|
159 |
+
|
160 |
+
it("should insert a side branches conversation", async () => {
|
161 |
+
const id = await insertSideBranchesConversation();
|
162 |
+
expect(id).toBeDefined();
|
163 |
+
});
|
164 |
+
});
|
@@ -12,7 +12,6 @@ export const POST: RequestHandler = async ({ locals, request }) => {
|
|
12 |
const body = await request.text();
|
13 |
|
14 |
let title = "";
|
15 |
-
let messages: Message[] = [];
|
16 |
|
17 |
const values = z
|
18 |
.object({
|
@@ -23,6 +22,19 @@ export const POST: RequestHandler = async ({ locals, request }) => {
|
|
23 |
})
|
24 |
.parse(JSON.parse(body));
|
25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
let embeddingModel: string;
|
27 |
|
28 |
if (values.fromShare) {
|
@@ -36,6 +48,7 @@ export const POST: RequestHandler = async ({ locals, request }) => {
|
|
36 |
|
37 |
title = conversation.title;
|
38 |
messages = conversation.messages;
|
|
|
39 |
values.model = conversation.model;
|
40 |
values.preprompt = conversation.preprompt;
|
41 |
values.assistantId = conversation.assistantId?.toString();
|
@@ -69,6 +82,7 @@ export const POST: RequestHandler = async ({ locals, request }) => {
|
|
69 |
const res = await collections.conversations.insertOne({
|
70 |
_id: new ObjectId(),
|
71 |
title: title || "New Chat",
|
|
|
72 |
messages,
|
73 |
model: values.model,
|
74 |
preprompt: preprompt === model?.preprompt ? model?.preprompt : preprompt,
|
|
|
12 |
const body = await request.text();
|
13 |
|
14 |
let title = "";
|
|
|
15 |
|
16 |
const values = z
|
17 |
.object({
|
|
|
22 |
})
|
23 |
.parse(JSON.parse(body));
|
24 |
|
25 |
+
let messages: Message[] = [
|
26 |
+
{
|
27 |
+
id: crypto.randomUUID(),
|
28 |
+
from: "system",
|
29 |
+
content: values.preprompt ?? "",
|
30 |
+
createdAt: new Date(),
|
31 |
+
updatedAt: new Date(),
|
32 |
+
children: [],
|
33 |
+
ancestors: [],
|
34 |
+
},
|
35 |
+
];
|
36 |
+
|
37 |
+
let rootMessageId: Message["id"] = messages[0].id;
|
38 |
let embeddingModel: string;
|
39 |
|
40 |
if (values.fromShare) {
|
|
|
48 |
|
49 |
title = conversation.title;
|
50 |
messages = conversation.messages;
|
51 |
+
rootMessageId = conversation.rootMessageId ?? rootMessageId;
|
52 |
values.model = conversation.model;
|
53 |
values.preprompt = conversation.preprompt;
|
54 |
values.assistantId = conversation.assistantId?.toString();
|
|
|
82 |
const res = await collections.conversations.insertOne({
|
83 |
_id: new ObjectId(),
|
84 |
title: title || "New Chat",
|
85 |
+
rootMessageId,
|
86 |
messages,
|
87 |
model: values.model,
|
88 |
preprompt: preprompt === model?.preprompt ? model?.preprompt : preprompt,
|
@@ -3,6 +3,7 @@ import { ObjectId } from "mongodb";
|
|
3 |
import { error } from "@sveltejs/kit";
|
4 |
import { authCondition } from "$lib/server/auth";
|
5 |
import { UrlDependency } from "$lib/types/UrlDependency";
|
|
|
6 |
|
7 |
export const load = async ({ params, depends, locals }) => {
|
8 |
let conversation;
|
@@ -45,16 +46,19 @@ export const load = async ({ params, depends, locals }) => {
|
|
45 |
}
|
46 |
}
|
47 |
|
|
|
|
|
48 |
return {
|
49 |
-
messages:
|
50 |
-
title:
|
51 |
-
model:
|
52 |
-
preprompt:
|
53 |
-
|
|
|
54 |
? JSON.parse(
|
55 |
JSON.stringify(
|
56 |
await collections.assistants.findOne({
|
57 |
-
_id: new ObjectId(
|
58 |
})
|
59 |
)
|
60 |
)
|
|
|
3 |
import { error } from "@sveltejs/kit";
|
4 |
import { authCondition } from "$lib/server/auth";
|
5 |
import { UrlDependency } from "$lib/types/UrlDependency";
|
6 |
+
import { convertLegacyConversation } from "$lib/utils/tree/convertLegacyConversation.js";
|
7 |
|
8 |
export const load = async ({ params, depends, locals }) => {
|
9 |
let conversation;
|
|
|
46 |
}
|
47 |
}
|
48 |
|
49 |
+
const convertedConv = { ...conversation, ...convertLegacyConversation(conversation) };
|
50 |
+
|
51 |
return {
|
52 |
+
messages: convertedConv.messages,
|
53 |
+
title: convertedConv.title,
|
54 |
+
model: convertedConv.model,
|
55 |
+
preprompt: convertedConv.preprompt,
|
56 |
+
rootMessageId: convertedConv.rootMessageId,
|
57 |
+
assistant: convertedConv.assistantId
|
58 |
? JSON.parse(
|
59 |
JSON.stringify(
|
60 |
await collections.assistants.findOne({
|
61 |
+
_id: new ObjectId(convertedConv.assistantId),
|
62 |
})
|
63 |
)
|
64 |
)
|
@@ -9,20 +9,21 @@
|
|
9 |
import { shareConversation } from "$lib/shareConversation";
|
10 |
import { UrlDependency } from "$lib/types/UrlDependency";
|
11 |
import { ERROR_MESSAGES, error } from "$lib/stores/errors";
|
12 |
-
import { randomUUID } from "$lib/utils/randomUuid";
|
13 |
import { findCurrentModel } from "$lib/utils/models";
|
14 |
import { webSearchParameters } from "$lib/stores/webSearchParameters";
|
15 |
import type { Message } from "$lib/types/Message";
|
16 |
-
import type { MessageUpdate
|
17 |
import titleUpdate from "$lib/stores/titleUpdate";
|
18 |
import file2base64 from "$lib/utils/file2base64";
|
|
|
|
|
|
|
|
|
19 |
export let data;
|
20 |
|
21 |
let messages = data.messages;
|
22 |
let lastLoadedMessages = data.messages;
|
23 |
|
24 |
-
let webSearchMessages: WebSearchUpdate[] = [];
|
25 |
-
|
26 |
// Since we modify the messages array locally, we don't want to reset it if an old version is passed
|
27 |
$: if (data.messages !== lastLoadedMessages) {
|
28 |
messages = data.messages;
|
@@ -66,12 +67,12 @@
|
|
66 |
// this function is used to send new message to the backends
|
67 |
async function writeMessage({
|
68 |
prompt,
|
69 |
-
messageId =
|
70 |
isRetry = false,
|
71 |
isContinue = false,
|
72 |
}: {
|
73 |
prompt?: string;
|
74 |
-
messageId?: ReturnType<typeof randomUUID>;
|
75 |
isRetry?: boolean;
|
76 |
isContinue?: boolean;
|
77 |
}): Promise<void> {
|
@@ -80,25 +81,7 @@
|
|
80 |
loading = true;
|
81 |
pending = true;
|
82 |
|
83 |
-
// first we check if the messageId already exists, indicating a retry
|
84 |
-
|
85 |
-
let msgIndex = messages.findIndex((msg) => msg.id === messageId);
|
86 |
-
|
87 |
-
if (msgIndex === -1) {
|
88 |
-
msgIndex = messages.length - 1;
|
89 |
-
}
|
90 |
-
if (isRetry && messages[msgIndex].from === "assistant") {
|
91 |
-
throw new Error("Trying to retry a message that is not from user");
|
92 |
-
}
|
93 |
-
|
94 |
-
if (isContinue && messages[msgIndex].from === "user") {
|
95 |
-
throw new Error("Trying to continue a message that is not from assistant");
|
96 |
-
}
|
97 |
-
|
98 |
-
// const isNewMessage = !isRetry && !isContinue;
|
99 |
-
|
100 |
const module = await import("browser-image-resizer");
|
101 |
-
|
102 |
// currently, only IDEFICS is supported by TGI
|
103 |
// the size of images is hardcoded to 224x224 in TGI
|
104 |
// this will need to be configurable when support for more models is added
|
@@ -114,35 +97,101 @@
|
|
114 |
})
|
115 |
);
|
116 |
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
121 |
{
|
122 |
-
|
123 |
-
|
124 |
-
id: messageId,
|
125 |
-
files: messages[msgIndex].files,
|
126 |
},
|
127 |
-
];
|
128 |
-
} else if (!isContinue) {
|
129 |
-
// or add a new message if its not a continue request
|
130 |
-
if (!prompt) {
|
131 |
-
throw new Error("Prompt is undefined");
|
132 |
-
}
|
133 |
-
messages = [
|
134 |
-
...messages,
|
135 |
{
|
136 |
from: "user",
|
137 |
content: prompt ?? "",
|
138 |
-
id: messageId,
|
139 |
files: resizedImages,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
140 |
},
|
141 |
-
|
|
|
142 |
}
|
143 |
|
144 |
-
|
|
|
145 |
|
|
|
|
|
|
|
146 |
// disable websearch if assistant is present
|
147 |
const hasAssistant = !!$page.data.assistant;
|
148 |
|
@@ -221,25 +270,16 @@
|
|
221 |
invalidate(UrlDependency.Conversation);
|
222 |
} else if (update.type === "stream") {
|
223 |
pending = false;
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
if (lastMessage.from !== "assistant") {
|
228 |
-
messages = [
|
229 |
-
...messages,
|
230 |
-
{ from: "assistant", id: randomUUID(), content: update.token },
|
231 |
-
];
|
232 |
-
} else {
|
233 |
-
lastMessage.content += update.token;
|
234 |
-
messages = [...messages];
|
235 |
-
}
|
236 |
} else if (update.type === "webSearch") {
|
237 |
-
|
|
|
238 |
} else if (update.type === "status") {
|
239 |
if (update.status === "title" && update.message) {
|
240 |
-
const
|
241 |
-
if (
|
242 |
-
|
243 |
|
244 |
$titleUpdate = {
|
245 |
title: update.message,
|
@@ -265,11 +305,7 @@
|
|
265 |
});
|
266 |
}
|
267 |
|
268 |
-
|
269 |
-
|
270 |
-
const lastMessage = messages[messages.length - 1];
|
271 |
-
lastMessage.updates = messageUpdates;
|
272 |
-
|
273 |
await invalidate(UrlDependency.ConversationList);
|
274 |
} catch (err) {
|
275 |
if (err instanceof Error && err.message.includes("overloaded")) {
|
@@ -285,6 +321,7 @@
|
|
285 |
} finally {
|
286 |
loading = false;
|
287 |
pending = false;
|
|
|
288 |
}
|
289 |
}
|
290 |
|
@@ -336,7 +373,7 @@
|
|
336 |
}
|
337 |
}
|
338 |
|
339 |
-
async function onRetry(event: CustomEvent<{ id: Message["id"]; content
|
340 |
if (!data.shared) {
|
341 |
await writeMessage({
|
342 |
prompt: event.detail.content,
|
@@ -363,11 +400,26 @@
|
|
363 |
async function onContinue(event: CustomEvent<{ id: Message["id"] }>) {
|
364 |
if (!data.shared) {
|
365 |
writeMessage({ messageId: event.detail.id, isContinue: true });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
366 |
}
|
367 |
}
|
368 |
|
369 |
-
$: $page.params.id, (($isAborted = true), (loading = false));
|
370 |
$: title = data.conversations.find((conv) => conv.id === $page.params.id)?.title ?? data.title;
|
|
|
|
|
371 |
</script>
|
372 |
|
373 |
<svelte:head>
|
@@ -386,7 +438,6 @@
|
|
386 |
{messages}
|
387 |
shared={data.shared}
|
388 |
preprompt={data.preprompt}
|
389 |
-
bind:webSearchMessages
|
390 |
bind:files
|
391 |
on:message={onMessage}
|
392 |
on:retry={onRetry}
|
|
|
9 |
import { shareConversation } from "$lib/shareConversation";
|
10 |
import { UrlDependency } from "$lib/types/UrlDependency";
|
11 |
import { ERROR_MESSAGES, error } from "$lib/stores/errors";
|
|
|
12 |
import { findCurrentModel } from "$lib/utils/models";
|
13 |
import { webSearchParameters } from "$lib/stores/webSearchParameters";
|
14 |
import type { Message } from "$lib/types/Message";
|
15 |
+
import type { MessageUpdate } from "$lib/types/MessageUpdate";
|
16 |
import titleUpdate from "$lib/stores/titleUpdate";
|
17 |
import file2base64 from "$lib/utils/file2base64";
|
18 |
+
import { addChildren } from "$lib/utils/tree/addChildren";
|
19 |
+
import { addSibling } from "$lib/utils/tree/addSibling";
|
20 |
+
import { createConvTreeStore } from "$lib/stores/convTree";
|
21 |
+
|
22 |
export let data;
|
23 |
|
24 |
let messages = data.messages;
|
25 |
let lastLoadedMessages = data.messages;
|
26 |
|
|
|
|
|
27 |
// Since we modify the messages array locally, we don't want to reset it if an old version is passed
|
28 |
$: if (data.messages !== lastLoadedMessages) {
|
29 |
messages = data.messages;
|
|
|
67 |
// this function is used to send new message to the backends
|
68 |
async function writeMessage({
|
69 |
prompt,
|
70 |
+
messageId = $convTreeStore.leaf ?? undefined,
|
71 |
isRetry = false,
|
72 |
isContinue = false,
|
73 |
}: {
|
74 |
prompt?: string;
|
75 |
+
messageId?: ReturnType<typeof crypto.randomUUID>;
|
76 |
isRetry?: boolean;
|
77 |
isContinue?: boolean;
|
78 |
}): Promise<void> {
|
|
|
81 |
loading = true;
|
82 |
pending = true;
|
83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
84 |
const module = await import("browser-image-resizer");
|
|
|
85 |
// currently, only IDEFICS is supported by TGI
|
86 |
// the size of images is hardcoded to 224x224 in TGI
|
87 |
// this will need to be configurable when support for more models is added
|
|
|
97 |
})
|
98 |
);
|
99 |
|
100 |
+
let messageToWriteToId: Message["id"] | undefined = undefined;
|
101 |
+
// used for building the prompt, subtree of the conversation that goes from the latest message to the root
|
102 |
+
|
103 |
+
if (isContinue && messageId) {
|
104 |
+
if ((messages.find((msg) => msg.id === messageId)?.children?.length ?? 0) > 0) {
|
105 |
+
$error = "Can only continue the last message";
|
106 |
+
} else {
|
107 |
+
messageToWriteToId = messageId;
|
108 |
+
}
|
109 |
+
} else if (isRetry && messageId) {
|
110 |
+
// two cases, if we're retrying a user message with a newPrompt set,
|
111 |
+
// it means we're editing a user message
|
112 |
+
// if we're retrying on an assistant message, newPrompt cannot be set
|
113 |
+
// it means we're retrying the last assistant message for a new answer
|
114 |
+
|
115 |
+
const messageToRetry = messages.find((message) => message.id === messageId);
|
116 |
+
|
117 |
+
if (!messageToRetry) {
|
118 |
+
$error = "Message not found";
|
119 |
+
}
|
120 |
+
|
121 |
+
if (messageToRetry?.from === "user" && prompt) {
|
122 |
+
// add a sibling to this message from the user, with the alternative prompt
|
123 |
+
// add a children to that sibling, where we can write to
|
124 |
+
const newUserMessageId = addSibling(
|
125 |
+
{
|
126 |
+
messages,
|
127 |
+
rootMessageId: data.rootMessageId,
|
128 |
+
},
|
129 |
+
{ from: "user", content: prompt },
|
130 |
+
messageId
|
131 |
+
);
|
132 |
+
messageToWriteToId = addChildren(
|
133 |
+
{
|
134 |
+
messages,
|
135 |
+
rootMessageId: data.rootMessageId,
|
136 |
+
},
|
137 |
+
{ from: "assistant", content: "", files: resizedImages },
|
138 |
+
newUserMessageId
|
139 |
+
);
|
140 |
+
} else if (messageToRetry?.from === "assistant") {
|
141 |
+
// we're retrying an assistant message, to generate a new answer
|
142 |
+
// just add a sibling to the assistant answer where we can write to
|
143 |
+
messageToWriteToId = addSibling(
|
144 |
+
{
|
145 |
+
messages,
|
146 |
+
rootMessageId: data.rootMessageId,
|
147 |
+
},
|
148 |
+
{ from: "assistant", content: "" },
|
149 |
+
messageId
|
150 |
+
);
|
151 |
+
}
|
152 |
+
} else {
|
153 |
+
// just a normal linear conversation, so we add the user message
|
154 |
+
// and the blank assistant message back to back
|
155 |
+
const newUserMessageId = addChildren(
|
156 |
{
|
157 |
+
messages,
|
158 |
+
rootMessageId: data.rootMessageId,
|
|
|
|
|
159 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
160 |
{
|
161 |
from: "user",
|
162 |
content: prompt ?? "",
|
|
|
163 |
files: resizedImages,
|
164 |
+
createdAt: new Date(),
|
165 |
+
updatedAt: new Date(),
|
166 |
+
},
|
167 |
+
messageId
|
168 |
+
);
|
169 |
+
|
170 |
+
if (!data.rootMessageId) {
|
171 |
+
data.rootMessageId = newUserMessageId;
|
172 |
+
}
|
173 |
+
|
174 |
+
messageToWriteToId = addChildren(
|
175 |
+
{
|
176 |
+
messages,
|
177 |
+
rootMessageId: data.rootMessageId,
|
178 |
+
},
|
179 |
+
{
|
180 |
+
from: "assistant",
|
181 |
+
content: "",
|
182 |
+
createdAt: new Date(),
|
183 |
+
updatedAt: new Date(),
|
184 |
},
|
185 |
+
newUserMessageId
|
186 |
+
);
|
187 |
}
|
188 |
|
189 |
+
messages = [...messages];
|
190 |
+
const messageToWriteTo = messages.find((message) => message.id === messageToWriteToId);
|
191 |
|
192 |
+
if (!messageToWriteTo) {
|
193 |
+
throw new Error("Message to write to not found");
|
194 |
+
}
|
195 |
// disable websearch if assistant is present
|
196 |
const hasAssistant = !!$page.data.assistant;
|
197 |
|
|
|
270 |
invalidate(UrlDependency.Conversation);
|
271 |
} else if (update.type === "stream") {
|
272 |
pending = false;
|
273 |
+
messageToWriteTo.content += update.token;
|
274 |
+
messages = [...messages];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
275 |
} else if (update.type === "webSearch") {
|
276 |
+
messageToWriteTo.updates = [...(messageToWriteTo.updates ?? []), update];
|
277 |
+
messages = [...messages];
|
278 |
} else if (update.type === "status") {
|
279 |
if (update.status === "title" && update.message) {
|
280 |
+
const convInData = data.conversations.find(({ id }) => id === $page.params.id);
|
281 |
+
if (convInData) {
|
282 |
+
convInData.title = update.message;
|
283 |
|
284 |
$titleUpdate = {
|
285 |
title: update.message,
|
|
|
305 |
});
|
306 |
}
|
307 |
|
308 |
+
messageToWriteTo.updates = messageUpdates;
|
|
|
|
|
|
|
|
|
309 |
await invalidate(UrlDependency.ConversationList);
|
310 |
} catch (err) {
|
311 |
if (err instanceof Error && err.message.includes("overloaded")) {
|
|
|
321 |
} finally {
|
322 |
loading = false;
|
323 |
pending = false;
|
324 |
+
await invalidate(UrlDependency.Conversation);
|
325 |
}
|
326 |
}
|
327 |
|
|
|
373 |
}
|
374 |
}
|
375 |
|
376 |
+
async function onRetry(event: CustomEvent<{ id: Message["id"]; content?: string }>) {
|
377 |
if (!data.shared) {
|
378 |
await writeMessage({
|
379 |
prompt: event.detail.content,
|
|
|
400 |
async function onContinue(event: CustomEvent<{ id: Message["id"] }>) {
|
401 |
if (!data.shared) {
|
402 |
writeMessage({ messageId: event.detail.id, isContinue: true });
|
403 |
+
} else {
|
404 |
+
await convFromShared()
|
405 |
+
.then(async (convId) => {
|
406 |
+
await goto(`${base}/conversation/${convId}`, { invalidateAll: true });
|
407 |
+
})
|
408 |
+
.then(
|
409 |
+
async () =>
|
410 |
+
await writeMessage({
|
411 |
+
messageId: event.detail.id,
|
412 |
+
isContinue: true,
|
413 |
+
})
|
414 |
+
)
|
415 |
+
.finally(() => (loading = false));
|
416 |
}
|
417 |
}
|
418 |
|
419 |
+
$: $page.params.id, (($isAborted = true), (loading = false), ($convTreeStore.editing = null));
|
420 |
$: title = data.conversations.find((conv) => conv.id === $page.params.id)?.title ?? data.title;
|
421 |
+
|
422 |
+
const convTreeStore = createConvTreeStore();
|
423 |
</script>
|
424 |
|
425 |
<svelte:head>
|
|
|
438 |
{messages}
|
439 |
shared={data.shared}
|
440 |
preprompt={data.preprompt}
|
|
|
441 |
bind:files
|
442 |
on:message={onMessage}
|
443 |
on:retry={onRetry}
|
@@ -9,11 +9,16 @@ import { ObjectId } from "mongodb";
|
|
9 |
import { z } from "zod";
|
10 |
import type { MessageUpdate } from "$lib/types/MessageUpdate";
|
11 |
import { runWebSearch } from "$lib/server/websearch/runWebSearch";
|
12 |
-
import type { WebSearch } from "$lib/types/WebSearch";
|
13 |
import { abortedGenerations } from "$lib/server/abortedGenerations";
|
14 |
import { summarize } from "$lib/server/summarize";
|
15 |
import { uploadFile } from "$lib/server/files/uploadFile";
|
16 |
import sizeof from "image-size";
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
|
18 |
export async function POST({ request, locals, params, getClientAddress }) {
|
19 |
const id = z.string().parse(params.id);
|
@@ -28,6 +33,29 @@ export async function POST({ request, locals, params, getClientAddress }) {
|
|
28 |
}
|
29 |
|
30 |
// check if the user has access to the conversation
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
31 |
const conv = await collections.conversations.findOne({
|
32 |
_id: convId,
|
33 |
...authCondition(locals),
|
@@ -97,8 +125,8 @@ export async function POST({ request, locals, params, getClientAddress }) {
|
|
97 |
files: b64files,
|
98 |
} = z
|
99 |
.object({
|
|
|
100 |
inputs: z.optional(z.string().trim().min(1)),
|
101 |
-
id: z.optional(z.string().uuid()),
|
102 |
is_retry: z.optional(z.boolean()),
|
103 |
is_continue: z.optional(z.boolean()),
|
104 |
web_search: z.optional(z.boolean()),
|
@@ -138,59 +166,93 @@ export async function POST({ request, locals, params, getClientAddress }) {
|
|
138 |
hashes = await Promise.all(files.map(async (file) => await uploadFile(file, conv)));
|
139 |
}
|
140 |
|
141 |
-
//
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
153 |
|
154 |
-
|
155 |
-
|
156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
157 |
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
} else {
|
172 |
-
// in normal conversation we add an extra user message
|
173 |
-
return [
|
174 |
-
...conv.messages,
|
175 |
-
{
|
176 |
-
content: newPrompt ?? "",
|
177 |
-
from: "user",
|
178 |
-
id: (messageId as Message["id"]) || crypto.randomUUID(),
|
179 |
-
createdAt: new Date(),
|
180 |
-
updatedAt: new Date(),
|
181 |
-
files: hashes,
|
182 |
-
},
|
183 |
-
];
|
184 |
-
} // else append the message at the bottom
|
185 |
-
})() satisfies Message[];
|
186 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
187 |
await collections.conversations.updateOne(
|
188 |
{
|
189 |
_id: convId,
|
190 |
},
|
191 |
{
|
192 |
$set: {
|
193 |
-
messages,
|
194 |
title: conv.title,
|
195 |
updatedAt: new Date(),
|
196 |
},
|
@@ -202,13 +264,10 @@ export async function POST({ request, locals, params, getClientAddress }) {
|
|
202 |
// we now build the stream
|
203 |
const stream = new ReadableStream({
|
204 |
async start(controller) {
|
205 |
-
|
206 |
-
? conv.messages[conv.messages.length - 1].updates ?? []
|
207 |
-
: [];
|
208 |
-
|
209 |
function update(newUpdate: MessageUpdate) {
|
210 |
if (newUpdate.type !== "stream") {
|
211 |
-
updates
|
212 |
}
|
213 |
|
214 |
if (newUpdate.type === "stream" && newUpdate.token === "") {
|
@@ -225,10 +284,21 @@ export async function POST({ request, locals, params, getClientAddress }) {
|
|
225 |
update({ type: "status", status: "started" });
|
226 |
|
227 |
const summarizeIfNeeded = (async () => {
|
228 |
-
if (conv.title === "New Chat" && messages.length ===
|
229 |
try {
|
230 |
-
conv.title = (await summarize(messages[
|
231 |
update({ type: "status", status: "title", message: conv.title });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
232 |
} catch (e) {
|
233 |
console.error(e);
|
234 |
}
|
@@ -241,31 +311,33 @@ export async function POST({ request, locals, params, getClientAddress }) {
|
|
241 |
},
|
242 |
{
|
243 |
$set: {
|
244 |
-
messages,
|
245 |
title: conv.title,
|
246 |
updatedAt: new Date(),
|
247 |
},
|
248 |
}
|
249 |
);
|
250 |
|
251 |
-
|
252 |
-
|
253 |
if (webSearch && !isContinue && !conv.assistantId) {
|
254 |
-
|
255 |
-
messages[messages.length - 1].webSearch = webSearchResults;
|
256 |
-
} else if (isContinue) {
|
257 |
-
webSearchResults = messages[messages.length - 1].webSearch;
|
258 |
}
|
259 |
|
260 |
-
|
|
|
|
|
|
|
|
|
|
|
261 |
|
262 |
-
const
|
263 |
-
? conv.messages.find((message) => message.id === messageId)?.content ?? ""
|
264 |
-
: "";
|
265 |
|
266 |
try {
|
267 |
const endpoint = await model.getEndpoint();
|
268 |
-
for await (const output of await endpoint({
|
|
|
|
|
|
|
|
|
269 |
// if not generated_text is here it means the generation is not done
|
270 |
if (!output.generated_text) {
|
271 |
// else we get the next token
|
@@ -274,63 +346,33 @@ export async function POST({ request, locals, params, getClientAddress }) {
|
|
274 |
type: "stream",
|
275 |
token: output.token.text,
|
276 |
});
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
// id doesn't match the backend id but it's not important for assistant messages
|
286 |
-
// First token has a space at the beginning, trim it
|
287 |
-
{
|
288 |
-
from: "assistant",
|
289 |
-
content: output.token.text.trimStart(),
|
290 |
-
webSearch: webSearchResults,
|
291 |
-
updates,
|
292 |
-
id: crypto.randomUUID(),
|
293 |
-
createdAt: new Date(),
|
294 |
-
updatedAt: new Date(),
|
295 |
-
},
|
296 |
-
];
|
297 |
-
} else {
|
298 |
-
// abort check
|
299 |
-
const date = abortedGenerations.get(convId.toString());
|
300 |
-
if (date && date > promptedAt) {
|
301 |
-
break;
|
302 |
-
}
|
303 |
-
|
304 |
-
if (!output) {
|
305 |
-
break;
|
306 |
-
}
|
307 |
-
|
308 |
-
// otherwise we just concatenate tokens
|
309 |
-
lastMessage.content += output.token.text;
|
310 |
}
|
|
|
|
|
|
|
311 |
}
|
312 |
} else {
|
313 |
-
|
314 |
// add output.generated text to the last message
|
315 |
// strip end tokens from the output.generated_text
|
316 |
const text = (model.parameters.stop ?? []).reduce((acc: string, curr: string) => {
|
317 |
if (acc.endsWith(curr)) {
|
318 |
-
interrupted = false;
|
319 |
return acc.slice(0, acc.length - curr.length);
|
320 |
}
|
321 |
return acc;
|
322 |
}, output.generated_text.trimEnd());
|
323 |
|
324 |
-
|
325 |
-
|
326 |
-
{
|
327 |
-
...messages[messages.length - 1],
|
328 |
-
content: previousContent + text,
|
329 |
-
interrupted, // if its a special token it finished on its own, else it was interrupted
|
330 |
-
updates,
|
331 |
-
updatedAt: new Date(),
|
332 |
-
},
|
333 |
-
];
|
334 |
}
|
335 |
}
|
336 |
} catch (e) {
|
@@ -343,7 +385,7 @@ export async function POST({ request, locals, params, getClientAddress }) {
|
|
343 |
},
|
344 |
{
|
345 |
$set: {
|
346 |
-
messages,
|
347 |
title: conv?.title,
|
348 |
updatedAt: new Date(),
|
349 |
},
|
@@ -355,7 +397,7 @@ export async function POST({ request, locals, params, getClientAddress }) {
|
|
355 |
|
356 |
update({
|
357 |
type: "finalAnswer",
|
358 |
-
text:
|
359 |
});
|
360 |
|
361 |
await summarizeIfNeeded;
|
@@ -370,7 +412,7 @@ export async function POST({ request, locals, params, getClientAddress }) {
|
|
370 |
},
|
371 |
{
|
372 |
$set: {
|
373 |
-
messages,
|
374 |
title: conv.title,
|
375 |
updatedAt: new Date(),
|
376 |
},
|
|
|
9 |
import { z } from "zod";
|
10 |
import type { MessageUpdate } from "$lib/types/MessageUpdate";
|
11 |
import { runWebSearch } from "$lib/server/websearch/runWebSearch";
|
|
|
12 |
import { abortedGenerations } from "$lib/server/abortedGenerations";
|
13 |
import { summarize } from "$lib/server/summarize";
|
14 |
import { uploadFile } from "$lib/server/files/uploadFile";
|
15 |
import sizeof from "image-size";
|
16 |
+
import { convertLegacyConversation } from "$lib/utils/tree/convertLegacyConversation";
|
17 |
+
import { isMessageId } from "$lib/utils/tree/isMessageId";
|
18 |
+
import { buildSubtree } from "$lib/utils/tree/buildSubtree.js";
|
19 |
+
import { addChildren } from "$lib/utils/tree/addChildren.js";
|
20 |
+
import { addSibling } from "$lib/utils/tree/addSibling.js";
|
21 |
+
import { preprocessMessages } from "$lib/server/preprocessMessages.js";
|
22 |
|
23 |
export async function POST({ request, locals, params, getClientAddress }) {
|
24 |
const id = z.string().parse(params.id);
|
|
|
33 |
}
|
34 |
|
35 |
// check if the user has access to the conversation
|
36 |
+
const convBeforeCheck = await collections.conversations.findOne({
|
37 |
+
_id: convId,
|
38 |
+
...authCondition(locals),
|
39 |
+
});
|
40 |
+
|
41 |
+
if (convBeforeCheck && !convBeforeCheck.rootMessageId) {
|
42 |
+
const res = await collections.conversations.updateOne(
|
43 |
+
{
|
44 |
+
_id: convId,
|
45 |
+
},
|
46 |
+
{
|
47 |
+
$set: {
|
48 |
+
...convBeforeCheck,
|
49 |
+
...convertLegacyConversation(convBeforeCheck),
|
50 |
+
},
|
51 |
+
}
|
52 |
+
);
|
53 |
+
|
54 |
+
if (!res.acknowledged) {
|
55 |
+
throw error(500, "Failed to convert conversation");
|
56 |
+
}
|
57 |
+
}
|
58 |
+
|
59 |
const conv = await collections.conversations.findOne({
|
60 |
_id: convId,
|
61 |
...authCondition(locals),
|
|
|
125 |
files: b64files,
|
126 |
} = z
|
127 |
.object({
|
128 |
+
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
|
129 |
inputs: z.optional(z.string().trim().min(1)),
|
|
|
130 |
is_retry: z.optional(z.boolean()),
|
131 |
is_continue: z.optional(z.boolean()),
|
132 |
web_search: z.optional(z.boolean()),
|
|
|
166 |
hashes = await Promise.all(files.map(async (file) => await uploadFile(file, conv)));
|
167 |
}
|
168 |
|
169 |
+
// we will append tokens to the content of this message
|
170 |
+
let messageToWriteToId: Message["id"] | undefined = undefined;
|
171 |
+
// used for building the prompt, subtree of the conversation that goes from the latest message to the root
|
172 |
+
let messagesForPrompt: Message[] = [];
|
173 |
|
174 |
+
if (isContinue && messageId) {
|
175 |
+
// if it's the last message and we continue then we build the prompt up to the last message
|
176 |
+
// we will strip the end tokens afterwards when the prompt is built
|
177 |
+
if ((conv.messages.find((msg) => msg.id === messageId)?.children?.length ?? 0) > 0) {
|
178 |
+
throw error(400, "Can only continue the last message");
|
179 |
+
}
|
180 |
+
messageToWriteToId = messageId;
|
181 |
+
messagesForPrompt = buildSubtree(conv, messageId);
|
182 |
+
} else if (isRetry && messageId) {
|
183 |
+
// two cases, if we're retrying a user message with a newPrompt set,
|
184 |
+
// it means we're editing a user message
|
185 |
+
// if we're retrying on an assistant message, newPrompt cannot be set
|
186 |
+
// it means we're retrying the last assistant message for a new answer
|
187 |
+
|
188 |
+
const messageToRetry = conv.messages.find((message) => message.id === messageId);
|
189 |
+
|
190 |
+
if (!messageToRetry) {
|
191 |
+
throw error(404, "Message not found");
|
192 |
+
}
|
193 |
|
194 |
+
if (messageToRetry.from === "user" && newPrompt) {
|
195 |
+
// add a sibling to this message from the user, with the alternative prompt
|
196 |
+
// add a children to that sibling, where we can write to
|
197 |
+
const newUserMessageId = addSibling(conv, { from: "user", content: newPrompt }, messageId);
|
198 |
+
messageToWriteToId = addChildren(
|
199 |
+
conv,
|
200 |
+
{ from: "assistant", content: "", files: hashes },
|
201 |
+
newUserMessageId
|
202 |
+
);
|
203 |
+
messagesForPrompt = buildSubtree(conv, newUserMessageId);
|
204 |
+
} else if (messageToRetry.from === "assistant") {
|
205 |
+
// we're retrying an assistant message, to generate a new answer
|
206 |
+
// just add a sibling to the assistant answer where we can write to
|
207 |
+
messageToWriteToId = addSibling(conv, { from: "assistant", content: "" }, messageId);
|
208 |
+
messagesForPrompt = buildSubtree(conv, messageId);
|
209 |
+
messagesForPrompt.pop(); // don't need the latest assistant message in the prompt since we're retrying it
|
210 |
+
}
|
211 |
+
} else {
|
212 |
+
// just a normal linear conversation, so we add the user message
|
213 |
+
// and the blank assistant message back to back
|
214 |
+
const newUserMessageId = addChildren(
|
215 |
+
conv,
|
216 |
+
{
|
217 |
+
from: "user",
|
218 |
+
content: newPrompt ?? "",
|
219 |
+
files: hashes,
|
220 |
+
createdAt: new Date(),
|
221 |
+
updatedAt: new Date(),
|
222 |
+
},
|
223 |
+
messageId
|
224 |
+
);
|
225 |
|
226 |
+
messageToWriteToId = addChildren(
|
227 |
+
conv,
|
228 |
+
{
|
229 |
+
from: "assistant",
|
230 |
+
content: "",
|
231 |
+
createdAt: new Date(),
|
232 |
+
updatedAt: new Date(),
|
233 |
+
},
|
234 |
+
newUserMessageId
|
235 |
+
);
|
236 |
+
// build the prompt from the user message
|
237 |
+
messagesForPrompt = buildSubtree(conv, newUserMessageId);
|
238 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
239 |
|
240 |
+
const messageToWriteTo = conv.messages.find((message) => message.id === messageToWriteToId);
|
241 |
+
if (!messageToWriteTo) {
|
242 |
+
throw error(500, "Failed to create message");
|
243 |
+
}
|
244 |
+
if (messagesForPrompt.length === 0) {
|
245 |
+
throw error(500, "Failed to create prompt");
|
246 |
+
}
|
247 |
+
|
248 |
+
// update the conversation with the new messages
|
249 |
await collections.conversations.updateOne(
|
250 |
{
|
251 |
_id: convId,
|
252 |
},
|
253 |
{
|
254 |
$set: {
|
255 |
+
messages: conv.messages,
|
256 |
title: conv.title,
|
257 |
updatedAt: new Date(),
|
258 |
},
|
|
|
264 |
// we now build the stream
|
265 |
const stream = new ReadableStream({
|
266 |
async start(controller) {
|
267 |
+
messageToWriteTo.updates ??= [];
|
|
|
|
|
|
|
268 |
function update(newUpdate: MessageUpdate) {
|
269 |
if (newUpdate.type !== "stream") {
|
270 |
+
messageToWriteTo?.updates?.push(newUpdate);
|
271 |
}
|
272 |
|
273 |
if (newUpdate.type === "stream" && newUpdate.token === "") {
|
|
|
284 |
update({ type: "status", status: "started" });
|
285 |
|
286 |
const summarizeIfNeeded = (async () => {
|
287 |
+
if (conv.title === "New Chat" && conv.messages.length === 3) {
|
288 |
try {
|
289 |
+
conv.title = (await summarize(conv.messages[1].content)) ?? conv.title;
|
290 |
update({ type: "status", status: "title", message: conv.title });
|
291 |
+
await collections.conversations.updateOne(
|
292 |
+
{
|
293 |
+
_id: convId,
|
294 |
+
},
|
295 |
+
{
|
296 |
+
$set: {
|
297 |
+
title: conv?.title,
|
298 |
+
updatedAt: new Date(),
|
299 |
+
},
|
300 |
+
}
|
301 |
+
);
|
302 |
} catch (e) {
|
303 |
console.error(e);
|
304 |
}
|
|
|
311 |
},
|
312 |
{
|
313 |
$set: {
|
|
|
314 |
title: conv.title,
|
315 |
updatedAt: new Date(),
|
316 |
},
|
317 |
}
|
318 |
);
|
319 |
|
320 |
+
// perform websearch if needed
|
|
|
321 |
if (webSearch && !isContinue && !conv.assistantId) {
|
322 |
+
messageToWriteTo.webSearch = await runWebSearch(conv, messagesForPrompt, update);
|
|
|
|
|
|
|
323 |
}
|
324 |
|
325 |
+
// inject websearch result & optionally images into the messages
|
326 |
+
const processedMessages = await preprocessMessages(
|
327 |
+
messagesForPrompt,
|
328 |
+
model.multimodal,
|
329 |
+
convId
|
330 |
+
);
|
331 |
|
332 |
+
const previousText = messageToWriteTo.content;
|
|
|
|
|
333 |
|
334 |
try {
|
335 |
const endpoint = await model.getEndpoint();
|
336 |
+
for await (const output of await endpoint({
|
337 |
+
messages: processedMessages,
|
338 |
+
preprompt: conv.preprompt,
|
339 |
+
continueMessage: isContinue,
|
340 |
+
})) {
|
341 |
// if not generated_text is here it means the generation is not done
|
342 |
if (!output.generated_text) {
|
343 |
// else we get the next token
|
|
|
346 |
type: "stream",
|
347 |
token: output.token.text,
|
348 |
});
|
349 |
+
// abort check
|
350 |
+
const date = abortedGenerations.get(convId.toString());
|
351 |
+
if (date && date > promptedAt) {
|
352 |
+
break;
|
353 |
+
}
|
354 |
+
// no output check
|
355 |
+
if (!output) {
|
356 |
+
break;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
357 |
}
|
358 |
+
|
359 |
+
// otherwise we just concatenate tokens
|
360 |
+
messageToWriteTo.content += output.token.text;
|
361 |
}
|
362 |
} else {
|
363 |
+
messageToWriteTo.interrupted = !output.token.special;
|
364 |
// add output.generated text to the last message
|
365 |
// strip end tokens from the output.generated_text
|
366 |
const text = (model.parameters.stop ?? []).reduce((acc: string, curr: string) => {
|
367 |
if (acc.endsWith(curr)) {
|
368 |
+
messageToWriteTo.interrupted = false;
|
369 |
return acc.slice(0, acc.length - curr.length);
|
370 |
}
|
371 |
return acc;
|
372 |
}, output.generated_text.trimEnd());
|
373 |
|
374 |
+
messageToWriteTo.content = previousText + text;
|
375 |
+
messageToWriteTo.updatedAt = new Date();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
376 |
}
|
377 |
}
|
378 |
} catch (e) {
|
|
|
385 |
},
|
386 |
{
|
387 |
$set: {
|
388 |
+
messages: conv.messages,
|
389 |
title: conv?.title,
|
390 |
updatedAt: new Date(),
|
391 |
},
|
|
|
397 |
|
398 |
update({
|
399 |
type: "finalAnswer",
|
400 |
+
text: messageToWriteTo.content,
|
401 |
});
|
402 |
|
403 |
await summarizeIfNeeded;
|
|
|
412 |
},
|
413 |
{
|
414 |
$set: {
|
415 |
+
messages: conv.messages,
|
416 |
title: conv.title,
|
417 |
updatedAt: new Date(),
|
418 |
},
|
@@ -2,6 +2,8 @@ import { buildPrompt } from "$lib/buildPrompt";
|
|
2 |
import { authCondition } from "$lib/server/auth";
|
3 |
import { collections } from "$lib/server/database";
|
4 |
import { models } from "$lib/server/models";
|
|
|
|
|
5 |
import { error } from "@sveltejs/kit";
|
6 |
import { ObjectId } from "mongodb";
|
7 |
|
@@ -24,7 +26,7 @@ export async function GET({ params, locals }) {
|
|
24 |
|
25 |
const messageIndex = conv.messages.findIndex((msg) => msg.id === messageId);
|
26 |
|
27 |
-
if (messageIndex === -1) {
|
28 |
throw error(404, "Message not found");
|
29 |
}
|
30 |
|
@@ -34,11 +36,10 @@ export async function GET({ params, locals }) {
|
|
34 |
throw error(404, "Conversation model not found");
|
35 |
}
|
36 |
|
37 |
-
const messagesUpTo = conv
|
38 |
|
39 |
const prompt = await buildPrompt({
|
40 |
preprompt: conv.preprompt,
|
41 |
-
webSearch: messagesUpTo[messagesUpTo.length - 1].webSearch,
|
42 |
messages: messagesUpTo,
|
43 |
model,
|
44 |
});
|
|
|
2 |
import { authCondition } from "$lib/server/auth";
|
3 |
import { collections } from "$lib/server/database";
|
4 |
import { models } from "$lib/server/models";
|
5 |
+
import { buildSubtree } from "$lib/utils/tree/buildSubtree";
|
6 |
+
import { isMessageId } from "$lib/utils/tree/isMessageId";
|
7 |
import { error } from "@sveltejs/kit";
|
8 |
import { ObjectId } from "mongodb";
|
9 |
|
|
|
26 |
|
27 |
const messageIndex = conv.messages.findIndex((msg) => msg.id === messageId);
|
28 |
|
29 |
+
if (!isMessageId(messageId) || messageIndex === -1) {
|
30 |
throw error(404, "Message not found");
|
31 |
}
|
32 |
|
|
|
36 |
throw error(404, "Conversation model not found");
|
37 |
}
|
38 |
|
39 |
+
const messagesUpTo = buildSubtree(conv, messageId);
|
40 |
|
41 |
const prompt = await buildPrompt({
|
42 |
preprompt: conv.preprompt,
|
|
|
43 |
messages: messagesUpTo,
|
44 |
model,
|
45 |
});
|
@@ -32,10 +32,11 @@ export async function POST({ params, url, locals }) {
|
|
32 |
|
33 |
const shared: SharedConversation = {
|
34 |
_id: nanoid(7),
|
35 |
-
createdAt: new Date(),
|
36 |
-
messages: conversation.messages,
|
37 |
hash,
|
|
|
38 |
updatedAt: new Date(),
|
|
|
|
|
39 |
title: conversation.title,
|
40 |
model: conversation.model,
|
41 |
embeddingModel: conversation.embeddingModel,
|
|
|
32 |
|
33 |
const shared: SharedConversation = {
|
34 |
_id: nanoid(7),
|
|
|
|
|
35 |
hash,
|
36 |
+
createdAt: new Date(),
|
37 |
updatedAt: new Date(),
|
38 |
+
rootMessageId: conversation.rootMessageId,
|
39 |
+
messages: conversation.messages,
|
40 |
title: conversation.title,
|
41 |
model: conversation.model,
|
42 |
embeddingModel: conversation.embeddingModel,
|