Spaces:
Running
Add websearch controls for assistants (#812)
Browse files* remove query modifiers from generateQuery
* Add backend for assistant RAG
* Add front-end for updating RAG assistant
* enable web parser to return plaintext directly for matching headers
* Update websearch flow for handling assistant rag preferences
* Add our old blocklist to .env.template
* Enable websearch to run on messages depending on assistant requirements
* reorganized imports
* Rename vars
* use projection
* Add environment variable for assistant rag
* fix assistant rag on runwebsearch
* fix styling if rag is disabled
* make sure we always omit credentials when fetching web pages
* Add new checks for SSRF, with a new env var `ENABLE_LOCAL_FETCH`
* Use DNS to check if the links are local or not
* Add a websearch indicator
* Add more tags to parser
* Add indicators
* Display RAG options in settings view
* ui
* fix rag detection
* bit more spacing
* fix button position in assistant form
* wording (mainly)
* reduce number of tags
* Bump max URLs from 3 to 10
* add ul and ol to parseWeb
* change splitting string
* link style
* wording
* add feedback link
* Update src/routes/settings/(nav)/assistants/[assistantId]/+page.svelte
Co-authored-by: Mishig <mishig.davaadorj@coloradocollege.edu>
* Update src/routes/settings/(nav)/assistants/[assistantId]/+page.svelte
Co-authored-by: Mishig <mishig.davaadorj@coloradocollege.edu>
* Update src/routes/assistants/+page.svelte
Co-authored-by: Mishig <mishig.davaadorj@coloradocollege.edu>
* Update src/routes/settings/(nav)/assistants/[assistantId]/+page.svelte
Co-authored-by: Mishig <mishig.davaadorj@coloradocollege.edu>
* Update src/lib/components/chat/ChatWindow.svelte
Co-authored-by: Mishig <mishig.davaadorj@coloradocollege.edu>
* Update src/routes/settings/(nav)/assistants/[assistantId]/+page.svelte
Co-authored-by: Mishig <mishig.davaadorj@coloradocollege.edu>
* Update src/lib/components/AssistantSettings.svelte
Co-authored-by: Mishig <mishig.davaadorj@coloradocollege.edu>
* lint
* throw error if not a string
* simplify rag check
---------
Co-authored-by: Mishig <mishig.davaadorj@coloradocollege.edu>
Co-authored-by: Victor Mustar <victor.mustar@gmail.com>
- .env +2 -1
- .env.template +3 -0
- package-lock.json +1 -0
- package.json +1 -0
- src/lib/components/AssistantSettings.svelte +139 -6
- src/lib/components/chat/AssistantIntroduction.svelte +31 -5
- src/lib/components/chat/ChatWindow.svelte +1 -1
- src/lib/components/icons/IconInternet.svelte +1 -0
- src/lib/server/isURLLocal.spec.ts +31 -0
- src/lib/server/isURLLocal.ts +26 -0
- src/lib/server/websearch/generateQuery.ts +1 -14
- src/lib/server/websearch/parseWeb.ts +28 -21
- src/lib/server/websearch/runWebSearch.ts +75 -24
- src/lib/types/Assistant.ts +5 -0
- src/lib/utils/parseStringToList.ts +10 -0
- src/routes/+layout.server.ts +2 -0
- src/routes/assistants/+page.svelte +16 -0
- src/routes/conversation/[id]/+server.ts +17 -3
- src/routes/settings/(nav)/+layout.svelte +1 -1
- src/routes/settings/(nav)/assistants/[assistantId]/+page.svelte +60 -8
- src/routes/settings/(nav)/assistants/[assistantId]/edit/+page.server.ts +9 -0
- src/routes/settings/(nav)/assistants/new/+page.server.ts +9 -0
@@ -135,7 +135,8 @@ EXPOSE_API=true
|
|
135 |
# PUBLIC_APP_DISCLAIMER=1
|
136 |
|
137 |
ENABLE_ASSISTANTS=false #set to true to enable assistants feature
|
138 |
-
|
|
|
139 |
ALTERNATIVE_REDIRECT_URLS=`[]` #valide alternative redirect URL for OAuth
|
140 |
|
141 |
WEBHOOK_URL_REPORT_ASSISTANT=#provide webhook url to get notified when an assistant gets reported
|
|
|
135 |
# PUBLIC_APP_DISCLAIMER=1
|
136 |
|
137 |
ENABLE_ASSISTANTS=false #set to true to enable assistants feature
|
138 |
+
ENABLE_ASSISTANTS_RAG=false # /!\ This will let users specify arbitrary URLs that the server will then request. Make sure you have the proper firewall rules in place.
|
139 |
+
ENABLE_LOCAL_FETCH=false #set to true to disable the blocklist for local fetches. Only enable this if you have the proper firewall rules to prevent SSRF attacks and understand the implications.
|
140 |
ALTERNATIVE_REDIRECT_URLS=`[]` #valide alternative redirect URL for OAuth
|
141 |
|
142 |
WEBHOOK_URL_REPORT_ASSISTANT=#provide webhook url to get notified when an assistant gets reported
|
@@ -277,8 +277,11 @@ PUBLIC_PLAUSIBLE_SCRIPT_URL="/js/script.js"
|
|
277 |
# XFF_DEPTH=2
|
278 |
|
279 |
ENABLE_ASSISTANTS=true
|
|
|
280 |
EXPOSE_API=true
|
281 |
|
282 |
ALTERNATIVE_REDIRECT_URLS=`[
|
283 |
huggingchat://login/callback
|
284 |
]`
|
|
|
|
|
|
277 |
# XFF_DEPTH=2
|
278 |
|
279 |
ENABLE_ASSISTANTS=true
|
280 |
+
ENABLE_ASSISTANTS_RAG=true
|
281 |
EXPOSE_API=true
|
282 |
|
283 |
ALTERNATIVE_REDIRECT_URLS=`[
|
284 |
huggingchat://login/callback
|
285 |
]`
|
286 |
+
|
287 |
+
WEBSEARCH_BLOCKLIST=`["youtube.com", "twitter.com"]`
|
@@ -20,6 +20,7 @@
|
|
20 |
"handlebars": "^4.7.8",
|
21 |
"highlight.js": "^11.7.0",
|
22 |
"image-size": "^1.0.2",
|
|
|
23 |
"jsdom": "^22.0.0",
|
24 |
"json5": "^2.2.3",
|
25 |
"marked": "^4.3.0",
|
|
|
20 |
"handlebars": "^4.7.8",
|
21 |
"highlight.js": "^11.7.0",
|
22 |
"image-size": "^1.0.2",
|
23 |
+
"ip-address": "^9.0.5",
|
24 |
"jsdom": "^22.0.0",
|
25 |
"json5": "^2.2.3",
|
26 |
"marked": "^4.3.0",
|
@@ -62,6 +62,7 @@
|
|
62 |
"handlebars": "^4.7.8",
|
63 |
"highlight.js": "^11.7.0",
|
64 |
"image-size": "^1.0.2",
|
|
|
65 |
"jsdom": "^22.0.0",
|
66 |
"json5": "^2.2.3",
|
67 |
"marked": "^4.3.0",
|
|
|
62 |
"handlebars": "^4.7.8",
|
63 |
"highlight.js": "^11.7.0",
|
64 |
"image-size": "^1.0.2",
|
65 |
+
"ip-address": "^9.0.5",
|
66 |
"jsdom": "^22.0.0",
|
67 |
"json5": "^2.2.3",
|
68 |
"marked": "^4.3.0",
|
@@ -5,11 +5,13 @@
|
|
5 |
|
6 |
import { onMount } from "svelte";
|
7 |
import { applyAction, enhance } from "$app/forms";
|
|
|
8 |
import { base } from "$app/paths";
|
9 |
import CarbonPen from "~icons/carbon/pen";
|
10 |
import CarbonUpload from "~icons/carbon/upload";
|
11 |
|
12 |
import { useSettingsStore } from "$lib/stores/settings";
|
|
|
13 |
|
14 |
type ActionData = {
|
15 |
error: boolean;
|
@@ -71,6 +73,14 @@
|
|
71 |
let deleteExistingAvatar = false;
|
72 |
|
73 |
let loading = false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
74 |
</script>
|
75 |
|
76 |
<form
|
@@ -103,6 +113,24 @@
|
|
103 |
}
|
104 |
}
|
105 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
106 |
return async ({ result }) => {
|
107 |
loading = false;
|
108 |
await applyAction(result);
|
@@ -126,7 +154,7 @@
|
|
126 |
{/if}
|
127 |
|
128 |
<div class="grid h-full w-full flex-1 grid-cols-2 gap-6 text-sm max-sm:grid-cols-1">
|
129 |
-
<div class="flex flex-col gap-4">
|
130 |
<div>
|
131 |
<div class="mb-1 block pb-2 text-sm font-semibold">Avatar</div>
|
132 |
<input
|
@@ -255,21 +283,126 @@
|
|
255 |
</div>
|
256 |
<p class="text-xs text-red-500">{getError("inputMessage1", form)}</p>
|
257 |
</label>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
258 |
</div>
|
259 |
|
260 |
-
<
|
261 |
-
<
|
262 |
<textarea
|
263 |
name="preprompt"
|
264 |
-
class="min-h-[8lh] flex-1 rounded-lg border-2 border-gray-200 bg-gray-100 p-2 text-sm"
|
265 |
placeholder="You'll act as..."
|
266 |
value={assistant?.preprompt ?? ""}
|
267 |
/>
|
268 |
<p class="text-xs text-red-500">{getError("preprompt", form)}</p>
|
269 |
-
</
|
270 |
</div>
|
271 |
|
272 |
-
<div
|
|
|
|
|
273 |
<a
|
274 |
href={assistant ? `${base}/settings/assistants/${assistant?._id}` : `${base}/settings`}
|
275 |
class="flex items-center justify-center rounded-full bg-gray-200 px-5 py-2 font-semibold text-gray-600"
|
|
|
5 |
|
6 |
import { onMount } from "svelte";
|
7 |
import { applyAction, enhance } from "$app/forms";
|
8 |
+
import { page } from "$app/stores";
|
9 |
import { base } from "$app/paths";
|
10 |
import CarbonPen from "~icons/carbon/pen";
|
11 |
import CarbonUpload from "~icons/carbon/upload";
|
12 |
|
13 |
import { useSettingsStore } from "$lib/stores/settings";
|
14 |
+
import { isHuggingChat } from "$lib/utils/isHuggingChat";
|
15 |
|
16 |
type ActionData = {
|
17 |
error: boolean;
|
|
|
73 |
let deleteExistingAvatar = false;
|
74 |
|
75 |
let loading = false;
|
76 |
+
|
77 |
+
let ragMode: false | "links" | "domains" | "all" = assistant?.rag?.allowAllDomains
|
78 |
+
? "all"
|
79 |
+
: assistant?.rag?.allowedLinks?.length ?? 0 > 0
|
80 |
+
? "links"
|
81 |
+
: (assistant?.rag?.allowedDomains?.length ?? 0) > 0
|
82 |
+
? "domains"
|
83 |
+
: false;
|
84 |
</script>
|
85 |
|
86 |
<form
|
|
|
113 |
}
|
114 |
}
|
115 |
|
116 |
+
formData.delete("ragMode");
|
117 |
+
|
118 |
+
if (ragMode === false || !$page.data.enableAssistantsRAG) {
|
119 |
+
formData.set("ragAllowAll", "false");
|
120 |
+
formData.set("ragLinkList", "");
|
121 |
+
formData.set("ragDomainList", "");
|
122 |
+
} else if (ragMode === "all") {
|
123 |
+
formData.set("ragAllowAll", "true");
|
124 |
+
formData.set("ragLinkList", "");
|
125 |
+
formData.set("ragDomainList", "");
|
126 |
+
} else if (ragMode === "links") {
|
127 |
+
formData.set("ragAllowAll", "false");
|
128 |
+
formData.set("ragDomainList", "");
|
129 |
+
} else if (ragMode === "domains") {
|
130 |
+
formData.set("ragAllowAll", "false");
|
131 |
+
formData.set("ragLinkList", "");
|
132 |
+
}
|
133 |
+
|
134 |
return async ({ result }) => {
|
135 |
loading = false;
|
136 |
await applyAction(result);
|
|
|
154 |
{/if}
|
155 |
|
156 |
<div class="grid h-full w-full flex-1 grid-cols-2 gap-6 text-sm max-sm:grid-cols-1">
|
157 |
+
<div class="col-span-1 flex flex-col gap-4">
|
158 |
<div>
|
159 |
<div class="mb-1 block pb-2 text-sm font-semibold">Avatar</div>
|
160 |
<input
|
|
|
283 |
</div>
|
284 |
<p class="text-xs text-red-500">{getError("inputMessage1", form)}</p>
|
285 |
</label>
|
286 |
+
{#if $page.data.enableAssistantsRAG}
|
287 |
+
<div class="mb-4 flex flex-col flex-nowrap">
|
288 |
+
<span class="mt-2 text-smd font-semibold"
|
289 |
+
>Internet access <span
|
290 |
+
class="ml-1 rounded bg-gray-100 px-1 py-0.5 text-xxs font-normal text-gray-600"
|
291 |
+
>Experimental</span
|
292 |
+
>
|
293 |
+
|
294 |
+
{#if isHuggingChat}
|
295 |
+
<a
|
296 |
+
href="https://huggingface.co/spaces/huggingchat/chat-ui/discussions/385"
|
297 |
+
target="_blank"
|
298 |
+
class="ml-0.5 rounded bg-gray-100 px-1 py-0.5 text-xxs font-normal text-gray-700 underline decoration-gray-400"
|
299 |
+
>Give feedback</a
|
300 |
+
>
|
301 |
+
{/if}
|
302 |
+
</span>
|
303 |
+
|
304 |
+
<label class="mt-1">
|
305 |
+
<input
|
306 |
+
checked={!ragMode}
|
307 |
+
on:change={() => (ragMode = false)}
|
308 |
+
type="radio"
|
309 |
+
name="ragMode"
|
310 |
+
value={false}
|
311 |
+
/>
|
312 |
+
<span class="my-2 text-sm" class:font-semibold={!ragMode}> Disabled </span>
|
313 |
+
{#if !ragMode}
|
314 |
+
<span class="block text-xs text-gray-500">
|
315 |
+
Assistant won't look for information from Internet and will be faster to answer.
|
316 |
+
</span>
|
317 |
+
{/if}
|
318 |
+
</label>
|
319 |
+
|
320 |
+
<label class="mt-1">
|
321 |
+
<input
|
322 |
+
checked={ragMode === "all"}
|
323 |
+
on:change={() => (ragMode = "all")}
|
324 |
+
type="radio"
|
325 |
+
name="ragMode"
|
326 |
+
value={"all"}
|
327 |
+
/>
|
328 |
+
<span class="my-2 text-sm" class:font-semibold={ragMode === "all"}> Enabled </span>
|
329 |
+
{#if ragMode === "all"}
|
330 |
+
<span class="block text-xs text-gray-500">
|
331 |
+
Assistant will do a web search on each user request to find information.
|
332 |
+
</span>
|
333 |
+
{/if}
|
334 |
+
</label>
|
335 |
+
|
336 |
+
<label class="mt-1">
|
337 |
+
<input
|
338 |
+
checked={ragMode === "domains"}
|
339 |
+
on:change={() => (ragMode = "domains")}
|
340 |
+
type="radio"
|
341 |
+
name="ragMode"
|
342 |
+
value={false}
|
343 |
+
/>
|
344 |
+
<span class="my-2 text-sm" class:font-semibold={ragMode === "domains"}>
|
345 |
+
Domains search
|
346 |
+
</span>
|
347 |
+
</label>
|
348 |
+
{#if ragMode === "domains"}
|
349 |
+
<span class="mb-2 text-xs text-gray-500">
|
350 |
+
Specify domains and URLs that the application can search, separated by commas.
|
351 |
+
</span>
|
352 |
+
|
353 |
+
<input
|
354 |
+
name="ragDomainList"
|
355 |
+
class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
|
356 |
+
placeholder="wikipedia.org,bbc.com"
|
357 |
+
value={assistant?.rag?.allowedDomains?.join(",") ?? ""}
|
358 |
+
/>
|
359 |
+
<p class="text-xs text-red-500">{getError("ragDomainList", form)}</p>
|
360 |
+
{/if}
|
361 |
+
|
362 |
+
<label class="mt-1">
|
363 |
+
<input
|
364 |
+
checked={ragMode === "links"}
|
365 |
+
on:change={() => (ragMode = "links")}
|
366 |
+
type="radio"
|
367 |
+
name="ragMode"
|
368 |
+
value={false}
|
369 |
+
/>
|
370 |
+
<span class="my-2 text-sm" class:font-semibold={ragMode === "links"}>
|
371 |
+
Specific Links
|
372 |
+
</span>
|
373 |
+
</label>
|
374 |
+
{#if ragMode === "links"}
|
375 |
+
<span class="mb-2 text-xs text-gray-500">
|
376 |
+
Specify a maximum of 10 direct URLs that the Assistant will access. HTML & Plain Text
|
377 |
+
only, separated by commas
|
378 |
+
</span>
|
379 |
+
<input
|
380 |
+
name="ragLinkList"
|
381 |
+
class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
|
382 |
+
placeholder="https://raw.githubusercontent.com/huggingface/chat-ui/main/README.md"
|
383 |
+
value={assistant?.rag?.allowedLinks.join(",") ?? ""}
|
384 |
+
/>
|
385 |
+
<p class="text-xs text-red-500">{getError("ragLinkList", form)}</p>
|
386 |
+
{/if}
|
387 |
+
</div>
|
388 |
+
{/if}
|
389 |
</div>
|
390 |
|
391 |
+
<div class="col-span-1 flex h-full flex-col">
|
392 |
+
<span class="mb-1 text-sm font-semibold"> Instructions (system prompt) </span>
|
393 |
<textarea
|
394 |
name="preprompt"
|
395 |
+
class="mb-20 min-h-[8lh] flex-1 rounded-lg border-2 border-gray-200 bg-gray-100 p-2 text-sm"
|
396 |
placeholder="You'll act as..."
|
397 |
value={assistant?.preprompt ?? ""}
|
398 |
/>
|
399 |
<p class="text-xs text-red-500">{getError("preprompt", form)}</p>
|
400 |
+
</div>
|
401 |
</div>
|
402 |
|
403 |
+
<div
|
404 |
+
class="ml-auto mt-6 flex w-fit justify-end gap-2 max-sm:fixed max-sm:bottom-6 max-sm:right-6"
|
405 |
+
>
|
406 |
<a
|
407 |
href={assistant ? `${base}/settings/assistants/${assistant?._id}` : `${base}/settings`}
|
408 |
class="flex items-center justify-center rounded-full bg-gray-200 px-5 py-2 font-semibold text-gray-600"
|
@@ -3,13 +3,26 @@
|
|
3 |
import IconGear from "~icons/bi/gear-fill";
|
4 |
import { base } from "$app/paths";
|
5 |
import type { Assistant } from "$lib/types/Assistant";
|
|
|
6 |
|
7 |
export let assistant: Pick<
|
8 |
Assistant,
|
9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
>;
|
11 |
|
12 |
const dispatch = createEventDispatcher<{ message: string }>();
|
|
|
|
|
|
|
|
|
|
|
13 |
</script>
|
14 |
|
15 |
<div class="flex h-full w-full flex-col content-center items-center justify-center pb-52">
|
@@ -17,7 +30,7 @@
|
|
17 |
class="relative mt-auto rounded-2xl bg-gray-100 text-gray-600 dark:border-gray-800 dark:bg-gray-800/60 dark:text-gray-300"
|
18 |
>
|
19 |
<div
|
20 |
-
class="flex min-w-[80dvw] items-center gap-4 p-4 pr-1 sm:min-w-[440px] md:p-8 md:pt-10 xl:gap-8"
|
21 |
>
|
22 |
{#if assistant.avatar}
|
23 |
<img
|
@@ -39,9 +52,21 @@
|
|
39 |
<p class="-mb-1">Assistant</p>
|
40 |
|
41 |
<p class="text-xl font-bold sm:text-2xl">{assistant.name}</p>
|
42 |
-
|
43 |
-
|
44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
45 |
|
46 |
{#if assistant.createdByName}
|
47 |
<p class="pt-2 text-sm text-gray-400 dark:text-gray-500">
|
@@ -55,6 +80,7 @@
|
|
55 |
{/if}
|
56 |
</div>
|
57 |
</div>
|
|
|
58 |
<div class="absolute right-3 top-3 md:right-4 md:top-4">
|
59 |
<a
|
60 |
href="{base}/settings/assistants/{assistant._id.toString()}"
|
|
|
3 |
import IconGear from "~icons/bi/gear-fill";
|
4 |
import { base } from "$app/paths";
|
5 |
import type { Assistant } from "$lib/types/Assistant";
|
6 |
+
import IconInternet from "../icons/IconInternet.svelte";
|
7 |
|
8 |
export let assistant: Pick<
|
9 |
Assistant,
|
10 |
+
| "avatar"
|
11 |
+
| "name"
|
12 |
+
| "rag"
|
13 |
+
| "modelId"
|
14 |
+
| "createdByName"
|
15 |
+
| "exampleInputs"
|
16 |
+
| "_id"
|
17 |
+
| "description"
|
18 |
>;
|
19 |
|
20 |
const dispatch = createEventDispatcher<{ message: string }>();
|
21 |
+
|
22 |
+
$: hasRag =
|
23 |
+
assistant?.rag?.allowAllDomains ||
|
24 |
+
(assistant?.rag?.allowedDomains?.length ?? 0) > 0 ||
|
25 |
+
(assistant?.rag?.allowedLinks?.length ?? 0) > 0;
|
26 |
</script>
|
27 |
|
28 |
<div class="flex h-full w-full flex-col content-center items-center justify-center pb-52">
|
|
|
30 |
class="relative mt-auto rounded-2xl bg-gray-100 text-gray-600 dark:border-gray-800 dark:bg-gray-800/60 dark:text-gray-300"
|
31 |
>
|
32 |
<div
|
33 |
+
class="mt-3 flex min-w-[80dvw] items-center gap-4 p-4 pr-1 sm:min-w-[440px] md:p-8 md:pt-10 xl:gap-8"
|
34 |
>
|
35 |
{#if assistant.avatar}
|
36 |
<img
|
|
|
52 |
<p class="-mb-1">Assistant</p>
|
53 |
|
54 |
<p class="text-xl font-bold sm:text-2xl">{assistant.name}</p>
|
55 |
+
{#if assistant.description}
|
56 |
+
<p class="line-clamp-6 text-sm text-gray-500 dark:text-gray-400">
|
57 |
+
{assistant.description}
|
58 |
+
</p>
|
59 |
+
{/if}
|
60 |
+
|
61 |
+
{#if hasRag}
|
62 |
+
<div
|
63 |
+
class="flex h-5 w-fit items-center gap-1 rounded-full bg-blue-500/10 pl-1 pr-2 text-xs"
|
64 |
+
title="This assistant uses the websearch."
|
65 |
+
>
|
66 |
+
<IconInternet classNames="text-sm text-blue-600" />
|
67 |
+
Has internet access
|
68 |
+
</div>
|
69 |
+
{/if}
|
70 |
|
71 |
{#if assistant.createdByName}
|
72 |
<p class="pt-2 text-sm text-gray-400 dark:text-gray-500">
|
|
|
80 |
{/if}
|
81 |
</div>
|
82 |
</div>
|
83 |
+
|
84 |
<div class="absolute right-3 top-3 md:right-4 md:top-4">
|
85 |
<a
|
86 |
href="{base}/settings/assistants/{assistant._id.toString()}"
|
@@ -138,7 +138,7 @@
|
|
138 |
bind:this={chatContainer}
|
139 |
>
|
140 |
<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">
|
141 |
-
{#if $page.data?.assistant}
|
142 |
<a
|
143 |
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"
|
144 |
href="{base}/settings/assistants/{$page.data.assistant._id}"
|
|
|
138 |
bind:this={chatContainer}
|
139 |
>
|
140 |
<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">
|
141 |
+
{#if $page.data?.assistant && !!messages.length}
|
142 |
<a
|
143 |
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"
|
144 |
href="{base}/settings/assistants/{$page.data.assistant._id}"
|
@@ -10,6 +10,7 @@
|
|
10 |
role="img"
|
11 |
width="1em"
|
12 |
height="1em"
|
|
|
13 |
preserveAspectRatio="xMidYMid meet"
|
14 |
viewBox="0 0 20 20"
|
15 |
>
|
|
|
10 |
role="img"
|
11 |
width="1em"
|
12 |
height="1em"
|
13 |
+
fill="currentColor"
|
14 |
preserveAspectRatio="xMidYMid meet"
|
15 |
viewBox="0 0 20 20"
|
16 |
>
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { isURLLocal } from "./isURLLocal";
|
2 |
+
import { describe, expect, it } from "vitest";
|
3 |
+
|
4 |
+
describe("isURLLocal", async () => {
|
5 |
+
it("should return true for localhost", async () => {
|
6 |
+
expect(await isURLLocal(new URL("http://localhost"))).toBe(true);
|
7 |
+
});
|
8 |
+
it("should return true for 127.0.0.1", async () => {
|
9 |
+
expect(await isURLLocal(new URL("http://127.0.0.1"))).toBe(true);
|
10 |
+
});
|
11 |
+
it("should return true for 127.254.254.254", async () => {
|
12 |
+
expect(await isURLLocal(new URL("http://127.254.254.254"))).toBe(true);
|
13 |
+
});
|
14 |
+
it("should return false for huggingface.co", async () => {
|
15 |
+
expect(await isURLLocal(new URL("https://huggingface.co/"))).toBe(false);
|
16 |
+
});
|
17 |
+
it("should return true for 127.0.0.1.nip.io", async () => {
|
18 |
+
expect(await isURLLocal(new URL("http://127.0.0.1.nip.io"))).toBe(true);
|
19 |
+
});
|
20 |
+
it("should fail on ipv6", async () => {
|
21 |
+
await expect(isURLLocal(new URL("http://[::1]"))).rejects.toThrow();
|
22 |
+
});
|
23 |
+
it("should fail on ipv6 --1.sslip.io", async () => {
|
24 |
+
await expect(isURLLocal(new URL("http://--1.sslip.io"))).rejects.toThrow();
|
25 |
+
});
|
26 |
+
it("should fail on invalid domain names", async () => {
|
27 |
+
await expect(
|
28 |
+
isURLLocal(new URL("http://34329487239847329874923948732984.com/"))
|
29 |
+
).rejects.toThrow();
|
30 |
+
});
|
31 |
+
});
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Address6, Address4 } from "ip-address";
|
2 |
+
|
3 |
+
import dns from "node:dns";
|
4 |
+
|
5 |
+
export async function isURLLocal(URL: URL): Promise<boolean> {
|
6 |
+
const isLocal = new Promise<boolean>((resolve, reject) => {
|
7 |
+
dns.lookup(URL.hostname, (err, address, family) => {
|
8 |
+
if (err) {
|
9 |
+
reject(err);
|
10 |
+
}
|
11 |
+
if (family === 4) {
|
12 |
+
const addr = new Address4(address);
|
13 |
+
resolve(addr.isInSubnet(new Address4("127.0.0.0/8")));
|
14 |
+
} else if (family === 6) {
|
15 |
+
const addr = new Address6(address);
|
16 |
+
resolve(
|
17 |
+
addr.isLoopback() || addr.isInSubnet(new Address6("::1/128")) || addr.isLinkLocal()
|
18 |
+
);
|
19 |
+
} else {
|
20 |
+
reject(new Error("Unknown IP family"));
|
21 |
+
}
|
22 |
+
});
|
23 |
+
});
|
24 |
+
|
25 |
+
return isLocal;
|
26 |
+
}
|
@@ -1,19 +1,6 @@
|
|
1 |
import type { Message } from "$lib/types/Message";
|
2 |
import { format } from "date-fns";
|
3 |
import { generateFromDefaultEndpoint } from "../generateFromDefaultEndpoint";
|
4 |
-
import { WEBSEARCH_ALLOWLIST, WEBSEARCH_BLOCKLIST } from "$env/static/private";
|
5 |
-
import { z } from "zod";
|
6 |
-
import JSON5 from "json5";
|
7 |
-
|
8 |
-
const listSchema = z.array(z.string()).default([]);
|
9 |
-
|
10 |
-
const allowList = listSchema.parse(JSON5.parse(WEBSEARCH_ALLOWLIST));
|
11 |
-
const blockList = listSchema.parse(JSON5.parse(WEBSEARCH_BLOCKLIST));
|
12 |
-
|
13 |
-
const queryModifier = [
|
14 |
-
...allowList.map((item) => "site:" + item),
|
15 |
-
...blockList.map((item) => "-site:" + item),
|
16 |
-
].join(" ");
|
17 |
|
18 |
export async function generateQuery(messages: Message[]) {
|
19 |
const currentDate = format(new Date(), "MMMM d, yyyy");
|
@@ -79,5 +66,5 @@ Current Question: Where is it being hosted?`,
|
|
79 |
preprompt: `You are tasked with generating web search queries. Give me an appropriate query to answer my question for google search. Answer with only the query. Today is ${currentDate}`,
|
80 |
});
|
81 |
|
82 |
-
return
|
83 |
}
|
|
|
1 |
import type { Message } from "$lib/types/Message";
|
2 |
import { format } from "date-fns";
|
3 |
import { generateFromDefaultEndpoint } from "../generateFromDefaultEndpoint";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
|
5 |
export async function generateQuery(messages: Message[]) {
|
6 |
const currentDate = format(new Date(), "MMMM d, yyyy");
|
|
|
66 |
preprompt: `You are tasked with generating web search queries. Give me an appropriate query to answer my question for google search. Answer with only the query. Today is ${currentDate}`,
|
67 |
});
|
68 |
|
69 |
+
return webQuery.trim();
|
70 |
}
|
@@ -3,30 +3,37 @@ import { JSDOM, VirtualConsole } from "jsdom";
|
|
3 |
export async function parseWeb(url: string) {
|
4 |
const abortController = new AbortController();
|
5 |
setTimeout(() => abortController.abort(), 10000);
|
6 |
-
const
|
7 |
-
.then((response) => response.text())
|
8 |
-
.catch();
|
9 |
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
|
|
14 |
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
|
28 |
-
|
29 |
-
|
30 |
|
31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
32 |
}
|
|
|
3 |
export async function parseWeb(url: string) {
|
4 |
const abortController = new AbortController();
|
5 |
setTimeout(() => abortController.abort(), 10000);
|
6 |
+
const r = await fetch(url, { signal: abortController.signal, credentials: "omit" }).catch();
|
|
|
|
|
7 |
|
8 |
+
if (r.headers.get("content-type")?.includes("text/html")) {
|
9 |
+
const virtualConsole = new VirtualConsole();
|
10 |
+
virtualConsole.on("error", () => {
|
11 |
+
// No-op to skip console errors.
|
12 |
+
});
|
13 |
|
14 |
+
// put the html string into a DOM
|
15 |
+
const dom = new JSDOM((await r.text()) ?? "", {
|
16 |
+
virtualConsole,
|
17 |
+
});
|
18 |
|
19 |
+
const { document } = dom.window;
|
20 |
+
const paragraphs = document.querySelectorAll("p, table, pre, ul, ol");
|
21 |
+
|
22 |
+
if (!paragraphs.length) {
|
23 |
+
throw new Error(`webpage doesn't have any parseable element`);
|
24 |
+
}
|
25 |
+
const paragraphTexts = Array.from(paragraphs).map((p) => p.textContent);
|
26 |
|
27 |
+
// combine text contents from paragraphs and then remove newlines and multiple spaces
|
28 |
+
const text = paragraphTexts.join(" ").replace(/ {2}|\r\n|\n|\r/gm, "");
|
29 |
|
30 |
+
return text;
|
31 |
+
} else if (
|
32 |
+
r.headers.get("content-type")?.includes("text/plain") ||
|
33 |
+
r.headers.get("content-type")?.includes("text/markdown")
|
34 |
+
) {
|
35 |
+
return r.text();
|
36 |
+
} else {
|
37 |
+
throw new Error("Unsupported content type");
|
38 |
+
}
|
39 |
}
|
@@ -1,24 +1,35 @@
|
|
1 |
import { searchWeb } from "$lib/server/websearch/searchWeb";
|
2 |
-
import type { Message } from "$lib/types/Message";
|
3 |
-
import type { WebSearch, WebSearchSource } from "$lib/types/WebSearch";
|
4 |
import { generateQuery } from "$lib/server/websearch/generateQuery";
|
5 |
import { parseWeb } from "$lib/server/websearch/parseWeb";
|
6 |
import { chunk } from "$lib/utils/chunk";
|
7 |
import { findSimilarSentences } from "$lib/server/sentenceSimilarity";
|
8 |
-
import type { Conversation } from "$lib/types/Conversation";
|
9 |
-
import type { MessageUpdate } from "$lib/types/MessageUpdate";
|
10 |
import { getWebSearchProvider } from "./searchWeb";
|
11 |
import { defaultEmbeddingModel, embeddingModels } from "$lib/server/embeddingModels";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
|
13 |
const MAX_N_PAGES_SCRAPE = 10 as const;
|
14 |
const MAX_N_PAGES_EMBED = 5 as const;
|
15 |
|
16 |
-
const
|
|
|
|
|
|
|
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 = {
|
@@ -36,26 +47,66 @@ export async function runWebSearch(
|
|
36 |
}
|
37 |
|
38 |
try {
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
56 |
webSearch.results = webSearch.results.filter((value) => value !== null);
|
57 |
webSearch.results = webSearch.results
|
58 |
-
.filter(({ link }) => !
|
59 |
.slice(0, MAX_N_PAGES_SCRAPE); // limit to first 10 links only
|
60 |
|
61 |
// fetch the model
|
|
|
1 |
import { searchWeb } from "$lib/server/websearch/searchWeb";
|
|
|
|
|
2 |
import { generateQuery } from "$lib/server/websearch/generateQuery";
|
3 |
import { parseWeb } from "$lib/server/websearch/parseWeb";
|
4 |
import { chunk } from "$lib/utils/chunk";
|
5 |
import { findSimilarSentences } from "$lib/server/sentenceSimilarity";
|
|
|
|
|
6 |
import { getWebSearchProvider } from "./searchWeb";
|
7 |
import { defaultEmbeddingModel, embeddingModels } from "$lib/server/embeddingModels";
|
8 |
+
import { WEBSEARCH_ALLOWLIST, WEBSEARCH_BLOCKLIST, ENABLE_LOCAL_FETCH } from "$env/static/private";
|
9 |
+
|
10 |
+
import type { Conversation } from "$lib/types/Conversation";
|
11 |
+
import type { MessageUpdate } from "$lib/types/MessageUpdate";
|
12 |
+
import type { Message } from "$lib/types/Message";
|
13 |
+
import type { WebSearch, WebSearchSource } from "$lib/types/WebSearch";
|
14 |
+
import type { Assistant } from "$lib/types/Assistant";
|
15 |
+
|
16 |
+
import { z } from "zod";
|
17 |
+
import JSON5 from "json5";
|
18 |
+
import { isURLLocal } from "../isURLLocal";
|
19 |
|
20 |
const MAX_N_PAGES_SCRAPE = 10 as const;
|
21 |
const MAX_N_PAGES_EMBED = 5 as const;
|
22 |
|
23 |
+
const listSchema = z.array(z.string()).default([]);
|
24 |
+
|
25 |
+
const allowList = listSchema.parse(JSON5.parse(WEBSEARCH_ALLOWLIST));
|
26 |
+
const blockList = listSchema.parse(JSON5.parse(WEBSEARCH_BLOCKLIST));
|
27 |
|
28 |
export async function runWebSearch(
|
29 |
conv: Conversation,
|
30 |
messages: Message[],
|
31 |
+
updatePad: (upd: MessageUpdate) => void,
|
32 |
+
ragSettings?: Assistant["rag"]
|
33 |
) {
|
34 |
const prompt = messages[messages.length - 1].content;
|
35 |
const webSearch: WebSearch = {
|
|
|
47 |
}
|
48 |
|
49 |
try {
|
50 |
+
// if the assistant specified direct links, skip the websearch
|
51 |
+
if (ragSettings && ragSettings?.allowedLinks.length > 0) {
|
52 |
+
appendUpdate("Using links specified in Assistant");
|
53 |
+
|
54 |
+
let linksToUse = [...ragSettings.allowedLinks];
|
55 |
+
|
56 |
+
if (ENABLE_LOCAL_FETCH !== "true") {
|
57 |
+
const localLinks = await Promise.all(
|
58 |
+
linksToUse.map(async (link) => {
|
59 |
+
try {
|
60 |
+
const url = new URL(link);
|
61 |
+
return await isURLLocal(url);
|
62 |
+
} catch (e) {
|
63 |
+
return true;
|
64 |
+
}
|
65 |
+
})
|
66 |
+
);
|
67 |
+
|
68 |
+
linksToUse = linksToUse.filter((_, index) => !localLinks[index]);
|
69 |
+
}
|
70 |
+
|
71 |
+
webSearch.results = linksToUse.map((link) => {
|
72 |
+
return { link, hostname: new URL(link).hostname, title: "", text: "" };
|
73 |
+
});
|
74 |
+
} else {
|
75 |
+
webSearch.searchQuery = await generateQuery(messages);
|
76 |
+
const searchProvider = getWebSearchProvider();
|
77 |
+
appendUpdate(`Searching ${searchProvider}`, [webSearch.searchQuery]);
|
78 |
+
|
79 |
+
if (ragSettings && ragSettings?.allowedDomains.length > 0) {
|
80 |
+
appendUpdate("Filtering on specified domains");
|
81 |
+
webSearch.searchQuery +=
|
82 |
+
" " + ragSettings.allowedDomains.map((item) => "site:" + item).join(" ");
|
83 |
+
}
|
84 |
+
|
85 |
+
// handle the global lists
|
86 |
+
webSearch.searchQuery +=
|
87 |
+
allowList.map((item) => "site:" + item).join(" ") +
|
88 |
+
" " +
|
89 |
+
blockList.map((item) => "-site:" + item).join(" ");
|
90 |
+
|
91 |
+
const results = await searchWeb(webSearch.searchQuery);
|
92 |
+
webSearch.results =
|
93 |
+
(results.organic_results &&
|
94 |
+
results.organic_results.map((el: { title?: string; link: string; text?: string }) => {
|
95 |
+
try {
|
96 |
+
const { title, link, text } = el;
|
97 |
+
const { hostname } = new URL(link);
|
98 |
+
return { title, link, hostname, text };
|
99 |
+
} catch (e) {
|
100 |
+
// Ignore Errors
|
101 |
+
return null;
|
102 |
+
}
|
103 |
+
})) ??
|
104 |
+
[];
|
105 |
+
}
|
106 |
+
|
107 |
webSearch.results = webSearch.results.filter((value) => value !== null);
|
108 |
webSearch.results = webSearch.results
|
109 |
+
.filter(({ link }) => !blockList.some((el) => link.includes(el))) // filter out blocklist links
|
110 |
.slice(0, MAX_N_PAGES_SCRAPE); // limit to first 10 links only
|
111 |
|
112 |
// fetch the model
|
@@ -14,5 +14,10 @@ export interface Assistant extends Timestamps {
|
|
14 |
preprompt: string;
|
15 |
userCount?: number;
|
16 |
featured?: boolean;
|
|
|
|
|
|
|
|
|
|
|
17 |
searchTokens: string[];
|
18 |
}
|
|
|
14 |
preprompt: string;
|
15 |
userCount?: number;
|
16 |
featured?: boolean;
|
17 |
+
rag?: {
|
18 |
+
allowAllDomains: boolean;
|
19 |
+
allowedDomains: string[];
|
20 |
+
allowedLinks: string[];
|
21 |
+
};
|
22 |
searchTokens: string[];
|
23 |
}
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export function parseStringToList(links: unknown): string[] {
|
2 |
+
if (typeof links !== "string") {
|
3 |
+
throw new Error("Expected a string");
|
4 |
+
}
|
5 |
+
|
6 |
+
return links
|
7 |
+
.split(",")
|
8 |
+
.map((link) => link.trim())
|
9 |
+
.filter((link) => link.length > 0);
|
10 |
+
}
|
@@ -14,6 +14,7 @@ import {
|
|
14 |
USE_LOCAL_WEBSEARCH,
|
15 |
SEARXNG_QUERY_URL,
|
16 |
ENABLE_ASSISTANTS,
|
|
|
17 |
} from "$env/static/private";
|
18 |
import { ObjectId } from "mongodb";
|
19 |
import type { ConvSidebar } from "$lib/types/ConvSidebar";
|
@@ -186,6 +187,7 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => {
|
|
186 |
},
|
187 |
assistant,
|
188 |
enableAssistants,
|
|
|
189 |
loginRequired,
|
190 |
loginEnabled: requiresUser,
|
191 |
guestMode: requiresUser && messagesBeforeLogin > 0,
|
|
|
14 |
USE_LOCAL_WEBSEARCH,
|
15 |
SEARXNG_QUERY_URL,
|
16 |
ENABLE_ASSISTANTS,
|
17 |
+
ENABLE_ASSISTANTS_RAG,
|
18 |
} from "$env/static/private";
|
19 |
import { ObjectId } from "mongodb";
|
20 |
import type { ConvSidebar } from "$lib/types/ConvSidebar";
|
|
|
187 |
},
|
188 |
assistant,
|
189 |
enableAssistants,
|
190 |
+
enableAssistantsRAG: ENABLE_ASSISTANTS_RAG === "true",
|
191 |
loginRequired,
|
192 |
loginEnabled: requiresUser,
|
193 |
guestMode: requiresUser && messagesBeforeLogin > 0,
|
@@ -20,6 +20,7 @@
|
|
20 |
import { getHref } from "$lib/utils/getHref";
|
21 |
import { debounce } from "$lib/utils/debounce";
|
22 |
import { useSettingsStore } from "$lib/stores/settings";
|
|
|
23 |
import { isDesktop } from "$lib/utils/isDesktop";
|
24 |
|
25 |
export let data: PageData;
|
@@ -201,6 +202,11 @@
|
|
201 |
|
202 |
<div class="mt-8 grid grid-cols-2 gap-3 sm:gap-5 md:grid-cols-3 lg:grid-cols-4">
|
203 |
{#each data.assistants as assistant (assistant._id)}
|
|
|
|
|
|
|
|
|
|
|
204 |
<button
|
205 |
class="relative flex flex-col items-center justify-center overflow-hidden text-balance rounded-xl border bg-gray-50/50 px-4 py-6 text-center shadow hover:bg-gray-50 hover:shadow-inner max-sm:px-4 sm:h-64 sm:pb-4 xl:pt-8 dark:border-gray-800/70 dark:bg-gray-950/20 dark:hover:bg-gray-950/40"
|
206 |
on:click={() => {
|
@@ -220,6 +226,16 @@
|
|
220 |
<CarbonUserMultiple class="text-xxs" />{formatUserCount(assistant.userCount)}
|
221 |
</div>
|
222 |
{/if}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
223 |
{#if assistant.avatar}
|
224 |
<img
|
225 |
src="{base}/settings/assistants/{assistant._id}/avatar.jpg"
|
|
|
20 |
import { getHref } from "$lib/utils/getHref";
|
21 |
import { debounce } from "$lib/utils/debounce";
|
22 |
import { useSettingsStore } from "$lib/stores/settings";
|
23 |
+
import IconInternet from "$lib/components/icons/IconInternet.svelte";
|
24 |
import { isDesktop } from "$lib/utils/isDesktop";
|
25 |
|
26 |
export let data: PageData;
|
|
|
202 |
|
203 |
<div class="mt-8 grid grid-cols-2 gap-3 sm:gap-5 md:grid-cols-3 lg:grid-cols-4">
|
204 |
{#each data.assistants as assistant (assistant._id)}
|
205 |
+
{@const hasRag =
|
206 |
+
assistant?.rag?.allowAllDomains ||
|
207 |
+
!!assistant?.rag?.allowedDomains?.length ||
|
208 |
+
!!assistant?.rag?.allowedLinks?.length}
|
209 |
+
|
210 |
<button
|
211 |
class="relative flex flex-col items-center justify-center overflow-hidden text-balance rounded-xl border bg-gray-50/50 px-4 py-6 text-center shadow hover:bg-gray-50 hover:shadow-inner max-sm:px-4 sm:h-64 sm:pb-4 xl:pt-8 dark:border-gray-800/70 dark:bg-gray-950/20 dark:hover:bg-gray-950/40"
|
212 |
on:click={() => {
|
|
|
226 |
<CarbonUserMultiple class="text-xxs" />{formatUserCount(assistant.userCount)}
|
227 |
</div>
|
228 |
{/if}
|
229 |
+
|
230 |
+
{#if hasRag}
|
231 |
+
<div
|
232 |
+
class="absolute left-3 top-3 grid size-5 place-items-center rounded-full bg-blue-500/10"
|
233 |
+
title="This assistant uses the websearch."
|
234 |
+
>
|
235 |
+
<IconInternet classNames="text-sm text-blue-600" />
|
236 |
+
</div>
|
237 |
+
{/if}
|
238 |
+
|
239 |
{#if assistant.avatar}
|
240 |
<img
|
241 |
src="{base}/settings/assistants/{assistant._id}/avatar.jpg"
|
@@ -1,4 +1,4 @@
|
|
1 |
-
import { MESSAGES_BEFORE_LOGIN } from "$env/static/private";
|
2 |
import { authCondition, requiresUser } from "$lib/server/auth";
|
3 |
import { collections } from "$lib/server/database";
|
4 |
import { models } from "$lib/server/models";
|
@@ -13,6 +13,7 @@ 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";
|
@@ -334,9 +335,22 @@ export async function POST({ request, locals, params, getClientAddress }) {
|
|
334 |
}
|
335 |
);
|
336 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
337 |
// perform websearch if needed
|
338 |
-
if (
|
339 |
-
messageToWriteTo.webSearch = await runWebSearch(conv, messagesForPrompt, update);
|
340 |
}
|
341 |
|
342 |
// inject websearch result & optionally images into the messages
|
|
|
1 |
+
import { MESSAGES_BEFORE_LOGIN, ENABLE_ASSISTANTS_RAG } from "$env/static/private";
|
2 |
import { authCondition, requiresUser } from "$lib/server/auth";
|
3 |
import { collections } from "$lib/server/database";
|
4 |
import { models } from "$lib/server/models";
|
|
|
13 |
import { summarize } from "$lib/server/summarize";
|
14 |
import { uploadFile } from "$lib/server/files/uploadFile";
|
15 |
import sizeof from "image-size";
|
16 |
+
import type { Assistant } from "$lib/types/Assistant";
|
17 |
import { convertLegacyConversation } from "$lib/utils/tree/convertLegacyConversation";
|
18 |
import { isMessageId } from "$lib/utils/tree/isMessageId";
|
19 |
import { buildSubtree } from "$lib/utils/tree/buildSubtree.js";
|
|
|
335 |
}
|
336 |
);
|
337 |
|
338 |
+
// check if assistant has a rag
|
339 |
+
const rag = (
|
340 |
+
await collections.assistants.findOne<Pick<Assistant, "rag">>(
|
341 |
+
{ _id: conv.assistantId },
|
342 |
+
{ projection: { rag: 1 } }
|
343 |
+
)
|
344 |
+
)?.rag;
|
345 |
+
|
346 |
+
const assistantHasRAG =
|
347 |
+
ENABLE_ASSISTANTS_RAG === "true" &&
|
348 |
+
rag &&
|
349 |
+
(rag.allowedLinks.length > 0 || rag.allowedDomains.length > 0 || rag.allowAllDomains);
|
350 |
+
|
351 |
// perform websearch if needed
|
352 |
+
if (!isContinue && ((webSearch && !conv.assistantId) || assistantHasRAG)) {
|
353 |
+
messageToWriteTo.webSearch = await runWebSearch(conv, messagesForPrompt, update, rag);
|
354 |
}
|
355 |
|
356 |
// inject websearch result & optionally images into the messages
|
@@ -32,7 +32,7 @@
|
|
32 |
</script>
|
33 |
|
34 |
<div
|
35 |
-
class="grid h-full w-full grid-cols-1 grid-rows-[auto,1fr] content-start gap-x-
|
36 |
>
|
37 |
<div class="col-span-1 mb-4 flex items-center justify-between md:col-span-3">
|
38 |
<h2 class="text-xl font-bold">Settings</h2>
|
|
|
32 |
</script>
|
33 |
|
34 |
<div
|
35 |
+
class="grid h-full w-full grid-cols-1 grid-rows-[auto,1fr] content-start gap-x-4 overflow-hidden p-4 md:grid-cols-3 md:grid-rows-[auto,1fr] md:p-8"
|
36 |
>
|
37 |
<div class="col-span-1 mb-4 flex items-center justify-between md:col-span-3">
|
38 |
<h2 class="text-xl font-bold">Settings</h2>
|
@@ -13,6 +13,7 @@
|
|
13 |
import CarbonLink from "~icons/carbon/link";
|
14 |
import CopyToClipBoardBtn from "$lib/components/CopyToClipBoardBtn.svelte";
|
15 |
import ReportModal from "./ReportModal.svelte";
|
|
|
16 |
|
17 |
export let data: PageData;
|
18 |
|
@@ -27,6 +28,11 @@
|
|
27 |
$: shareUrl = `${prefix}/assistant/${assistant?._id}`;
|
28 |
|
29 |
let displayReportModal = false;
|
|
|
|
|
|
|
|
|
|
|
30 |
</script>
|
31 |
|
32 |
{#if displayReportModal}
|
@@ -51,10 +57,19 @@
|
|
51 |
|
52 |
<div class="flex-1">
|
53 |
<div class="mb-1.5">
|
54 |
-
<h1 class="mr-
|
55 |
{assistant?.name}
|
56 |
</h1>
|
57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
58 |
>public</span
|
59 |
>
|
60 |
</div>
|
@@ -147,11 +162,48 @@
|
|
147 |
</div>
|
148 |
</div>
|
149 |
|
150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
151 |
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
157 |
</div>
|
|
|
13 |
import CarbonLink from "~icons/carbon/link";
|
14 |
import CopyToClipBoardBtn from "$lib/components/CopyToClipBoardBtn.svelte";
|
15 |
import ReportModal from "./ReportModal.svelte";
|
16 |
+
import IconInternet from "$lib/components/icons/IconInternet.svelte";
|
17 |
|
18 |
export let data: PageData;
|
19 |
|
|
|
28 |
$: shareUrl = `${prefix}/assistant/${assistant?._id}`;
|
29 |
|
30 |
let displayReportModal = false;
|
31 |
+
|
32 |
+
$: hasRag =
|
33 |
+
assistant?.rag?.allowAllDomains ||
|
34 |
+
!!assistant?.rag?.allowedDomains?.length ||
|
35 |
+
!!assistant?.rag?.allowedLinks?.length;
|
36 |
</script>
|
37 |
|
38 |
{#if displayReportModal}
|
|
|
57 |
|
58 |
<div class="flex-1">
|
59 |
<div class="mb-1.5">
|
60 |
+
<h1 class="mr-1 inline text-xl font-semibold">
|
61 |
{assistant?.name}
|
62 |
</h1>
|
63 |
+
|
64 |
+
{#if hasRag}
|
65 |
+
<span
|
66 |
+
class="inline-grid size-5 place-items-center rounded-full bg-blue-500/10"
|
67 |
+
title="This assistant uses the websearch."
|
68 |
+
>
|
69 |
+
<IconInternet classNames="text-sm text-blue-600" />
|
70 |
+
</span>
|
71 |
+
{/if}
|
72 |
+
<span class="ml-1 rounded-full border px-2 py-0.5 text-sm leading-none text-gray-500"
|
73 |
>public</span
|
74 |
>
|
75 |
</div>
|
|
|
162 |
</div>
|
163 |
</div>
|
164 |
|
165 |
+
<!-- two columns for big screen, single column for small screen -->
|
166 |
+
<div class="mb-12 mt-3">
|
167 |
+
<h2 class="mb-2 font-semibold">System Instructions</h2>
|
168 |
+
<textarea
|
169 |
+
disabled
|
170 |
+
class="box-border h-full min-h-[8lh] w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2 disabled:cursor-not-allowed"
|
171 |
+
>{assistant?.preprompt}</textarea
|
172 |
+
>
|
173 |
|
174 |
+
{#if hasRag}
|
175 |
+
<div class="mt-4">
|
176 |
+
<h2 class=" font-semibold">Internet Access</h2>
|
177 |
+
{#if assistant?.rag?.allowAllDomains}
|
178 |
+
<p class="text-sm text-gray-500">
|
179 |
+
This Assistant uses Web Search to find information on Internet.
|
180 |
+
</p>
|
181 |
+
{:else if !!assistant?.rag?.allowedDomains && assistant?.rag?.allowedDomains.length}
|
182 |
+
<p class="pb-4 text-sm text-gray-500">
|
183 |
+
This Assistant can use Web Search on the following domains:
|
184 |
+
</p>
|
185 |
+
<ul class="mr-2 flex flex-wrap gap-2.5 text-sm text-gray-800">
|
186 |
+
{#each assistant?.rag?.allowedDomains as domain}
|
187 |
+
<li
|
188 |
+
class="break-all rounded-lg border border-gray-200 bg-gray-100 px-2 py-0.5 leading-tight decoration-gray-400"
|
189 |
+
>
|
190 |
+
<a target="_blank" class="underline" href={domain}>{domain}</a>
|
191 |
+
</li>
|
192 |
+
{/each}
|
193 |
+
</ul>
|
194 |
+
{:else if !!assistant?.rag?.allowedLinks && assistant?.rag?.allowedLinks.length}
|
195 |
+
<p class="pb-4 text-sm text-gray-500">This Assistant can browse the following links:</p>
|
196 |
+
<ul class="mr-2 flex flex-wrap gap-2.5 text-sm text-gray-800">
|
197 |
+
{#each assistant?.rag?.allowedLinks as link}
|
198 |
+
<li
|
199 |
+
class="break-all rounded-lg border border-gray-200 bg-gray-100 px-2 py-0.5 leading-tight decoration-gray-400"
|
200 |
+
>
|
201 |
+
<a target="_blank" class="underline" href={link}>{link}</a>
|
202 |
+
</li>
|
203 |
+
{/each}
|
204 |
+
</ul>
|
205 |
+
{/if}
|
206 |
+
</div>
|
207 |
+
{/if}
|
208 |
+
</div>
|
209 |
</div>
|
@@ -8,6 +8,7 @@ import { z } from "zod";
|
|
8 |
import { sha256 } from "$lib/utils/sha256";
|
9 |
|
10 |
import sharp from "sharp";
|
|
|
11 |
import { generateSearchTokens } from "$lib/utils/searchTokens";
|
12 |
|
13 |
const newAsssistantSchema = z.object({
|
@@ -20,6 +21,9 @@ const newAsssistantSchema = z.object({
|
|
20 |
exampleInput3: z.string().optional(),
|
21 |
exampleInput4: z.string().optional(),
|
22 |
avatar: z.union([z.instanceof(File), z.literal("null")]).optional(),
|
|
|
|
|
|
|
23 |
});
|
24 |
|
25 |
const uploadAvatar = async (avatar: File, assistantId: ObjectId): Promise<string> => {
|
@@ -131,6 +135,11 @@ export const actions: Actions = {
|
|
131 |
exampleInputs,
|
132 |
avatar: deleteAvatar ? undefined : hash ?? assistant.avatar,
|
133 |
updatedAt: new Date(),
|
|
|
|
|
|
|
|
|
|
|
134 |
searchTokens: generateSearchTokens(parse.data.name),
|
135 |
},
|
136 |
}
|
|
|
8 |
import { sha256 } from "$lib/utils/sha256";
|
9 |
|
10 |
import sharp from "sharp";
|
11 |
+
import { parseStringToList } from "$lib/utils/parseStringToList";
|
12 |
import { generateSearchTokens } from "$lib/utils/searchTokens";
|
13 |
|
14 |
const newAsssistantSchema = z.object({
|
|
|
21 |
exampleInput3: z.string().optional(),
|
22 |
exampleInput4: z.string().optional(),
|
23 |
avatar: z.union([z.instanceof(File), z.literal("null")]).optional(),
|
24 |
+
ragLinkList: z.preprocess(parseStringToList, z.string().url().array().max(10)),
|
25 |
+
ragDomainList: z.preprocess(parseStringToList, z.string().array()),
|
26 |
+
ragAllowAll: z.preprocess((v) => v === "true", z.boolean()),
|
27 |
});
|
28 |
|
29 |
const uploadAvatar = async (avatar: File, assistantId: ObjectId): Promise<string> => {
|
|
|
135 |
exampleInputs,
|
136 |
avatar: deleteAvatar ? undefined : hash ?? assistant.avatar,
|
137 |
updatedAt: new Date(),
|
138 |
+
rag: {
|
139 |
+
allowedLinks: parse.data.ragLinkList,
|
140 |
+
allowedDomains: parse.data.ragDomainList,
|
141 |
+
allowAllDomains: parse.data.ragAllowAll,
|
142 |
+
},
|
143 |
searchTokens: generateSearchTokens(parse.data.name),
|
144 |
},
|
145 |
}
|
@@ -7,6 +7,7 @@ import { ObjectId } from "mongodb";
|
|
7 |
import { z } from "zod";
|
8 |
import { sha256 } from "$lib/utils/sha256";
|
9 |
import sharp from "sharp";
|
|
|
10 |
import { usageLimits } from "$lib/server/usageLimits";
|
11 |
import { generateSearchTokens } from "$lib/utils/searchTokens";
|
12 |
|
@@ -20,6 +21,9 @@ const newAsssistantSchema = z.object({
|
|
20 |
exampleInput3: z.string().optional(),
|
21 |
exampleInput4: z.string().optional(),
|
22 |
avatar: z.instanceof(File).optional(),
|
|
|
|
|
|
|
23 |
});
|
24 |
|
25 |
const uploadAvatar = async (avatar: File, assistantId: ObjectId): Promise<string> => {
|
@@ -113,6 +117,11 @@ export const actions: Actions = {
|
|
113 |
updatedAt: new Date(),
|
114 |
userCount: 1,
|
115 |
featured: false,
|
|
|
|
|
|
|
|
|
|
|
116 |
searchTokens: generateSearchTokens(parse.data.name),
|
117 |
});
|
118 |
|
|
|
7 |
import { z } from "zod";
|
8 |
import { sha256 } from "$lib/utils/sha256";
|
9 |
import sharp from "sharp";
|
10 |
+
import { parseStringToList } from "$lib/utils/parseStringToList";
|
11 |
import { usageLimits } from "$lib/server/usageLimits";
|
12 |
import { generateSearchTokens } from "$lib/utils/searchTokens";
|
13 |
|
|
|
21 |
exampleInput3: z.string().optional(),
|
22 |
exampleInput4: z.string().optional(),
|
23 |
avatar: z.instanceof(File).optional(),
|
24 |
+
ragLinkList: z.preprocess(parseStringToList, z.string().url().array().max(10)),
|
25 |
+
ragDomainList: z.preprocess(parseStringToList, z.string().array()),
|
26 |
+
ragAllowAll: z.preprocess((v) => v === "true", z.boolean()),
|
27 |
});
|
28 |
|
29 |
const uploadAvatar = async (avatar: File, assistantId: ObjectId): Promise<string> => {
|
|
|
117 |
updatedAt: new Date(),
|
118 |
userCount: 1,
|
119 |
featured: false,
|
120 |
+
rag: {
|
121 |
+
allowedLinks: parse.data.ragLinkList,
|
122 |
+
allowedDomains: parse.data.ragDomainList,
|
123 |
+
allowAllDomains: parse.data.ragAllowAll,
|
124 |
+
},
|
125 |
searchTokens: generateSearchTokens(parse.data.name),
|
126 |
});
|
127 |
|