From d6dc46a3980478887a825d73a2041a44303e21c4 Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Mon, 4 Nov 2024 08:35:45 +0000 Subject: [PATCH 1/9] fix: bump copy limit into conversation --- src/lib/components/chat/ChatWindow.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/chat/ChatWindow.svelte b/src/lib/components/chat/ChatWindow.svelte index 0ab86c9035a..f6dfe767e3e 100644 --- a/src/lib/components/chat/ChatWindow.svelte +++ b/src/lib/components/chat/ChatWindow.svelte @@ -92,7 +92,7 @@ const onPaste = (e: ClipboardEvent) => { const textContent = e.clipboardData?.getData("text"); - if (!$settings.directPaste && textContent && textContent.length > 1000) { + if (!$settings.directPaste && textContent && textContent.length >= 3984) { e.preventDefault(); pastedLongContent = true; setTimeout(() => { From 54297d06694328bbef1666d1d9b8e6c1b0570432 Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Tue, 5 Nov 2024 11:26:20 +0000 Subject: [PATCH 2/9] fix: only show playground button for models that are available --- src/lib/server/models.ts | 39 ++++++++++++--------------------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/src/lib/server/models.ts b/src/lib/server/models.ts index d2ac82cd990..be6e7d608c0 100644 --- a/src/lib/server/models.ts +++ b/src/lib/server/models.ts @@ -337,32 +337,17 @@ const addEndpoint = (m: Awaited>) => ({ }, }); -const hasInferenceAPI = async (m: Awaited>) => { - if (!isHuggingChat) { - return false; - } - - let r: Response; - try { - r = await fetch(`https://huggingface.co/api/models/${m.id}`); - } catch (e) { - console.log(e); - return false; - } - - if (!r.ok) { - logger.warn(`Failed to check if ${m.id} has inference API: ${r.statusText}`); - return false; - } - - const json = await r.json(); - - if (json.cardData.inference === false) { - return false; - } - - return true; -}; +const inferenceApiIds = isHuggingChat + ? await fetch( + "https://huggingface.co/api/models?pipeline_tag=text-generation&inference=warm&filter=conversational" + ) + .then((r) => r.json()) + .then((json) => json.map((r: { id: string }) => r.id)) + .catch((err) => { + logger.error(err, "Failed to fetch inference API ids"); + return []; + }) + : []; export const models = await Promise.all( modelsRaw.map((e) => @@ -370,7 +355,7 @@ export const models = await Promise.all( .then(addEndpoint) .then(async (m) => ({ ...m, - hasInferenceAPI: await hasInferenceAPI(m), + hasInferenceAPI: inferenceApiIds.includes(m.id ?? m.name), })) ) ); From b4774974c7246edebe9ebe6570dc6f8d5e6d9051 Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Tue, 5 Nov 2024 16:30:56 +0100 Subject: [PATCH 3/9] feat: add sources for websearch (#1551) * feat: playwright, spatial parsing, markdown for web search Co-authored-by: Aaditya Sahay * feat: choose multiple clusters if necessary (#2) * chore: resolve linting failures * feat: improve paring performance and error messages * feat: inline citations * feat: adjust inline citation prompt, less intrusive tokens * feat: add sources to message when using websearch * fix: clean up packages * fix: packages * fix: packages lol * fix: make websearch citation work better wiht tools * fix: use single brackets for sources, only render source element if a matching source is available * fix: bad import --------- Co-authored-by: Liam Dyer Co-authored-by: Aaditya Sahay Co-authored-by: Aaditya Sahay <56438732+Aaditya-Sahay@users.noreply.github.com> --- package-lock.json | 2 +- src/lib/components/chat/ChatMessage.svelte | 24 ++++++++++++++++++- .../server/endpoints/preprocessMessages.ts | 4 ++-- src/lib/server/tools/web/search.ts | 17 +++++++++---- 4 files changed, 39 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 85a225810fe..0f8696b69cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13434,4 +13434,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/lib/components/chat/ChatMessage.svelte b/src/lib/components/chat/ChatMessage.svelte index f7b0adeb14f..6d24914e337 100644 --- a/src/lib/components/chat/ChatMessage.svelte +++ b/src/lib/components/chat/ChatMessage.svelte @@ -37,6 +37,28 @@ import DOMPurify from "isomorphic-dompurify"; import { enhance } from "$app/forms"; import { browser } from "$app/environment"; + import type { WebSearchSource } from "$lib/types/WebSearch"; + + function addInlineCitations(md: string, webSearchSources: WebSearchSource[] = []): string { + const linkStyle = + "color: rgb(59, 130, 246); text-decoration: none; hover:text-decoration: underline;"; + + return md.replace(/\[(\d+)\]/g, (match: string) => { + const indices: number[] = (match.match(/\d+/g) || []).map(Number); + const links: string = indices + .map((index: number) => { + const source = webSearchSources[index - 1]; + if (source) { + return `${index}`; + } + return ""; + }) + .filter(Boolean) + .join(", "); + + return links ? ` ${links}` : match; + }); + } function sanitizeMd(md: string) { let ret = md @@ -114,7 +136,7 @@ }) ); - $: tokens = marked.lexer(sanitizeMd(message.content ?? "")); + $: tokens = marked.lexer(addInlineCitations(sanitizeMd(message.content), webSearchSources)); $: emptyLoad = !message.content && (webSearchIsDone || (searchUpdates && searchUpdates.length === 0)); diff --git a/src/lib/server/endpoints/preprocessMessages.ts b/src/lib/server/endpoints/preprocessMessages.ts index 6b9560a45fa..1d23c3a3c5e 100644 --- a/src/lib/server/endpoints/preprocessMessages.ts +++ b/src/lib/server/endpoints/preprocessMessages.ts @@ -17,7 +17,7 @@ export async function preprocessMessages( function addWebSearchContext(messages: Message[], webSearch: Message["webSearch"]) { const webSearchContext = webSearch?.contextSources - .map(({ context }) => context.trim()) + .map(({ context }, idx) => `Source [${idx + 1}]\n${context.trim()}`) .join("\n\n----------\n\n"); // No web search context available, skip @@ -35,7 +35,7 @@ function addWebSearchContext(messages: Message[], webSearch: Message["webSearch" const finalMessage = { ...messages[messages.length - 1], content: `I searched the web using the query: ${webSearch.searchQuery}. -Today is ${currentDate} and here are the results: +Today is ${currentDate} and here are the results. When answering the question, if you use a source, cite its index inline like this: [1], [2], etc. ===================== ${webSearchContext} ===================== diff --git a/src/lib/server/tools/web/search.ts b/src/lib/server/tools/web/search.ts index 129598d2434..0778536fdd5 100644 --- a/src/lib/server/tools/web/search.ts +++ b/src/lib/server/tools/web/search.ts @@ -25,12 +25,21 @@ const websearch: ConfigTool = { showOutput: false, async *call({ query }, { conv, assistant, messages }) { const webSearchToolResults = yield* runWebSearch(conv, messages, assistant?.rag, String(query)); - const chunks = webSearchToolResults?.contextSources - .map(({ context }) => context) - .join("\n------------\n"); + + const webSearchContext = webSearchToolResults?.contextSources + .map(({ context }, idx) => `Source [${idx + 1}]\n${context.trim()}`) + .join("\n\n----------\n\n"); return { - outputs: [{ websearch: chunks }], + outputs: [ + { + websearch: webSearchContext, + }, + { + instructions: + "When answering the question, if you use sources from the websearch results above, cite each index inline individually wrapped like: [1], [2] etc.", + }, + ], display: false, }; }, From c9703f4d37539df38a707d26e709b5060a7ed69c Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Tue, 5 Nov 2024 15:41:01 +0000 Subject: [PATCH 4/9] fix: better websearch quotes --- src/lib/components/chat/ChatMessage.svelte | 1 + src/lib/server/endpoints/preprocessMessages.ts | 3 ++- src/lib/server/tools/web/search.ts | 8 +++----- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lib/components/chat/ChatMessage.svelte b/src/lib/components/chat/ChatMessage.svelte index 6d24914e337..056984e78bb 100644 --- a/src/lib/components/chat/ChatMessage.svelte +++ b/src/lib/components/chat/ChatMessage.svelte @@ -47,6 +47,7 @@ const indices: number[] = (match.match(/\d+/g) || []).map(Number); const links: string = indices .map((index: number) => { + if (index === 0) return " "; const source = webSearchSources[index - 1]; if (source) { return `${index}`; diff --git a/src/lib/server/endpoints/preprocessMessages.ts b/src/lib/server/endpoints/preprocessMessages.ts index 1d23c3a3c5e..0394e95cfb2 100644 --- a/src/lib/server/endpoints/preprocessMessages.ts +++ b/src/lib/server/endpoints/preprocessMessages.ts @@ -35,7 +35,8 @@ function addWebSearchContext(messages: Message[], webSearch: Message["webSearch" const finalMessage = { ...messages[messages.length - 1], content: `I searched the web using the query: ${webSearch.searchQuery}. -Today is ${currentDate} and here are the results. When answering the question, if you use a source, cite its index inline like this: [1], [2], etc. +Today is ${currentDate} and here are the results. +When answering the question, you must reference the sources you used inline by wrapping the index in brackets like this: [1]. If multiple sources are used, you must reference each one of them without commas like this: [1][2][3]. ===================== ${webSearchContext} ===================== diff --git a/src/lib/server/tools/web/search.ts b/src/lib/server/tools/web/search.ts index 0778536fdd5..b181d661bfd 100644 --- a/src/lib/server/tools/web/search.ts +++ b/src/lib/server/tools/web/search.ts @@ -33,11 +33,9 @@ const websearch: ConfigTool = { return { outputs: [ { - websearch: webSearchContext, - }, - { - instructions: - "When answering the question, if you use sources from the websearch results above, cite each index inline individually wrapped like: [1], [2] etc.", + websearch: + webSearchContext + + "\n\nWhen answering the question, you must reference the sources you used inline by wrapping the index in brackets like this: [1]. If multiple sources are used, you must reference each one of them without commas like this: [1][2][3].", }, ], display: false, From 0cbb8626bbba11ce970c2e446d37b695dad6c249 Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Tue, 5 Nov 2024 20:46:26 +0100 Subject: [PATCH 5/9] feat: lazy stream conversations load function (#1553) * feat: lazy stream conversations load function * fix: lint * feat: add animation * feat: skip db call if no convs --- src/lib/components/MobileNav.svelte | 8 +- src/lib/components/NavConversationItem.svelte | 4 +- src/lib/components/NavMenu.svelte | 48 ++++--- src/lib/types/ConvSidebar.ts | 2 +- src/routes/+layout.server.ts | 133 +++++++++++------- src/routes/+layout.svelte | 18 +-- src/routes/conversation/[id]/+page.svelte | 17 ++- .../assistants/[assistantId]/+page.svelte | 2 +- src/routes/settings/+layout.server.ts | 2 +- 9 files changed, 148 insertions(+), 86 deletions(-) diff --git a/src/lib/components/MobileNav.svelte b/src/lib/components/MobileNav.svelte index d031c98c5b7..9c493b52b13 100644 --- a/src/lib/components/MobileNav.svelte +++ b/src/lib/components/MobileNav.svelte @@ -10,7 +10,7 @@ import IconNew from "$lib/components/icons/IconNew.svelte"; export let isOpen = false; - export let title: string | undefined; + export let title: Promise | string; $: title = title ?? "New Chat"; @@ -40,7 +40,11 @@ aria-label="Open menu" bind:this={openEl}> - {title} + {#await title} +
+ {:then title} + {title ?? ""} + {/await} Delete {/if} - {#if conv.avatarHash} + {#if conv.avatarUrl} Assistant avatar diff --git a/src/lib/components/NavMenu.svelte b/src/lib/components/NavMenu.svelte index 40786f030d7..f9f7409e4f6 100644 --- a/src/lib/components/NavMenu.svelte +++ b/src/lib/components/NavMenu.svelte @@ -11,7 +11,8 @@ import type { Model } from "$lib/types/Model"; import { page } from "$app/stores"; - export let conversations: ConvSidebar[] = []; + import { fade } from "svelte/transition"; + export let conversations: Promise; export let canLogin: boolean; export let user: LayoutData["user"]; @@ -25,16 +26,16 @@ new Date().setMonth(new Date().getMonth() - 1), ]; - $: groupedConversations = { - today: conversations.filter(({ updatedAt }) => updatedAt.getTime() > dateRanges[0]), - week: conversations.filter( + $: groupedConversations = conversations.then((convs) => ({ + today: convs.filter(({ updatedAt }) => updatedAt.getTime() > dateRanges[0]), + week: convs.filter( ({ updatedAt }) => updatedAt.getTime() > dateRanges[1] && updatedAt.getTime() < dateRanges[0] ), - month: conversations.filter( + month: convs.filter( ({ updatedAt }) => updatedAt.getTime() > dateRanges[2] && updatedAt.getTime() < dateRanges[1] ), - older: conversations.filter(({ updatedAt }) => updatedAt.getTime() < dateRanges[2]), - }; + older: convs.filter(({ updatedAt }) => updatedAt.getTime() < dateRanges[2]), + })); const titles: { [key: string]: string } = { today: "Today", @@ -65,16 +66,31 @@
- {#each Object.entries(groupedConversations) as [group, convs]} - {#if convs.length} -

- {titles[group]} -

- {#each convs as conv} - - {/each} + {#await groupedConversations} + {#if $page.data.nConversations > 0} +
+
+
+ {#each Array(100) as _} +
+ {/each} +
+
{/if} - {/each} + {:then groupedConversations} +
+ {#each Object.entries(groupedConversations) as [group, convs]} + {#if convs.length} +

+ {titles[group]} +

+ {#each convs as conv} + + {/each} + {/if} + {/each} +
+ {/await}
{ }) : null; - const conversations = await collections.conversations - .find(authCondition(locals)) - .sort({ updatedAt: -1 }) - .project< - Pick - >({ - title: 1, - model: 1, - _id: 1, - updatedAt: 1, - createdAt: 1, - assistantId: 1, - }) - .limit(300) - .toArray(); + const nConversations = await collections.conversations.countDocuments(authCondition(locals)); + + const conversations = + nConversations === 0 + ? Promise.resolve([]) + : collections.conversations + .find(authCondition(locals)) + .sort({ updatedAt: -1 }) + .project< + Pick< + Conversation, + "title" | "model" | "_id" | "updatedAt" | "createdAt" | "assistantId" + > + >({ + title: 1, + model: 1, + _id: 1, + updatedAt: 1, + createdAt: 1, + assistantId: 1, + }) + .limit(300) + .toArray(); const userAssistants = settings?.assistants?.map((assistantId) => assistantId.toString()) ?? []; const userAssistantsSet = new Set(userAssistants); - const assistantIds = [ - ...userAssistants.map((el) => new ObjectId(el)), - ...(conversations.map((conv) => conv.assistantId).filter((el) => !!el) as ObjectId[]), - ]; - - const assistants = await collections.assistants.find({ _id: { $in: assistantIds } }).toArray(); + const assistants = conversations.then((conversations) => + collections.assistants + .find({ + _id: { + $in: [ + ...userAssistants.map((el) => new ObjectId(el)), + ...(conversations.map((conv) => conv.assistantId).filter((el) => !!el) as ObjectId[]), + ], + }, + }) + .toArray() + ); const messagesBeforeLogin = env.MESSAGES_BEFORE_LOGIN ? parseInt(env.MESSAGES_BEFORE_LOGIN) : 0; let loginRequired = false; if (requiresUser && !locals.user && messagesBeforeLogin) { - if (conversations.length > messagesBeforeLogin) { + if (nConversations > messagesBeforeLogin) { loginRequired = true; } else { // get the number of messages where `from === "assistant"` across all conversations. @@ -129,25 +143,42 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => { ); return { - conversations: conversations.map((conv) => { - if (settings?.hideEmojiOnSidebar) { - conv.title = conv.title.replace(/\p{Emoji}/gu, ""); - } - - // remove invalid unicode and trim whitespaces - conv.title = conv.title.replace(/\uFFFD/gu, "").trimStart(); - - return { - id: conv._id.toString(), - title: conv.title, - model: conv.model ?? defaultModel, - updatedAt: conv.updatedAt, - assistantId: conv.assistantId?.toString(), - avatarHash: - conv.assistantId && - assistants.find((a) => a._id.toString() === conv.assistantId?.toString())?.avatar, - }; - }) satisfies ConvSidebar[], + nConversations, + conversations: conversations.then( + async (convs) => + await Promise.all( + convs.map(async (conv) => { + if (settings?.hideEmojiOnSidebar) { + conv.title = conv.title.replace(/\p{Emoji}/gu, ""); + } + + // remove invalid unicode and trim whitespaces + conv.title = conv.title.replace(/\uFFFD/gu, "").trimStart(); + + let avatarUrl: string | undefined = undefined; + + if (conv.assistantId) { + const hash = ( + await collections.assistants.findOne({ + _id: new ObjectId(conv.assistantId), + }) + )?.avatar; + if (hash) { + avatarUrl = `/settings/assistants/${conv.assistantId}/avatar.jpg?hash=${hash}`; + } + } + + return { + id: conv._id.toString(), + title: conv.title, + model: conv.model ?? defaultModel, + updatedAt: conv.updatedAt, + assistantId: conv.assistantId?.toString(), + avatarUrl, + } satisfies ConvSidebar; + }) + ) + ), settings: { searchEnabled: !!( env.SERPAPI_KEY || @@ -223,15 +254,17 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => { type: "community", review: ReviewStatus.APPROVED, }), - assistants: assistants - .filter((el) => userAssistantsSet.has(el._id.toString())) - .map((el) => ({ - ...el, - _id: el._id.toString(), - createdById: undefined, - createdByMe: - el.createdById.toString() === (locals.user?._id ?? locals.sessionId).toString(), - })), + assistants: assistants.then((assistants) => + assistants + .filter((el) => userAssistantsSet.has(el._id.toString())) + .map((el) => ({ + ...el, + _id: el._id.toString(), + createdById: undefined, + createdByMe: + el.createdById.toString() === (locals.user?._id ?? locals.sessionId).toString(), + })) + ), user: locals.user && { id: locals.user._id.toString(), username: locals.user.username, diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 17259c17e5f..8c98e4aee67 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -100,15 +100,17 @@ $: if ($error) onError(); $: if ($titleUpdate) { - const convIdx = data.conversations.findIndex(({ id }) => id === $titleUpdate?.convId); + data.conversations.then((convs) => { + const convIdx = convs.findIndex(({ id }) => id === $titleUpdate?.convId); - if (convIdx != -1) { - data.conversations[convIdx].title = $titleUpdate?.title ?? data.conversations[convIdx].title; - } - // update data.conversations - data.conversations = [...data.conversations]; + if (convIdx != -1) { + convs[convIdx].title = $titleUpdate?.title ?? convs[convIdx].title; + } + // update data.conversations + data.conversations = Promise.resolve([...convs]); - $titleUpdate = null; + $titleUpdate = null; + }); } const settings = createSettingsStore(data.settings); @@ -147,7 +149,7 @@ $: mobileNavTitle = ["/models", "/assistants", "/privacy"].includes($page.route.id ?? "") ? "" - : data.conversations.find((conv) => conv.id === $page.params.id)?.title; + : data.conversations.then((convs) => convs.find((conv) => conv.id === $page.params.id)?.title); diff --git a/src/routes/conversation/[id]/+page.svelte b/src/routes/conversation/[id]/+page.svelte index b5d7ad3f569..3c168f753fe 100644 --- a/src/routes/conversation/[id]/+page.svelte +++ b/src/routes/conversation/[id]/+page.svelte @@ -4,7 +4,7 @@ import { isAborted } from "$lib/stores/isAborted"; import { onMount } from "svelte"; import { page } from "$app/stores"; - import { goto, invalidateAll } from "$app/navigation"; + import { goto, invalidate } from "$app/navigation"; import { base } from "$app/paths"; import { shareConversation } from "$lib/shareConversation"; import { ERROR_MESSAGES, error } from "$lib/stores/errors"; @@ -24,6 +24,7 @@ import { createConvTreeStore } from "$lib/stores/convTree"; import type { v4 } from "uuid"; import { useSettingsStore } from "$lib/stores/settings.js"; + import { UrlDependency } from "$lib/types/UrlDependency.js"; export let data; @@ -247,7 +248,9 @@ ) { $error = update.message ?? "An error has occurred"; } else if (update.type === MessageUpdateType.Title) { - const convInData = data.conversations.find(({ id }) => id === $page.params.id); + const convInData = await data.conversations.then((convs) => + convs.find(({ id }) => id === $page.params.id) + ); if (convInData) { convInData.title = update.title; @@ -280,7 +283,7 @@ } finally { loading = false; pending = false; - await invalidateAll(); + await invalidate(UrlDependency.Conversation); } } @@ -376,14 +379,18 @@ } $: $page.params.id, (($isAborted = true), (loading = false), ($convTreeStore.editing = null)); - $: title = data.conversations.find((conv) => conv.id === $page.params.id)?.title ?? data.title; + $: title = data.conversations.then( + (convs) => convs.find((conv) => conv.id === $page.params.id)?.title ?? data.title + ); const convTreeStore = createConvTreeStore(); const settings = useSettingsStore(); - {title} + {#await title then title} + {title} + {/await}
- {/if} + {:else} + Start chatting + {/if} + {#if $page.data.loginEnabled}