nsarrazin HF Staff commited on
Commit
10b8070
·
unverified ·
1 Parent(s): 4f8e8e5

feat(assistants): use community tools in assistants (#1421)

Browse files

* wip: give assistant tool access

* add api endpoints

* add/delete tools from assistant settings

* make tools work with assistants

* feat(tools): add more tools indicator throughout UI

* formatting

src/lib/components/AssistantSettings.svelte CHANGED
@@ -11,6 +11,7 @@
11
  import CarbonUpload from "~icons/carbon/upload";
12
  import CarbonHelpFilled from "~icons/carbon/help";
13
  import CarbonSettingsAdjust from "~icons/carbon/settings-adjust";
 
14
 
15
  import { useSettingsStore } from "$lib/stores/settings";
16
  import { isHuggingChat } from "$lib/utils/isHuggingChat";
@@ -18,6 +19,7 @@
18
  import TokensCounter from "./TokensCounter.svelte";
19
  import HoverTooltip from "./HoverTooltip.svelte";
20
  import { findCurrentModel } from "$lib/utils/models";
 
21
 
22
  type ActionData = {
23
  error: boolean;
@@ -93,7 +95,9 @@
93
  ? "domains"
94
  : false;
95
 
 
96
  const regex = /{{\s?url=(.+?)\s?}}/g;
 
97
  $: templateVariables = [...systemPrompt.matchAll(regex)].map((match) => match[1]);
98
  $: selectedModel = models.find((m) => m.id === modelId);
99
  </script>
@@ -146,6 +150,8 @@
146
  formData.set("ragLinkList", "");
147
  }
148
 
 
 
149
  return async ({ result }) => {
150
  loading = false;
151
  await applyAction(result);
@@ -403,16 +409,27 @@
403
  </div>
404
  <p class="text-xs text-red-500">{getError("inputMessage1", form)}</p>
405
  </label>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
406
  {#if $page.data.enableAssistantsRAG}
407
- <div class="mb-4 flex flex-col flex-nowrap">
408
  <span class="mt-2 text-smd font-semibold"
409
  >Internet access
410
  <IconInternet classNames="inline text-sm text-blue-600" />
411
 
412
- <span class="ml-1 rounded bg-gray-100 px-1 py-0.5 text-xxs font-normal text-gray-600"
413
- >Experimental</span
414
- >
415
-
416
  {#if isHuggingChat}
417
  <a
418
  href="https://huggingface.co/spaces/huggingchat/chat-ui/discussions/385"
@@ -507,26 +524,13 @@
507
  />
508
  <p class="text-xs text-red-500">{getError("ragLinkList", form)}</p>
509
  {/if}
510
-
511
- <!-- divider -->
512
- <div class="my-3 ml-0 mr-6 w-full border border-gray-200" />
513
-
514
- <label class="text-sm has-[:checked]:font-semibold">
515
- <input type="checkbox" name="dynamicPrompt" bind:checked={dynamicPrompt} />
516
- Dynamic Prompt
517
- <p class="mb-2 text-xs font-normal text-gray-500">
518
- Allow the use of template variables {"{{url=https://example.com/path}}"}
519
- to insert dynamic content into your prompt by making GET requests to specified URLs on
520
- each inference.
521
- </p>
522
- </label>
523
  </div>
524
  {/if}
525
  </div>
526
 
527
  <div class="relative col-span-1 flex h-full flex-col">
528
  <div class="mb-1 flex justify-between text-sm">
529
- <span class="font-semibold"> Instructions (System Prompt) </span>
530
  {#if dynamicPrompt && templateVariables.length}
531
  <div class="relative">
532
  <button
@@ -549,6 +553,16 @@
549
  </div>
550
  {/if}
551
  </div>
 
 
 
 
 
 
 
 
 
 
552
  <div class="relative mb-20 flex h-full flex-col gap-2">
553
  <textarea
554
  name="preprompt"
 
11
  import CarbonUpload from "~icons/carbon/upload";
12
  import CarbonHelpFilled from "~icons/carbon/help";
13
  import CarbonSettingsAdjust from "~icons/carbon/settings-adjust";
14
+ import CarbonTools from "~icons/carbon/tools";
15
 
16
  import { useSettingsStore } from "$lib/stores/settings";
17
  import { isHuggingChat } from "$lib/utils/isHuggingChat";
 
19
  import TokensCounter from "./TokensCounter.svelte";
20
  import HoverTooltip from "./HoverTooltip.svelte";
21
  import { findCurrentModel } from "$lib/utils/models";
22
+ import AssistantToolPicker from "./AssistantToolPicker.svelte";
23
 
24
  type ActionData = {
25
  error: boolean;
 
95
  ? "domains"
96
  : false;
97
 
98
+ let tools = assistant?.tools ?? [];
99
  const regex = /{{\s?url=(.+?)\s?}}/g;
100
+
101
  $: templateVariables = [...systemPrompt.matchAll(regex)].map((match) => match[1]);
102
  $: selectedModel = models.find((m) => m.id === modelId);
103
  </script>
 
150
  formData.set("ragLinkList", "");
151
  }
152
 
153
+ formData.set("tools", tools.join(","));
154
+
155
  return async ({ result }) => {
156
  loading = false;
157
  await applyAction(result);
 
409
  </div>
410
  <p class="text-xs text-red-500">{getError("inputMessage1", form)}</p>
411
  </label>
412
+ {#if $page.data.user?.isEarlyAccess && selectedModel?.tools}
413
+ <div>
414
+ <span class="text-smd font-semibold"
415
+ >Tools
416
+ <CarbonTools class="inline text-xs text-purple-600" />
417
+ <span class="ml-1 rounded bg-gray-100 px-1 py-0.5 text-xxs font-normal text-gray-600"
418
+ >Experimental</span
419
+ >
420
+ </span>
421
+ <p class="text-xs text-gray-500">
422
+ Choose up to 3 community tools that will be used with this assistant.
423
+ </p>
424
+ </div>
425
+ <AssistantToolPicker bind:toolIds={tools} />
426
+ {/if}
427
  {#if $page.data.enableAssistantsRAG}
428
+ <div class="flex flex-col flex-nowrap pb-4">
429
  <span class="mt-2 text-smd font-semibold"
430
  >Internet access
431
  <IconInternet classNames="inline text-sm text-blue-600" />
432
 
 
 
 
 
433
  {#if isHuggingChat}
434
  <a
435
  href="https://huggingface.co/spaces/huggingchat/chat-ui/discussions/385"
 
524
  />
525
  <p class="text-xs text-red-500">{getError("ragLinkList", form)}</p>
526
  {/if}
 
 
 
 
 
 
 
 
 
 
 
 
 
527
  </div>
528
  {/if}
529
  </div>
530
 
531
  <div class="relative col-span-1 flex h-full flex-col">
532
  <div class="mb-1 flex justify-between text-sm">
533
+ <span class="block font-semibold"> Instructions (System Prompt) </span>
534
  {#if dynamicPrompt && templateVariables.length}
535
  <div class="relative">
536
  <button
 
553
  </div>
554
  {/if}
555
  </div>
556
+ <label class="pb-2 text-sm has-[:checked]:font-semibold">
557
+ <input type="checkbox" name="dynamicPrompt" bind:checked={dynamicPrompt} />
558
+ Dynamic Prompt
559
+ <p class="mb-2 text-xs font-normal text-gray-500">
560
+ Allow the use of template variables {"{{url=https://example.com/path}}"}
561
+ to insert dynamic content into your prompt by making GET requests to specified URLs on each
562
+ inference.
563
+ </p>
564
+ </label>
565
+
566
  <div class="relative mb-20 flex h-full flex-col gap-2">
567
  <textarea
568
  name="preprompt"
src/lib/components/AssistantToolPicker.svelte ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { base } from "$app/paths";
3
+ import type { ToolLogoColor, ToolLogoIcon } from "$lib/types/Tool";
4
+ import { debounce } from "$lib/utils/debounce";
5
+ import { onMount } from "svelte";
6
+ import ToolLogo from "./ToolLogo.svelte";
7
+
8
+ import CarbonClose from "~icons/carbon/close";
9
+
10
+ interface ToolSuggestion {
11
+ _id: string;
12
+ displayName: string;
13
+ createdByName: string;
14
+ color: ToolLogoColor;
15
+ icon: ToolLogoIcon;
16
+ }
17
+
18
+ export let toolIds: string[] = [];
19
+
20
+ let selectedValues: ToolSuggestion[] = [];
21
+
22
+ onMount(async () => {
23
+ selectedValues = await Promise.all(
24
+ toolIds.map(async (id) => await fetch(`${base}/api/tools/${id}`).then((res) => res.json()))
25
+ );
26
+
27
+ await fetchSuggestions("");
28
+ });
29
+
30
+ let inputValue = "";
31
+ let maxValues = 3;
32
+
33
+ let suggestions: ToolSuggestion[] = [];
34
+
35
+ async function fetchSuggestions(query: string) {
36
+ suggestions = (await fetch(`${base}/api/tools/search?q=${query}`).then((res) =>
37
+ res.json()
38
+ )) satisfies ToolSuggestion[];
39
+ }
40
+
41
+ const debouncedFetch = debounce((query: string) => fetchSuggestions(query), 300);
42
+
43
+ function addValue(value: ToolSuggestion) {
44
+ if (selectedValues.length < maxValues && !selectedValues.includes(value)) {
45
+ selectedValues = [...selectedValues, value];
46
+ toolIds = [...toolIds, value._id];
47
+ inputValue = "";
48
+ suggestions = [];
49
+ }
50
+ }
51
+
52
+ function removeValue(id: ToolSuggestion["_id"]) {
53
+ selectedValues = selectedValues.filter((v) => v._id !== id);
54
+ toolIds = selectedValues.map((value) => value._id);
55
+ }
56
+ </script>
57
+
58
+ {#if selectedValues.length > 0}
59
+ <div class="flex flex-wrap items-center justify-center gap-2">
60
+ {#each selectedValues as value}
61
+ <div
62
+ class="flex items-center justify-center space-x-2 rounded border border-gray-300 bg-gray-200 px-2 py-1"
63
+ >
64
+ <ToolLogo color={value.color} icon={value.icon} size="sm" />
65
+ <div class="flex flex-col items-center justify-center py-1">
66
+ <a
67
+ href={`${base}/tools/${value._id}`}
68
+ target="_blank"
69
+ class="line-clamp-1 truncate font-semibold text-blue-600 hover:underline"
70
+ >{value.displayName}</a
71
+ >
72
+ {#if value.createdByName}
73
+ <p class="text-center text-xs text-gray-500">
74
+ Created by
75
+ <a class="underline" href="{base}/tools?user={value.createdByName}" target="_blank"
76
+ >{value.createdByName}</a
77
+ >
78
+ </p>
79
+ {:else}
80
+ <p class="text-center text-xs text-gray-500">Official HuggingChat tool</p>
81
+ {/if}
82
+ </div>
83
+ <button
84
+ on:click|stopPropagation|preventDefault={() => removeValue(value._id)}
85
+ class="text-lg text-gray-600"
86
+ >
87
+ <CarbonClose />
88
+ </button>
89
+ </div>
90
+ {/each}
91
+ </div>
92
+ {/if}
93
+
94
+ {#if selectedValues.length < maxValues}
95
+ <div class="group relative block">
96
+ <input
97
+ type="text"
98
+ bind:value={inputValue}
99
+ on:input={(ev) => {
100
+ inputValue = ev.currentTarget.value;
101
+ debouncedFetch(inputValue);
102
+ }}
103
+ disabled={selectedValues.length >= maxValues}
104
+ class="w-full rounded border border-gray-200 bg-gray-100 px-3 py-2"
105
+ class:opacity-50={selectedValues.length >= maxValues}
106
+ class:bg-gray-100={selectedValues.length >= maxValues}
107
+ placeholder="Type to search tools..."
108
+ />
109
+ {#if suggestions.length > 0}
110
+ <div
111
+ class="invisible absolute z-10 mt-1 w-full rounded border border-gray-300 bg-white shadow-lg group-focus-within:visible"
112
+ >
113
+ {#each suggestions as suggestion}
114
+ <button
115
+ on:click|stopPropagation|preventDefault={() => addValue(suggestion)}
116
+ class="w-full cursor-pointer px-3 py-2 text-left hover:bg-blue-500 hover:text-white"
117
+ >
118
+ {suggestion.displayName}
119
+ </button>
120
+ {/each}
121
+ </div>
122
+ {/if}
123
+ </div>
124
+ {/if}
src/lib/components/ToolBadge.svelte ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import ToolLogo from "./ToolLogo.svelte";
3
+ import { base } from "$app/paths";
4
+ import { browser } from "$app/environment";
5
+
6
+ export let toolId: string;
7
+ </script>
8
+
9
+ <div
10
+ class="relative flex items-center justify-center space-x-2 rounded border border-gray-300 bg-gray-200 px-2 py-1"
11
+ >
12
+ {#if browser}
13
+ {#await fetch(`${base}/api/tools/${toolId}`).then((res) => res.json()) then value}
14
+ <ToolLogo color={value.color} icon={value.icon} size="sm" />
15
+ <div class="flex flex-col items-center justify-center py-1">
16
+ <a
17
+ href={`${base}/tools/${value._id}`}
18
+ target="_blank"
19
+ class="line-clamp-1 truncate font-semibold text-blue-600 hover:underline"
20
+ >{value.displayName}</a
21
+ >
22
+ {#if value.createdByName}
23
+ <p class="text-center text-xs text-gray-500">
24
+ Created by
25
+ <a class="underline" href="{base}/tools?user={value.createdByName}" target="_blank"
26
+ >{value.createdByName}</a
27
+ >
28
+ </p>
29
+ {:else}
30
+ <p class="text-center text-xs text-gray-500">Official HuggingChat tool</p>
31
+ {/if}
32
+ </div>
33
+ {/await}
34
+ {/if}
35
+ </div>
src/lib/components/ToolLogo.svelte CHANGED
@@ -13,7 +13,7 @@
13
 
14
  export let color: string;
15
  export let icon: string;
16
- export let size: "md" | "lg" = "md";
17
 
18
  $: gradientColor = (() => {
19
  switch (color) {
@@ -72,6 +72,8 @@
72
 
73
  $: sizeClass = (() => {
74
  switch (size) {
 
 
75
  case "md":
76
  return "size-14";
77
  case "lg":
 
13
 
14
  export let color: string;
15
  export let icon: string;
16
+ export let size: "sm" | "md" | "lg" = "md";
17
 
18
  $: gradientColor = (() => {
19
  switch (color) {
 
72
 
73
  $: sizeClass = (() => {
74
  switch (size) {
75
+ case "sm":
76
+ return "size-8";
77
  case "md":
78
  return "size-14";
79
  case "lg":
src/lib/components/chat/AssistantIntroduction.svelte CHANGED
@@ -12,6 +12,7 @@
12
  import CarbonCheckmark from "~icons/carbon/checkmark";
13
  import CarbonRenew from "~icons/carbon/renew";
14
  import CarbonUserMultiple from "~icons/carbon/user-multiple";
 
15
 
16
  import { share } from "$lib/utils/share";
17
  import { env as envPublic } from "$env/dynamic/public";
@@ -30,6 +31,7 @@
30
  | "_id"
31
  | "description"
32
  | "userCount"
 
33
  >;
34
 
35
  const dispatch = createEventDispatcher<{ message: string }>();
@@ -83,6 +85,15 @@
83
  </p>
84
  {/if}
85
 
 
 
 
 
 
 
 
 
 
86
  {#if hasRag}
87
  <div
88
  class="flex h-5 w-fit items-center gap-1 rounded-full bg-blue-500/10 pl-1 pr-2 text-xs"
 
12
  import CarbonCheckmark from "~icons/carbon/checkmark";
13
  import CarbonRenew from "~icons/carbon/renew";
14
  import CarbonUserMultiple from "~icons/carbon/user-multiple";
15
+ import CarbonTools from "~icons/carbon/tools";
16
 
17
  import { share } from "$lib/utils/share";
18
  import { env as envPublic } from "$env/dynamic/public";
 
31
  | "_id"
32
  | "description"
33
  | "userCount"
34
+ | "tools"
35
  >;
36
 
37
  const dispatch = createEventDispatcher<{ message: string }>();
 
85
  </p>
86
  {/if}
87
 
88
+ {#if assistant?.tools?.length}
89
+ <div
90
+ class="flex h-5 w-fit items-center gap-1 rounded-full bg-purple-500/10 pl-1 pr-2 text-xs"
91
+ title="This assistant uses the websearch."
92
+ >
93
+ <CarbonTools class="text-sm text-purple-600" />
94
+ Has tools
95
+ </div>
96
+ {/if}
97
  {#if hasRag}
98
  <div
99
  class="flex h-5 w-fit items-center gap-1 rounded-full bg-blue-500/10 pl-1 pr-2 text-xs"
src/lib/server/models.ts CHANGED
@@ -110,7 +110,6 @@ async function getChatPromptRender(
110
  ];
111
  }
112
 
113
- logger.info({ formattedMessages });
114
  if (toolResults?.length) {
115
  // todo: should update the command r+ tokenizer to support system messages at any location
116
  // or use the `rag` mode without the citations
 
110
  ];
111
  }
112
 
 
113
  if (toolResults?.length) {
114
  // todo: should update the command r+ tokenizer to support system messages at any location
115
  // or use the `rag` mode without the citations
src/lib/server/textGeneration/assistant.ts CHANGED
@@ -31,9 +31,9 @@ export async function processPreprompt(preprompt: string) {
31
 
32
  export async function getAssistantById(id?: ObjectId) {
33
  return collections.assistants
34
- .findOne<Pick<Assistant, "rag" | "dynamicPrompt" | "generateSettings">>(
35
  { _id: id },
36
- { projection: { rag: 1, dynamicPrompt: 1, generateSettings: 1 } }
37
  )
38
  .then((a) => a ?? undefined);
39
  }
 
31
 
32
  export async function getAssistantById(id?: ObjectId) {
33
  return collections.assistants
34
+ .findOne<Pick<Assistant, "rag" | "dynamicPrompt" | "generateSettings" | "tools">>(
35
  { _id: id },
36
+ { projection: { rag: 1, dynamicPrompt: 1, generateSettings: 1, tools: 1 } }
37
  )
38
  .then((a) => a ?? undefined);
39
  }
src/lib/server/textGeneration/index.ts CHANGED
@@ -8,7 +8,7 @@ import {
8
  getAssistantById,
9
  processPreprompt,
10
  } from "./assistant";
11
- import { filterToolsOnPreferences, runTools } from "./tools";
12
  import type { WebSearch } from "$lib/types/WebSearch";
13
  import {
14
  type MessageUpdate,
@@ -77,8 +77,8 @@ async function* textGenerationWithoutTitle(
77
 
78
  let toolResults: ToolResult[] = [];
79
 
80
- if (model.tools && !conv.assistantId) {
81
- const tools = await filterToolsOnPreferences(toolsPreference, Boolean(assistant));
82
  const toolCallsRequired = tools.some((tool) => !toolHasName("directly_answer", tool));
83
  if (toolCallsRequired) toolResults = yield* runTools(ctx, tools, preprompt);
84
  }
 
8
  getAssistantById,
9
  processPreprompt,
10
  } from "./assistant";
11
+ import { getTools, runTools } from "./tools";
12
  import type { WebSearch } from "$lib/types/WebSearch";
13
  import {
14
  type MessageUpdate,
 
77
 
78
  let toolResults: ToolResult[] = [];
79
 
80
+ if (model.tools) {
81
+ const tools = await getTools(toolsPreference, ctx.assistant);
82
  const toolCallsRequired = tools.some((tool) => !toolHasName("directly_answer", tool));
83
  if (toolCallsRequired) toolResults = yield* runTools(ctx, tools, preprompt);
84
  }
src/lib/server/textGeneration/tools.ts CHANGED
@@ -20,27 +20,36 @@ import { stringifyError } from "$lib/utils/stringifyError";
20
  import { collections } from "../database";
21
  import { ObjectId } from "mongodb";
22
  import type { Message } from "$lib/types/Message";
 
23
 
24
- export async function filterToolsOnPreferences(
25
  toolsPreference: Array<string>,
26
- isAssistant: boolean
27
  ): Promise<Tool[]> {
28
- // if it's an assistant, only support websearch for now
29
- if (isAssistant) return [directlyAnswer, websearch];
 
 
 
 
 
 
 
30
 
31
  // filter based on tool preferences, add the tools that are on by default
32
  const activeConfigTools = toolFromConfigs.filter((el) => {
33
- if (el.isLocked && el.isOnByDefault) return true;
34
- return toolsPreference?.includes(el._id.toString()) ?? el.isOnByDefault;
35
  });
36
 
37
- // find tool where the id is in toolsPreference
38
  const activeCommunityTools = await collections.tools
39
  .find({
40
- _id: { $in: toolsPreference.map((el) => new ObjectId(el)) },
41
  })
42
  .toArray()
43
  .then((el) => el.map((el) => ({ ...el, call: getCallMethod(el) })));
 
44
  return [...activeConfigTools, ...activeCommunityTools];
45
  }
46
 
 
20
  import { collections } from "../database";
21
  import { ObjectId } from "mongodb";
22
  import type { Message } from "$lib/types/Message";
23
+ import type { Assistant } from "$lib/types/Assistant";
24
 
25
+ export async function getTools(
26
  toolsPreference: Array<string>,
27
+ assistant: Pick<Assistant, "tools"> | undefined
28
  ): Promise<Tool[]> {
29
+ let preferences = toolsPreference;
30
+
31
+ if (assistant) {
32
+ if (assistant?.tools?.length) {
33
+ preferences = assistant.tools;
34
+ } else {
35
+ return [directlyAnswer, websearch];
36
+ }
37
+ }
38
 
39
  // filter based on tool preferences, add the tools that are on by default
40
  const activeConfigTools = toolFromConfigs.filter((el) => {
41
+ if (el.isLocked && el.isOnByDefault && !assistant) return true;
42
+ return preferences?.includes(el._id.toString()) ?? (el.isOnByDefault && !assistant);
43
  });
44
 
45
+ // find tool where the id is in preferences
46
  const activeCommunityTools = await collections.tools
47
  .find({
48
+ _id: { $in: preferences.map((el) => new ObjectId(el)) },
49
  })
50
  .toArray()
51
  .then((el) => el.map((el) => ({ ...el, call: getCallMethod(el) })));
52
+
53
  return [...activeConfigTools, ...activeCommunityTools];
54
  }
55
 
src/lib/server/textGeneration/types.ts CHANGED
@@ -9,7 +9,7 @@ export interface TextGenerationContext {
9
  endpoint: Endpoint;
10
  conv: Conversation;
11
  messages: Message[];
12
- assistant?: Pick<Assistant, "rag" | "dynamicPrompt" | "generateSettings">;
13
  isContinue: boolean;
14
  webSearch: boolean;
15
  toolsPreference: Array<string>;
 
9
  endpoint: Endpoint;
10
  conv: Conversation;
11
  messages: Message[];
12
+ assistant?: Pick<Assistant, "rag" | "dynamicPrompt" | "generateSettings" | "tools">;
13
  isContinue: boolean;
14
  webSearch: boolean;
15
  toolsPreference: Array<string>;
src/lib/types/Assistant.ts CHANGED
@@ -28,6 +28,7 @@ export interface Assistant extends Timestamps {
28
  dynamicPrompt?: boolean;
29
  searchTokens: string[];
30
  last24HoursCount: number;
 
31
  }
32
 
33
  // eslint-disable-next-line no-shadow
 
28
  dynamicPrompt?: boolean;
29
  searchTokens: string[];
30
  last24HoursCount: number;
31
+ tools?: string[];
32
  }
33
 
34
  // eslint-disable-next-line no-shadow
src/routes/api/tools/[toolId]/+server.ts ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { collections } from "$lib/server/database.js";
2
+ import { toolFromConfigs } from "$lib/server/tools/index.js";
3
+ import type { CommunityToolDB } from "$lib/types/Tool.js";
4
+ import { ObjectId } from "mongodb";
5
+
6
+ export async function GET({ params, locals }) {
7
+ // XXX: feature_flag_tools
8
+ if (!locals.user?.isEarlyAccess) {
9
+ return new Response("Not early access", { status: 403 });
10
+ }
11
+
12
+ const toolId = params.toolId;
13
+
14
+ try {
15
+ const configTool = toolFromConfigs.find((el) => el._id.toString() === toolId);
16
+ if (configTool) {
17
+ return Response.json({
18
+ _id: toolId,
19
+ displayName: configTool.displayName,
20
+ color: configTool.color,
21
+ icon: configTool.icon,
22
+ createdByName: undefined,
23
+ });
24
+ } else {
25
+ // try community tools
26
+ const tool = await collections.tools
27
+ .findOne<CommunityToolDB>({ _id: new ObjectId(toolId) })
28
+ .then((tool) =>
29
+ tool
30
+ ? {
31
+ _id: tool._id.toString(),
32
+ displayName: tool.displayName,
33
+ color: tool.color,
34
+ icon: tool.icon,
35
+ createdByName: tool.createdByName,
36
+ }
37
+ : undefined
38
+ );
39
+
40
+ if (!tool) {
41
+ return new Response(`Tool "${toolId}" not found`, { status: 404 });
42
+ }
43
+
44
+ return Response.json(tool);
45
+ }
46
+ } catch (e) {
47
+ return new Response(`Tool "${toolId}" not found`, { status: 404 });
48
+ }
49
+ }
src/routes/api/tools/search/+server.ts ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { collections } from "$lib/server/database.js";
2
+ import { toolFromConfigs } from "$lib/server/tools/index.js";
3
+ import type { BaseTool, CommunityToolDB } from "$lib/types/Tool.js";
4
+ import { generateQueryTokens, generateSearchTokens } from "$lib/utils/searchTokens.js";
5
+ import type { Filter } from "mongodb";
6
+
7
+ export async function GET({ url, locals }) {
8
+ // XXX: feature_flag_tools
9
+ if (!locals.user?.isEarlyAccess) {
10
+ return new Response("Not early access", { status: 403 });
11
+ }
12
+
13
+ const query = url.searchParams.get("q")?.trim() ?? null;
14
+ const queryTokens = !!query && generateQueryTokens(query);
15
+
16
+ const filter: Filter<CommunityToolDB> = {
17
+ ...(queryTokens && { searchTokens: { $all: queryTokens } }),
18
+ featured: true,
19
+ };
20
+
21
+ const matchingCommunityTools = await collections.tools
22
+ .find(filter)
23
+ .project<Pick<BaseTool, "_id" | "displayName" | "color" | "icon">>({
24
+ _id: 1,
25
+ displayName: 1,
26
+ color: 1,
27
+ icon: 1,
28
+ createdByName: 1,
29
+ })
30
+ .sort({ useCount: -1 })
31
+ .limit(5)
32
+ .toArray();
33
+
34
+ const matchingConfigTools = toolFromConfigs
35
+ .filter((tool) => !tool?.isHidden)
36
+ .filter((tool) => {
37
+ if (queryTokens) {
38
+ return generateSearchTokens(tool.displayName).some((token) =>
39
+ queryTokens.some((queryToken) => queryToken.test(token))
40
+ );
41
+ }
42
+ return true;
43
+ })
44
+ .map((tool) => ({
45
+ _id: tool._id,
46
+ displayName: tool.displayName,
47
+ color: tool.color,
48
+ icon: tool.icon,
49
+ createdByName: undefined,
50
+ }));
51
+
52
+ const tools = [...matchingConfigTools, ...matchingCommunityTools] satisfies Array<
53
+ Pick<BaseTool, "_id" | "displayName" | "color" | "icon"> & { createdByName?: string }
54
+ >;
55
+
56
+ return Response.json(tools.map((tool) => ({ ...tool, _id: tool._id.toString() })).slice(0, 5));
57
+ }
src/routes/assistants/+page.svelte CHANGED
@@ -15,6 +15,8 @@
15
  import CarbonEarthAmerica from "~icons/carbon/earth-americas-filled";
16
  import CarbonUserMultiple from "~icons/carbon/user-multiple";
17
  import CarbonSearch from "~icons/carbon/search";
 
 
18
  import Pagination from "$lib/components/Pagination.svelte";
19
  import { formatUserCount } from "$lib/utils/formatUserCount";
20
  import { getHref } from "$lib/utils/getHref";
@@ -246,14 +248,24 @@
246
  </div>
247
  {/if}
248
 
249
- {#if hasRag}
250
- <div
251
- class="absolute left-3 top-3 grid size-5 place-items-center rounded-full bg-blue-500/10"
252
- title="This assistant uses the websearch."
253
- >
254
- <IconInternet classNames="text-sm text-blue-600" />
255
- </div>
256
- {/if}
 
 
 
 
 
 
 
 
 
 
257
 
258
  {#if assistant.avatar}
259
  <img
 
15
  import CarbonEarthAmerica from "~icons/carbon/earth-americas-filled";
16
  import CarbonUserMultiple from "~icons/carbon/user-multiple";
17
  import CarbonSearch from "~icons/carbon/search";
18
+ import CarbonTools from "~icons/carbon/tools";
19
+
20
  import Pagination from "$lib/components/Pagination.svelte";
21
  import { formatUserCount } from "$lib/utils/formatUserCount";
22
  import { getHref } from "$lib/utils/getHref";
 
248
  </div>
249
  {/if}
250
 
251
+ <div class="absolute left-3 top-3 flex items-center gap-1 text-xs text-gray-400">
252
+ {#if assistant.tools?.length}
253
+ <div
254
+ class="grid size-5 place-items-center rounded-full bg-purple-500/10"
255
+ title="This assistant uses the websearch."
256
+ >
257
+ <CarbonTools class="text-xs text-purple-600" />
258
+ </div>
259
+ {/if}
260
+ {#if hasRag}
261
+ <div
262
+ class="grid size-5 place-items-center rounded-full bg-blue-500/10"
263
+ title="This assistant uses the websearch."
264
+ >
265
+ <IconInternet classNames="text-sm text-blue-600" />
266
+ </div>
267
+ {/if}
268
+ </div>
269
 
270
  {#if assistant.avatar}
271
  <img
src/routes/conversation/[id]/+server.ts CHANGED
@@ -422,6 +422,7 @@ export async function POST({ request, locals, params, getClientAddress }) {
422
 
423
  let hasError = false;
424
  const initialMessageContent = messageToWriteTo.content;
 
425
  try {
426
  const ctx: TextGenerationContext = {
427
  model,
 
422
 
423
  let hasError = false;
424
  const initialMessageContent = messageToWriteTo.content;
425
+
426
  try {
427
  const ctx: TextGenerationContext = {
428
  model,
src/routes/settings/(nav)/assistants/[assistantId]/+page.svelte CHANGED
@@ -12,9 +12,11 @@
12
  import CarbonFlag from "~icons/carbon/flag";
13
  import CarbonLink from "~icons/carbon/link";
14
  import CarbonStar from "~icons/carbon/star";
 
15
  import CopyToClipBoardBtn from "$lib/components/CopyToClipBoardBtn.svelte";
16
  import ReportModal from "./ReportModal.svelte";
17
  import IconInternet from "$lib/components/icons/IconInternet.svelte";
 
18
 
19
  export let data: PageData;
20
 
@@ -214,6 +216,27 @@
214
  {/if}
215
  </div>
216
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  {#if hasRag}
218
  <div class="mt-4">
219
  <div class="mb-1 flex items-center gap-1">
 
12
  import CarbonFlag from "~icons/carbon/flag";
13
  import CarbonLink from "~icons/carbon/link";
14
  import CarbonStar from "~icons/carbon/star";
15
+ import CarbonTools from "~icons/carbon/tools";
16
  import CopyToClipBoardBtn from "$lib/components/CopyToClipBoardBtn.svelte";
17
  import ReportModal from "./ReportModal.svelte";
18
  import IconInternet from "$lib/components/icons/IconInternet.svelte";
19
+ import ToolBadge from "$lib/components/ToolBadge.svelte";
20
 
21
  export let data: PageData;
22
 
 
216
  {/if}
217
  </div>
218
 
219
+ {#if assistant?.tools?.length}
220
+ <div class="mt-4">
221
+ <div class="mb-1 flex items-center gap-1">
222
+ <span
223
+ class="inline-grid size-5 place-items-center rounded-full bg-purple-500/10"
224
+ title="This assistant uses the websearch."
225
+ >
226
+ <CarbonTools class="text-xs text-purple-600" />
227
+ </span>
228
+ <h2 class="font-semibold">Tools</h2>
229
+ </div>
230
+ <p class="w-full text-sm text-gray-500">
231
+ This Assistant has access to the following tools:
232
+ </p>
233
+ <ul class="mr-2 mt-2 flex flex-wrap gap-2.5 text-sm text-gray-800">
234
+ {#each assistant.tools as tool}
235
+ <ToolBadge toolId={tool} />
236
+ {/each}
237
+ </ul>
238
+ </div>
239
+ {/if}
240
  {#if hasRag}
241
  <div class="mt-4">
242
  <div class="mb-1 flex items-center gap-1">
src/routes/settings/(nav)/assistants/[assistantId]/edit/+page.server.ts CHANGED
@@ -10,6 +10,7 @@ import { sha256 } from "$lib/utils/sha256";
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({
15
  name: z.string().min(1),
@@ -39,6 +40,21 @@ const newAsssistantSchema = z.object({
39
  top_k: z
40
  .union([z.literal(""), z.coerce.number().min(5).max(100)])
41
  .transform((v) => (v === "" ? undefined : v)),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  });
43
 
44
  const uploadAvatar = async (avatar: File, assistantId: ObjectId): Promise<string> => {
@@ -74,7 +90,7 @@ export const actions: Actions = {
74
 
75
  const formData = Object.fromEntries(await request.formData());
76
 
77
- const parse = newAsssistantSchema.safeParse(formData);
78
 
79
  if (!parse.success) {
80
  // Loop through the errors array and create a custom errors array
@@ -155,6 +171,8 @@ export const actions: Actions = {
155
  allowedDomains: parse.data.ragDomainList,
156
  allowAllDomains: parse.data.ragAllowAll,
157
  },
 
 
158
  dynamicPrompt: parse.data.dynamicPrompt,
159
  searchTokens: generateSearchTokens(parse.data.name),
160
  generateSettings: {
 
10
  import sharp from "sharp";
11
  import { parseStringToList } from "$lib/utils/parseStringToList";
12
  import { generateSearchTokens } from "$lib/utils/searchTokens";
13
+ import { toolFromConfigs } from "$lib/server/tools";
14
 
15
  const newAsssistantSchema = z.object({
16
  name: z.string().min(1),
 
40
  top_k: z
41
  .union([z.literal(""), z.coerce.number().min(5).max(100)])
42
  .transform((v) => (v === "" ? undefined : v)),
43
+ tools: z
44
+ .string()
45
+ .optional()
46
+ .transform((v) => (v ? v.split(",") : []))
47
+ .transform(async (v) => [
48
+ ...(await collections.tools
49
+ .find({ _id: { $in: v.map((toolId) => new ObjectId(toolId)) } })
50
+ .project({ _id: 1 })
51
+ .toArray()
52
+ .then((tools) => tools.map((tool) => tool._id.toString()))),
53
+ ...toolFromConfigs
54
+ .filter((el) => (v ?? []).includes(el._id.toString()))
55
+ .map((el) => el._id.toString()),
56
+ ])
57
+ .optional(),
58
  });
59
 
60
  const uploadAvatar = async (avatar: File, assistantId: ObjectId): Promise<string> => {
 
90
 
91
  const formData = Object.fromEntries(await request.formData());
92
 
93
+ const parse = await newAsssistantSchema.safeParseAsync(formData);
94
 
95
  if (!parse.success) {
96
  // Loop through the errors array and create a custom errors array
 
171
  allowedDomains: parse.data.ragDomainList,
172
  allowAllDomains: parse.data.ragAllowAll,
173
  },
174
+ // XXX: feature_flag_tools
175
+ tools: locals.user?.isEarlyAccess ? parse.data.tools : undefined,
176
  dynamicPrompt: parse.data.dynamicPrompt,
177
  searchTokens: generateSearchTokens(parse.data.name),
178
  generateSettings: {
src/routes/settings/(nav)/assistants/[assistantId]/edit/[email protected] CHANGED
@@ -6,7 +6,7 @@
6
  export let data: PageData;
7
  export let form: ActionData;
8
 
9
- $: assistant = data.assistants.find((el) => el._id.toString() === $page.params.assistantId);
10
  </script>
11
 
12
  <AssistantSettings bind:form {assistant} models={data.models} />
 
6
  export let data: PageData;
7
  export let form: ActionData;
8
 
9
+ let assistant = data.assistants.find((el) => el._id.toString() === $page.params.assistantId);
10
  </script>
11
 
12
  <AssistantSettings bind:form {assistant} models={data.models} />
src/routes/settings/(nav)/assistants/new/+page.server.ts CHANGED
@@ -10,6 +10,7 @@ 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
 
14
  const newAsssistantSchema = z.object({
15
  name: z.string().min(1),
@@ -39,6 +40,21 @@ const newAsssistantSchema = z.object({
39
  top_k: z
40
  .union([z.literal(""), z.coerce.number().min(5).max(100)])
41
  .transform((v) => (v === "" ? undefined : v)),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  });
43
 
44
  const uploadAvatar = async (avatar: File, assistantId: ObjectId): Promise<string> => {
@@ -62,7 +78,7 @@ export const actions: Actions = {
62
  default: async ({ request, locals }) => {
63
  const formData = Object.fromEntries(await request.formData());
64
 
65
- const parse = newAsssistantSchema.safeParse(formData);
66
 
67
  if (!parse.success) {
68
  // Loop through the errors array and create a custom errors array
@@ -126,6 +142,8 @@ export const actions: Actions = {
126
  createdById,
127
  createdByName: locals.user?.username ?? locals.user?.name,
128
  ...parse.data,
 
 
129
  exampleInputs,
130
  avatar: hash,
131
  createdAt: new Date(),
 
10
  import { parseStringToList } from "$lib/utils/parseStringToList";
11
  import { usageLimits } from "$lib/server/usageLimits";
12
  import { generateSearchTokens } from "$lib/utils/searchTokens";
13
+ import { toolFromConfigs } from "$lib/server/tools";
14
 
15
  const newAsssistantSchema = z.object({
16
  name: z.string().min(1),
 
40
  top_k: z
41
  .union([z.literal(""), z.coerce.number().min(5).max(100)])
42
  .transform((v) => (v === "" ? undefined : v)),
43
+ tools: z
44
+ .string()
45
+ .optional()
46
+ .transform((v) => (v ? v.split(",") : []))
47
+ .transform(async (v) => [
48
+ ...(await collections.tools
49
+ .find({ _id: { $in: v.map((toolId) => new ObjectId(toolId)) } })
50
+ .project({ _id: 1 })
51
+ .toArray()
52
+ .then((tools) => tools.map((tool) => tool._id.toString()))),
53
+ ...toolFromConfigs
54
+ .filter((el) => (v ?? []).includes(el._id.toString()))
55
+ .map((el) => el._id.toString()),
56
+ ])
57
+ .optional(),
58
  });
59
 
60
  const uploadAvatar = async (avatar: File, assistantId: ObjectId): Promise<string> => {
 
78
  default: async ({ request, locals }) => {
79
  const formData = Object.fromEntries(await request.formData());
80
 
81
+ const parse = await newAsssistantSchema.safeParseAsync(formData);
82
 
83
  if (!parse.success) {
84
  // Loop through the errors array and create a custom errors array
 
142
  createdById,
143
  createdByName: locals.user?.username ?? locals.user?.name,
144
  ...parse.data,
145
+ // XXX: feature_flag_tools
146
+ tools: locals.user?.isEarlyAccess ? parse.data.tools : undefined,
147
  exampleInputs,
148
  avatar: hash,
149
  createdAt: new Date(),