nsarrazin HF staff Mishig victor HF staff commited on
Commit
3f5871c
1 Parent(s): eb071be

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 CHANGED
@@ -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
.env.template CHANGED
@@ -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"]`
package-lock.json CHANGED
@@ -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",
package.json CHANGED
@@ -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",
src/lib/components/AssistantSettings.svelte CHANGED
@@ -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
- <label class="flex flex-col">
261
- <div class="mb-1 text-sm font-semibold">Instructions (system prompt)</div>
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
- </label>
270
  </div>
271
 
272
- <div class="mt-6 flex justify-end gap-2">
 
 
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"
src/lib/components/chat/AssistantIntroduction.svelte CHANGED
@@ -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
- "avatar" | "name" | "modelId" | "createdByName" | "exampleInputs" | "_id" | "description"
 
 
 
 
 
 
 
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
- <p class="line-clamp-6 text-sm text-gray-500 dark:text-gray-400">
43
- {assistant.description}
44
- </p>
 
 
 
 
 
 
 
 
 
 
 
 
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()}"
src/lib/components/chat/ChatWindow.svelte CHANGED
@@ -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}"
src/lib/components/icons/IconInternet.svelte CHANGED
@@ -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
  >
src/lib/server/isURLLocal.spec.ts ADDED
@@ -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
+ });
src/lib/server/isURLLocal.ts ADDED
@@ -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
+ }
src/lib/server/websearch/generateQuery.ts CHANGED
@@ -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 (queryModifier + " " + webQuery).trim();
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
  }
src/lib/server/websearch/parseWeb.ts CHANGED
@@ -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 htmlString = await fetch(url, { signal: abortController.signal })
7
- .then((response) => response.text())
8
- .catch();
9
 
10
- const virtualConsole = new VirtualConsole();
11
- virtualConsole.on("error", () => {
12
- // No-op to skip console errors.
13
- });
 
14
 
15
- // put the html string into a DOM
16
- const dom = new JSDOM(htmlString ?? "", {
17
- virtualConsole,
18
- });
19
 
20
- const { document } = dom.window;
21
- const textElTags = "p";
22
- const paragraphs = document.querySelectorAll(textElTags);
23
- if (!paragraphs.length) {
24
- throw new Error(`webpage doesn't have any "${textElTags}" element`);
25
- }
26
- const paragraphTexts = Array.from(paragraphs).map((p) => p.textContent);
27
 
28
- // combine text contents from paragraphs and then remove newlines and multiple spaces
29
- const text = paragraphTexts.join(" ").replace(/ {2}|\r\n|\n|\r/gm, "");
30
 
31
- return text;
 
 
 
 
 
 
 
 
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
  }
src/lib/server/websearch/runWebSearch.ts CHANGED
@@ -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 DOMAIN_BLOCKLIST = ["youtube.com", "twitter.com"];
 
 
 
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
- webSearch.searchQuery = await generateQuery(messages);
40
- const searchProvider = getWebSearchProvider();
41
- appendUpdate(`Searching ${searchProvider}`, [webSearch.searchQuery]);
42
- const results = await searchWeb(webSearch.searchQuery);
43
- webSearch.results =
44
- (results.organic_results &&
45
- results.organic_results.map((el: { title?: string; link: string; text?: string }) => {
46
- try {
47
- const { title, link, text } = el;
48
- const { hostname } = new URL(link);
49
- return { title, link, hostname, text };
50
- } catch (e) {
51
- // Ignore Errors
52
- return null;
53
- }
54
- })) ??
55
- [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  webSearch.results = webSearch.results.filter((value) => value !== null);
57
  webSearch.results = webSearch.results
58
- .filter(({ link }) => !DOMAIN_BLOCKLIST.some((el) => link.includes(el))) // filter out blocklist links
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
src/lib/types/Assistant.ts CHANGED
@@ -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
  }
src/lib/utils/parseStringToList.ts ADDED
@@ -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
+ }
src/routes/+layout.server.ts CHANGED
@@ -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,
src/routes/assistants/+page.svelte CHANGED
@@ -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"
src/routes/conversation/[id]/+server.ts CHANGED
@@ -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 (webSearch && !isContinue && !conv.assistantId) {
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
src/routes/settings/(nav)/+layout.svelte CHANGED
@@ -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-8 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>
 
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>
src/routes/settings/(nav)/assistants/[assistantId]/+page.svelte CHANGED
@@ -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-2 inline text-xl font-semibold">
55
  {assistant?.name}
56
  </h1>
57
- <span class="rounded-full border px-2 py-0.5 text-sm leading-none text-gray-500"
 
 
 
 
 
 
 
 
 
58
  >public</span
59
  >
60
  </div>
@@ -147,11 +162,48 @@
147
  </div>
148
  </div>
149
 
150
- <h2 class="mt-4 text-lg font-semibold">System Instructions</h2>
 
 
 
 
 
 
 
151
 
152
- <textarea
153
- disabled
154
- class="min-h-[8lh] w-full flex-1 rounded-lg border-2 border-gray-200 bg-gray-100 p-2 disabled:cursor-not-allowed 2xl:min-h-[12lh]"
155
- >{assistant?.preprompt}</textarea
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>
src/routes/settings/(nav)/assistants/[assistantId]/edit/+page.server.ts CHANGED
@@ -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
  }
src/routes/settings/(nav)/assistants/new/+page.server.ts CHANGED
@@ -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