nsarrazin HF Staff commited on
Commit
0c3e3b2
·
unverified ·
1 Parent(s): b713bbd

Community tools (#1250)

Browse files

* wip community tools

* push

* fix

* bugfix

* pass inputs to gradio correctly

* full inputs for imagegen

* lint

* update gradio

* fix call to gradio

* fix imagegen tool

* fix name

* fix image display

* Community tools (#1297)

* work on tools

* wip

* icon

* page

* use IDs for every tools

* improve tools page

* wip

* add optimized deps

* refacto 1tool=1function

* Add preview page

* fix active indicator

* fix text alignment

* fix populate script

* tooledit

* fixes

* better inputs

* fix console error & cancel button

* upload new tools

* edit

* improve lint

* working community tools

* fix active featured check

* Simplify outputs in user form

* fix non file outputs

* fix imagegen default tool

* add community tools popup

* redirect gradio API calls through chat-ui backend

* better usecount

* Better loading & output element

* better redirects & auto activate

* bugfixes

* fix migration changes

* Add feature flag for tools

* fix tools menu

* Keep track of output component index

* fix null check

* catch error for spaces config

* wip: MIME types

* wip: rework tools to use filenames

* fix broken tool calling

* fix input tools

* Working files

* fix(form): bind on name

* fix(prompt): double system prompt

* fix(prompt): file list

* fix(tools): make sure tools are off by default

* Add description to ToolEdit

* Display runs

* feat(tools): add back document parser

* feat(tools): bring back image editing tool

* fix MIME types

* Fix file passing prompts

* fix lint

* Set tools to default in migration

* Revert "Set tools to default in migration"

This reverts commit 87fdd094b73e2427af1a272e6697dafd4d466f1c.

* Use correct space for official image generation

* Remove debug logs

* fix(redirect): working redirect when no `APP_BASE`

* sveltekit 2 migration

* fix types with gradio update

* fix name based searching

* fix cut titles

* add min & max

* add error if no endpoint

* make form not submittable until it's filled

* fix return

* Better support for varied input types

* fix(update): hide null values for params that have not been called explicitly by the model

* add extra migration for transition

* fix document parser mime types

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env +3 -0
  2. chart/env/prod.yaml +145 -0
  3. package-lock.json +0 -0
  4. package.json +3 -1
  5. scripts/populate.ts +75 -1
  6. src/lib/buildPrompt.ts +5 -1
  7. src/lib/components/NavMenu.svelte +10 -1
  8. src/lib/components/ToolLogo.svelte +100 -0
  9. src/lib/components/ToolsMenu.svelte +67 -22
  10. src/lib/components/chat/ChatWindow.svelte +2 -2
  11. src/lib/components/chat/ToolUpdate.svelte +12 -9
  12. src/lib/migrations/routines/03-add-tools-in-settings.ts +1 -1
  13. src/lib/migrations/routines/07-reset-tools-in-settings.ts +1 -1
  14. src/lib/migrations/routines/08-reset-tools-in-settings-2.ts +19 -0
  15. src/lib/migrations/routines/index.ts +2 -0
  16. src/lib/server/database.ts +9 -0
  17. src/lib/server/metrics.ts +3 -4
  18. src/lib/server/models.ts +32 -11
  19. src/lib/server/textGeneration/index.ts +3 -4
  20. src/lib/server/textGeneration/tools.ts +91 -41
  21. src/lib/server/textGeneration/types.ts +1 -1
  22. src/lib/server/tools/calculator.ts +23 -14
  23. src/lib/server/tools/directlyAnswer.ts +16 -6
  24. src/lib/server/tools/documentParser.ts +0 -60
  25. src/lib/server/tools/images/editing.ts +0 -95
  26. src/lib/server/tools/images/generation.ts +0 -84
  27. src/lib/server/tools/index.ts +281 -20
  28. src/lib/server/tools/outputs.ts +32 -0
  29. src/lib/server/tools/utils.ts +7 -2
  30. src/lib/server/tools/web/search.ts +19 -10
  31. src/lib/server/tools/web/url.ts +24 -14
  32. src/lib/server/usageLimits.ts +1 -0
  33. src/lib/stores/settings.ts +1 -1
  34. src/lib/types/Report.ts +2 -1
  35. src/lib/types/Settings.ts +2 -2
  36. src/lib/types/Tool.ts +145 -26
  37. src/lib/utils/getGradioApi.ts +11 -0
  38. src/lib/utils/messageUpdates.ts +1 -1
  39. src/lib/utils/tools.ts +16 -0
  40. src/routes/+layout.server.ts +49 -15
  41. src/routes/api/spaces-config/+server.ts +31 -0
  42. src/routes/assistants/+page.server.ts +2 -2
  43. src/routes/conversation/[id]/+server.ts +14 -3
  44. src/routes/settings/(nav)/+server.ts +10 -1
  45. src/routes/settings/(nav)/assistants/[assistantId]/+page.server.ts +4 -2
  46. src/routes/settings/(nav)/assistants/[assistantId]/ReportModal.svelte +2 -2
  47. src/routes/settings/+layout.server.ts +5 -2
  48. src/routes/tools/+layout.svelte +1 -0
  49. src/routes/tools/+layout.ts +15 -0
  50. src/routes/tools/+page.server.ts +94 -0
.env CHANGED
@@ -163,6 +163,9 @@ ALLOW_INSECURE_COOKIES=false # recommended to keep this to false but set to true
163
  METRICS_ENABLED=false
164
  METRICS_PORT=5565
165
  LOG_LEVEL=info
 
 
 
166
  BODY_SIZE_LIMIT=15728640
167
 
168
  HF_ORG_ADMIN=
 
163
  METRICS_ENABLED=false
164
  METRICS_PORT=5565
165
  LOG_LEVEL=info
166
+
167
+
168
+ TOOLS=`[]`
169
  BODY_SIZE_LIMIT=15728640
170
 
171
  HF_ORG_ADMIN=
chart/env/prod.yaml CHANGED
@@ -328,6 +328,151 @@ envVars:
328
  }]
329
  WEBSEARCH_BLOCKLIST: '["youtube.com", "twitter.com"]'
330
  XFF_DEPTH: '2'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
  HF_ORG_ADMIN: 'huggingchat'
332
  HF_ORG_EARLY_ACCESS: 'huggingface'
333
 
 
328
  }]
329
  WEBSEARCH_BLOCKLIST: '["youtube.com", "twitter.com"]'
330
  XFF_DEPTH: '2'
331
+ TOOLS: >
332
+ [
333
+ {
334
+ "_id": "000000000000000000000001",
335
+ "displayName": "Image Generation",
336
+ "description": "Use this tool to generate images based on a prompt.",
337
+ "color": "yellow",
338
+ "icon": "camera",
339
+ "baseUrl": "stabilityai/stable-diffusion-3-medium",
340
+ "name": "image_generation",
341
+ "endpoint": "/infer",
342
+ "inputs": [
343
+ {
344
+ "name": "prompt",
345
+ "description": "A prompt to generate an image from",
346
+ "paramType": "required",
347
+ "type": "str"
348
+ },
349
+ {
350
+ "name": "negative_prompt",
351
+ "description": "A prompt for things that should not be in the image.",
352
+ "paramType": "optional",
353
+ "default": "",
354
+ "type": "str"
355
+ },
356
+ {
357
+ "name": "seed",
358
+ "paramType": "fixed",
359
+ "value": "0",
360
+ "type": "float"
361
+ },
362
+ {
363
+ "name": "randomize_seed",
364
+ "paramType": "fixed",
365
+ "value": "true",
366
+ "type": "bool"
367
+ },
368
+ {
369
+ "name": "width",
370
+ "description": "numeric value between 256 and 1344",
371
+ "paramType": "optional",
372
+ "default": 1024,
373
+ "type": "float"
374
+ },
375
+ {
376
+ "name": "height",
377
+ "description": "numeric value between 256 and 1344",
378
+ "paramType": "optional",
379
+ "default": 1024,
380
+ "type": "float"
381
+ },
382
+ {
383
+ "name": "guidance_scale",
384
+ "description": "numeric value between 0.0 and 10.0",
385
+ "paramType": "optional",
386
+ "default": 5,
387
+ "type": "float"
388
+ },
389
+ {
390
+ "name": "num_inference_steps",
391
+ "description": "numeric value between 1 and 50",
392
+ "paramType": "optional",
393
+ "default": 28,
394
+ "type": "float"
395
+ }
396
+ ],
397
+ "outputComponent": "image",
398
+ "outputComponentIdx": 0,
399
+ "showOutput": true
400
+ },
401
+ {
402
+ "_id": "000000000000000000000002",
403
+ "displayName": "Document parser",
404
+ "description": "Use this tool to parse any document and get its content in markdown format.",
405
+ "color": "yellow",
406
+ "icon": "cloud",
407
+ "baseUrl": "huggingchat/document-parser",
408
+ "name": "document_parser",
409
+ "endpoint": "/predict",
410
+ "inputs": [
411
+ {
412
+ "name": "document",
413
+ "description": "Filename of the document to parse",
414
+ "paramType": "required",
415
+ "type": "file",
416
+ "mimeTypes": 'application/*'
417
+ },
418
+ {
419
+ "name": "filename",
420
+ "paramType": "fixed",
421
+ "value": "document.pdf",
422
+ "type": "str"
423
+ }
424
+ ],
425
+ "outputComponent": "textbox",
426
+ "outputComponentIdx": 0,
427
+ "showOutput": false
428
+ },
429
+ {
430
+ "_id": "000000000000000000000003",
431
+ "name": "edit_image",
432
+ "baseUrl": "multimodalart/cosxl",
433
+ "endpoint": "/run_edit",
434
+ "inputs": [
435
+ {
436
+ "name": "image",
437
+ "description": "The image path to be edited",
438
+ "paramType": "required",
439
+ "type": "file",
440
+ "mimeTypes": 'image/*'
441
+ },
442
+ {
443
+ "name": "prompt",
444
+ "description": "The prompt with which to edit the image",
445
+ "paramType": "required",
446
+ "type": "str"
447
+ },
448
+ {
449
+ "name": "negative_prompt",
450
+ "paramType": "fixed",
451
+ "value": "",
452
+ "type": "str"
453
+ },
454
+ {
455
+ "name": "guidance_scale",
456
+ "paramType": "fixed",
457
+ "value": 6.5,
458
+ "type": "float"
459
+ },
460
+ {
461
+ "name": "steps",
462
+ "paramType": "fixed",
463
+ "value": 30,
464
+ "type": "float"
465
+ }
466
+ ],
467
+ "outputComponent": "image",
468
+ "showOutput": true,
469
+ "displayName": "Image editor",
470
+ "color": "green",
471
+ "icon": "camera",
472
+ "description": "This tool lets you edit images",
473
+ "outputComponentIdx": 0
474
+ }
475
+ ]
476
  HF_ORG_ADMIN: 'huggingchat'
477
  HF_ORG_EARLY_ACCESS: 'huggingface'
478
 
package-lock.json CHANGED
The diff for this file is too large to render. See raw diff
 
package.json CHANGED
@@ -27,6 +27,7 @@
27
  "@types/express": "^4.17.21",
28
  "@types/js-yaml": "^4.0.9",
29
  "@types/jsdom": "^21.1.1",
 
30
  "@types/minimist": "^1.2.5",
31
  "@types/node": "^22.1.0",
32
  "@types/parquetjs": "^0.10.3",
@@ -59,7 +60,7 @@
59
  "dependencies": {
60
  "@aws-sdk/credential-providers": "^3.592.0",
61
  "@cliqz/adblocker-playwright": "^1.27.2",
62
- "@gradio/client": "^0.19.4",
63
  "@huggingface/hub": "^0.5.1",
64
  "@huggingface/inference": "^2.7.0",
65
  "@iconify-json/bi": "^1.1.21",
@@ -82,6 +83,7 @@
82
  "jose": "^5.3.0",
83
  "jsdom": "^22.0.0",
84
  "json5": "^2.2.3",
 
85
  "lint-staged": "^15.2.7",
86
  "marked": "^12.0.1",
87
  "marked-katex-extension": "^5.0.1",
 
27
  "@types/express": "^4.17.21",
28
  "@types/js-yaml": "^4.0.9",
29
  "@types/jsdom": "^21.1.1",
30
+ "@types/jsonpath": "^0.2.4",
31
  "@types/minimist": "^1.2.5",
32
  "@types/node": "^22.1.0",
33
  "@types/parquetjs": "^0.10.3",
 
60
  "dependencies": {
61
  "@aws-sdk/credential-providers": "^3.592.0",
62
  "@cliqz/adblocker-playwright": "^1.27.2",
63
+ "@gradio/client": "^1.1.1",
64
  "@huggingface/hub": "^0.5.1",
65
  "@huggingface/inference": "^2.7.0",
66
  "@iconify-json/bi": "^1.1.21",
 
83
  "jose": "^5.3.0",
84
  "jsdom": "^22.0.0",
85
  "json5": "^2.2.3",
86
+ "jsonpath": "^1.1.1",
87
  "lint-staged": "^15.2.7",
88
  "marked": "^12.0.1",
89
  "marked-katex-extension": "^5.0.1",
scripts/populate.ts CHANGED
@@ -14,6 +14,7 @@ import type { User } from "../src/lib/types/User";
14
  import type { Assistant } from "../src/lib/types/Assistant";
15
  import type { Conversation } from "../src/lib/types/Conversation";
16
  import type { Settings } from "../src/lib/types/Settings";
 
17
  import { defaultEmbeddingModel } from "../src/lib/server/embeddingModels.ts";
18
  import { Message } from "../src/lib/types/Message.ts";
19
 
@@ -29,7 +30,7 @@ rl.on("close", function () {
29
  process.exit(0);
30
  });
31
 
32
- const possibleFlags = ["reset", "all", "users", "settings", "assistants", "conversations"];
33
  const argv = minimist(process.argv.slice(2));
34
  const flags = argv["_"].filter((flag) => possibleFlags.includes(flag));
35
 
@@ -113,6 +114,7 @@ async function seed() {
113
  await collections.settings.deleteMany({});
114
  await collections.assistants.deleteMany({});
115
  await collections.conversations.deleteMany({});
 
116
  console.log("Reset done");
117
  }
118
 
@@ -239,6 +241,78 @@ async function seed() {
239
  );
240
  console.log("Done creating conversations.");
241
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  }
243
 
244
  // run seed
 
14
  import type { Assistant } from "../src/lib/types/Assistant";
15
  import type { Conversation } from "../src/lib/types/Conversation";
16
  import type { Settings } from "../src/lib/types/Settings";
17
+ import type { CommunityToolDB, ToolLogoColor, ToolLogoIcon } from "../src/lib/types/Tool";
18
  import { defaultEmbeddingModel } from "../src/lib/server/embeddingModels.ts";
19
  import { Message } from "../src/lib/types/Message.ts";
20
 
 
30
  process.exit(0);
31
  });
32
 
33
+ const possibleFlags = ["reset", "all", "users", "settings", "assistants", "conversations", "tools"];
34
  const argv = minimist(process.argv.slice(2));
35
  const flags = argv["_"].filter((flag) => possibleFlags.includes(flag));
36
 
 
114
  await collections.settings.deleteMany({});
115
  await collections.assistants.deleteMany({});
116
  await collections.conversations.deleteMany({});
117
+ await collections.tools.deleteMany({});
118
  console.log("Reset done");
119
  }
120
 
 
241
  );
242
  console.log("Done creating conversations.");
243
  }
244
+
245
+ // generate Community Tools
246
+ if (flags.includes("tools") || flags.includes("all")) {
247
+ const tools = await Promise.all(
248
+ faker.helpers.multiple(
249
+ () => {
250
+ const _id = new ObjectId();
251
+ const displayName = faker.company.catchPhrase();
252
+ const description = faker.company.catchPhrase();
253
+ const color = faker.helpers.arrayElement([
254
+ "purple",
255
+ "blue",
256
+ "green",
257
+ "yellow",
258
+ "red",
259
+ ]) satisfies ToolLogoColor;
260
+ const icon = faker.helpers.arrayElement([
261
+ "wikis",
262
+ "tools",
263
+ "camera",
264
+ "code",
265
+ "email",
266
+ "cloud",
267
+ "terminal",
268
+ "game",
269
+ "chat",
270
+ "speaker",
271
+ "video",
272
+ ]) satisfies ToolLogoIcon;
273
+ const baseUrl = faker.helpers.arrayElement([
274
+ "stabilityai/stable-diffusion-3-medium",
275
+ "multimodalart/cosxl",
276
+ "gokaygokay/SD3-Long-Captioner",
277
+ "xichenhku/MimicBrush",
278
+ ]);
279
+
280
+ // keep empty for populate for now
281
+
282
+ const user: User = faker.helpers.arrayElement(users);
283
+ const createdById = user._id;
284
+ const createdByName = user.username ?? user.name;
285
+
286
+ return {
287
+ type: "community" as const,
288
+ _id,
289
+ createdById,
290
+ createdByName,
291
+ displayName,
292
+ name: displayName.toLowerCase().replace(" ", "_"),
293
+ endpoint: "/test",
294
+ description,
295
+ color,
296
+ icon,
297
+ baseUrl,
298
+ inputs: [],
299
+ outputPath: null,
300
+ outputType: "str" as const,
301
+ showOutput: false,
302
+ useCount: faker.number.int({ min: 0, max: 100000 }),
303
+ last24HoursUseCount: faker.number.int({ min: 0, max: 1000 }),
304
+ createdAt: faker.date.recent({ days: 30 }),
305
+ updatedAt: faker.date.recent({ days: 30 }),
306
+ searchTokens: generateSearchTokens(displayName),
307
+ featured: faker.datatype.boolean(),
308
+ };
309
+ },
310
+ { count: faker.number.int({ min: 10, max: 200 }) }
311
+ )
312
+ );
313
+
314
+ await collections.tools.insertMany(tools satisfies CommunityToolDB[]);
315
+ }
316
  }
317
 
318
  // run seed
src/lib/buildPrompt.ts CHANGED
@@ -16,7 +16,11 @@ export async function buildPrompt({
16
  tools,
17
  toolResults,
18
  }: buildPromptOptions): Promise<string> {
19
- const filteredMessages = messages.filter((m) => m.from !== "system");
 
 
 
 
20
 
21
  let prompt = model
22
  .chatPromptRender({
 
16
  tools,
17
  toolResults,
18
  }: buildPromptOptions): Promise<string> {
19
+ const filteredMessages = messages;
20
+
21
+ if (filteredMessages[0].from === "system" && preprompt) {
22
+ filteredMessages[0].content = preprompt;
23
+ }
24
 
25
  let prompt = model
26
  .chatPromptRender({
src/lib/components/NavMenu.svelte CHANGED
@@ -134,8 +134,17 @@
134
  class="flex h-9 flex-none items-center gap-1.5 rounded-lg pl-2.5 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
135
  >
136
  Assistants
 
 
 
 
 
 
 
 
 
137
  <span
138
- class="ml-auto rounded-full border border-gray-300 px-2 py-0.5 text-xs text-gray-500 dark:border-gray-500 dark:text-gray-400"
139
  >New</span
140
  >
141
  </a>
 
134
  class="flex h-9 flex-none items-center gap-1.5 rounded-lg pl-2.5 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
135
  >
136
  Assistants
137
+ </a>
138
+ {/if}
139
+ <!-- XXX: feature_flag_tools -->
140
+ {#if $page.data.user?.isEarlyAccess}
141
+ <a
142
+ href="{base}/tools"
143
+ class="flex h-9 flex-none items-center gap-1.5 rounded-lg pl-2.5 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
144
+ >
145
+ Tools
146
  <span
147
+ class="ml-auto rounded-full border border-purple-300 px-2 py-0.5 text-xs text-purple-500 dark:border-purple-500 dark:text-purple-400"
148
  >New</span
149
  >
150
  </a>
src/lib/components/ToolLogo.svelte ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import CarbonWikis from "~icons/carbon/wikis";
3
+ import CarbonTools from "~icons/carbon/tools";
4
+ import CarbonCamera from "~icons/carbon/camera";
5
+ import CarbonCode from "~icons/carbon/code";
6
+ import CarbonEmail from "~icons/carbon/email";
7
+ import CarbonCloud from "~icons/carbon/cloud-upload";
8
+ import CarbonTerminal from "~icons/carbon/terminal";
9
+ import CarbonGame from "~icons/carbon/game-console";
10
+ import CarbonChat from "~icons/carbon/chat-bot";
11
+ import CarbonSpeaker from "~icons/carbon/volume-up";
12
+ import CarbonVideo from "~icons/carbon/video";
13
+
14
+ export let color: string;
15
+ export let icon: string;
16
+ export let size: "md" | "lg" = "md";
17
+
18
+ $: gradientColor = (() => {
19
+ switch (color) {
20
+ case "purple":
21
+ return "#653789";
22
+ case "blue":
23
+ return "#375889";
24
+ case "green":
25
+ return "#37894E";
26
+ case "yellow":
27
+ return "#897C37";
28
+ case "red":
29
+ return "#893737";
30
+ default:
31
+ return "#FFF";
32
+ }
33
+ })();
34
+
35
+ let iconEl = CarbonWikis;
36
+
37
+ switch (icon) {
38
+ case "wikis":
39
+ iconEl = CarbonWikis;
40
+ break;
41
+ case "tools":
42
+ iconEl = CarbonTools;
43
+ break;
44
+ case "camera":
45
+ iconEl = CarbonCamera;
46
+ break;
47
+ case "code":
48
+ iconEl = CarbonCode;
49
+ break;
50
+ case "email":
51
+ iconEl = CarbonEmail;
52
+ break;
53
+ case "cloud":
54
+ iconEl = CarbonCloud;
55
+ break;
56
+ case "terminal":
57
+ iconEl = CarbonTerminal;
58
+ break;
59
+ case "game":
60
+ iconEl = CarbonGame;
61
+ break;
62
+ case "chat":
63
+ iconEl = CarbonChat;
64
+ break;
65
+ case "speaker":
66
+ iconEl = CarbonSpeaker;
67
+ break;
68
+ case "video":
69
+ iconEl = CarbonVideo;
70
+ break;
71
+ }
72
+
73
+ $: sizeClass = (() => {
74
+ switch (size) {
75
+ case "md":
76
+ return "size-14";
77
+ case "lg":
78
+ return "size-24";
79
+ }
80
+ })();
81
+ </script>
82
+
83
+ <div class="flex {sizeClass} items-center justify-center">
84
+ <svg xmlns="http://www.w3.org/2000/svg" class="absolute {sizeClass} h-full" viewBox="0 0 52 58">
85
+ <defs>
86
+ <linearGradient id="gradient-{gradientColor}" gradientTransform="rotate(90)">
87
+ <stop offset="0%" stop-color="#0E1523" />
88
+ <stop offset="100%" stop-color={gradientColor} />
89
+ </linearGradient>
90
+ <mask id="mask">
91
+ <path
92
+ d="M22.3043 1.2486C23.4279 0.603043 24.7025 0.263184 26 0.263184C27.2975 0.263184 28.5721 0.603043 29.6957 1.2486L48.3043 11.9373C49.4279 12.5828 50.361 13.5113 51.0097 14.6294C51.6584 15.7475 52 17.0158 52 18.3069V39.6902C52 40.9813 51.6584 42.2496 51.0097 43.3677C50.361 44.4858 49.4279 45.4143 48.3043 46.0598L29.6957 56.7514C28.5721 57.397 27.2975 57.7369 26 57.7369C24.7025 57.7369 23.4279 57.397 22.3043 56.7514L3.6957 46.0598C2.57209 45.4143 1.63904 44.4858 0.990308 43.3677C0.341578 42.2496 3.34785e-05 40.9813 5.18628e-07 39.6902V18.3099C-0.000485629 17.0183 0.340813 15.7494 0.989568 14.6307C1.63832 13.512 2.57166 12.5831 3.6957 11.9373L22.3043 1.2486Z"
93
+ fill="white"
94
+ />
95
+ </mask>
96
+ </defs>
97
+ <rect width="100%" height="100%" fill="url(#gradient-{gradientColor})" mask="url(#mask)" />
98
+ </svg>
99
+ <svelte:component this={iconEl} class="relative {sizeClass} scale-50 text-clip text-gray-200" />
100
+ </div>
src/lib/components/ToolsMenu.svelte CHANGED
@@ -1,4 +1,5 @@
1
  <script lang="ts">
 
2
  import { page } from "$app/stores";
3
  import { clickOutside } from "$lib/actions/clickOutside";
4
  import { useSettingsStore } from "$lib/stores/settings";
@@ -14,15 +15,30 @@
14
 
15
  // active tools are all the checked tools, either from settings or on by default
16
  $: activeToolCount = $page.data.tools.filter(
17
- (tool: ToolFront) => $settings?.tools?.[tool.name] ?? tool.isOnByDefault
 
 
18
  ).length;
19
 
20
- function setAllTools(value: boolean) {
21
- settings.instantSet({
22
- tools: Object.fromEntries($page.data.tools.map((tool: ToolFront) => [tool.name, value])),
23
- });
 
 
 
 
 
 
 
 
 
 
24
  }
 
25
  $: allToolsEnabled = activeToolCount === $page.data.tools.length;
 
 
26
  </script>
27
 
28
  <details
@@ -67,24 +83,53 @@
67
  {/if}
68
  </button>
69
  </div>
70
- {#each $page.data.tools as tool}
71
- {@const isChecked = $settings?.tools?.[tool.name] ?? tool.isOnByDefault}
 
 
 
 
 
 
 
 
 
 
 
 
72
  <div class="flex items-center gap-1.5">
73
- <input
74
- type="checkbox"
75
- id={tool.name}
76
- checked={isChecked}
77
- disabled={loading}
78
- on:click={async () => {
79
- await settings.instantSet({
80
- tools: {
81
- ...$settings.tools,
82
- [tool.name]: !isChecked,
83
- },
84
- });
85
- }}
86
- />
87
- <label class="cursor-pointer" for={tool.name}>{tool.displayName ?? tool.name} </label>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  </div>
89
  {/each}
90
  </div>
 
1
  <script lang="ts">
2
+ import { base } from "$app/paths";
3
  import { page } from "$app/stores";
4
  import { clickOutside } from "$lib/actions/clickOutside";
5
  import { useSettingsStore } from "$lib/stores/settings";
 
15
 
16
  // active tools are all the checked tools, either from settings or on by default
17
  $: activeToolCount = $page.data.tools.filter(
18
+ (tool: ToolFront) =>
19
+ // community tools are always on by default
20
+ tool.type === "community" || $settings?.tools?.includes(tool._id)
21
  ).length;
22
 
23
+ async function setAllTools(value: boolean) {
24
+ const configToolsIds = $page.data.tools
25
+ .filter((t: ToolFront) => t.type === "config")
26
+ .map((t: ToolFront) => t._id);
27
+
28
+ if (value) {
29
+ await settings.instantSet({
30
+ tools: Array.from(new Set([...configToolsIds, ...($settings?.tools ?? [])])),
31
+ });
32
+ } else {
33
+ await settings.instantSet({
34
+ tools: [],
35
+ });
36
+ }
37
  }
38
+
39
  $: allToolsEnabled = activeToolCount === $page.data.tools.length;
40
+
41
+ $: tools = $page.data.tools;
42
  </script>
43
 
44
  <details
 
83
  {/if}
84
  </button>
85
  </div>
86
+ <!-- XXX: feature_flag_tools -->
87
+ {#if $page.data.user?.isEarlyAccess}
88
+ <a
89
+ href="{base}/tools"
90
+ class="col-span-2 my-1 h-fit w-fit items-center justify-center rounded-full bg-purple-500/20 px-2.5 py-1.5 text-sm hover:bg-purple-500/30"
91
+ >
92
+ <span class="mr-1 rounded-full bg-purple-700 px-1.5 py-1 text-xs font-bold uppercase">
93
+ new
94
+ </span>
95
+ Browse community tools ({$page.data.communityToolCount ?? 0})
96
+ </a>
97
+ {/if}
98
+ {#each tools as tool}
99
+ {@const isChecked = $settings?.tools?.includes(tool._id)}
100
  <div class="flex items-center gap-1.5">
101
+ {#if tool.type === "community"}
102
+ <input
103
+ type="checkbox"
104
+ id={tool._id}
105
+ checked={true}
106
+ class="rounded-xs font-semibold accent-purple-500 hover:accent-purple-600"
107
+ on:click|stopPropagation|preventDefault={async () => {
108
+ await settings.instantSet({
109
+ tools: $settings?.tools?.filter((t) => t !== tool._id) ?? [],
110
+ });
111
+ }}
112
+ />
113
+ {:else}
114
+ <input
115
+ type="checkbox"
116
+ id={tool._id}
117
+ checked={isChecked}
118
+ disabled={loading}
119
+ on:click|stopPropagation={async () => {
120
+ if (isChecked) {
121
+ await settings.instantSet({
122
+ tools: ($settings?.tools ?? []).filter((t) => t !== tool._id),
123
+ });
124
+ } else {
125
+ await settings.instantSet({
126
+ tools: [...($settings?.tools ?? []), tool._id],
127
+ });
128
+ }
129
+ }}
130
+ />
131
+ {/if}
132
+ <label class="cursor-pointer" for={tool._id}>{tool.displayName} </label>
133
  </div>
134
  {/each}
135
  </div>
src/lib/components/chat/ChatWindow.svelte CHANGED
@@ -155,8 +155,8 @@
155
  const settings = useSettingsStore();
156
 
157
  // active tools are all the checked tools, either from settings or on by default
158
- $: activeTools = $page.data.tools.filter(
159
- (tool: ToolFront) => $settings?.tools?.[tool.name] ?? tool.isOnByDefault
160
  );
161
  $: activeMimeTypes = [
162
  ...(!$page.data?.assistant && currentModel.tools
 
155
  const settings = useSettingsStore();
156
 
157
  // active tools are all the checked tools, either from settings or on by default
158
+ $: activeTools = $page.data.tools.filter((tool: ToolFront) =>
159
+ $settings?.tools?.includes(tool._id)
160
  );
161
  $: activeMimeTypes = [
162
  ...(!$page.data?.assistant && currentModel.tools
src/lib/components/chat/ToolUpdate.svelte CHANGED
@@ -7,7 +7,6 @@
7
  } from "$lib/utils/messageUpdates";
8
 
9
  import CarbonTools from "~icons/carbon/tools";
10
- import { toolHasName } from "$lib/utils/tools";
11
  import type { ToolFront } from "$lib/types/Tool";
12
  import { page } from "$app/stores";
13
  import { onMount } from "svelte";
@@ -16,7 +15,7 @@
16
  export let tool: MessageToolUpdate[];
17
  export let loading: boolean = false;
18
 
19
- const toolName = tool.find(isMessageToolCallUpdate)?.call.name;
20
  $: toolError = tool.some(isMessageToolErrorUpdate);
21
  $: toolDone = tool.some(isMessageToolResultUpdate);
22
 
@@ -26,12 +25,13 @@
26
  let animation: Animation | undefined = undefined;
27
 
28
  let isShowingLoadingBar = false;
 
29
  onMount(() => {
30
  if (!toolError && !toolDone && loading && loadingBarEl) {
31
  loadingBarEl.classList.remove("hidden");
32
  isShowingLoadingBar = true;
33
  animation = loadingBarEl.animate([{ width: "0%" }, { width: "calc(100%+1rem)" }], {
34
- duration: availableTools.find((tool) => tool.name === toolName)?.timeToUseMS,
35
  fill: "forwards",
36
  });
37
  }
@@ -63,7 +63,7 @@
63
  })();
64
  </script>
65
 
66
- {#if toolName && toolName !== "websearch"}
67
  <details
68
  class="group/tool my-2.5 w-fit cursor-pointer rounded-lg border border-gray-200 bg-white pl-1 pr-2.5 text-sm shadow-sm transition-all open:mb-3
69
  open:border-purple-500/10 open:bg-purple-600/5 open:shadow-sm dark:border-gray-800 dark:bg-gray-900 open:dark:border-purple-800/40 open:dark:bg-purple-800/10"
@@ -103,7 +103,8 @@
103
  <span>
104
  {toolError ? "Error calling" : toolDone ? "Called" : "Calling"} tool
105
  <span class="font-semibold"
106
- >{availableTools.find((el) => toolHasName(toolName, el))?.displayName}</span
 
107
  >
108
  </span>
109
  </summary>
@@ -115,10 +116,12 @@
115
  </div>
116
  <ul class="py-1 text-sm">
117
  {#each Object.entries(toolUpdate.call.parameters ?? {}) as [k, v]}
118
- <li>
119
- <span class="font-semibold">{k}</span>:
120
- <span>{v}</span>
121
- </li>
 
 
122
  {/each}
123
  </ul>
124
  {:else if toolUpdate.subtype === MessageToolUpdateType.Error}
 
7
  } from "$lib/utils/messageUpdates";
8
 
9
  import CarbonTools from "~icons/carbon/tools";
 
10
  import type { ToolFront } from "$lib/types/Tool";
11
  import { page } from "$app/stores";
12
  import { onMount } from "svelte";
 
15
  export let tool: MessageToolUpdate[];
16
  export let loading: boolean = false;
17
 
18
+ const toolFnName = tool.find(isMessageToolCallUpdate)?.call.name;
19
  $: toolError = tool.some(isMessageToolErrorUpdate);
20
  $: toolDone = tool.some(isMessageToolResultUpdate);
21
 
 
25
  let animation: Animation | undefined = undefined;
26
 
27
  let isShowingLoadingBar = false;
28
+
29
  onMount(() => {
30
  if (!toolError && !toolDone && loading && loadingBarEl) {
31
  loadingBarEl.classList.remove("hidden");
32
  isShowingLoadingBar = true;
33
  animation = loadingBarEl.animate([{ width: "0%" }, { width: "calc(100%+1rem)" }], {
34
+ duration: availableTools.find((tool) => tool.name === toolFnName)?.timeToUseMS,
35
  fill: "forwards",
36
  });
37
  }
 
63
  })();
64
  </script>
65
 
66
+ {#if toolFnName && toolFnName !== "websearch"}
67
  <details
68
  class="group/tool my-2.5 w-fit cursor-pointer rounded-lg border border-gray-200 bg-white pl-1 pr-2.5 text-sm shadow-sm transition-all open:mb-3
69
  open:border-purple-500/10 open:bg-purple-600/5 open:shadow-sm dark:border-gray-800 dark:bg-gray-900 open:dark:border-purple-800/40 open:dark:bg-purple-800/10"
 
103
  <span>
104
  {toolError ? "Error calling" : toolDone ? "Called" : "Calling"} tool
105
  <span class="font-semibold"
106
+ >{availableTools.find((tool) => tool.name === toolFnName)?.displayName ??
107
+ toolFnName}</span
108
  >
109
  </span>
110
  </summary>
 
116
  </div>
117
  <ul class="py-1 text-sm">
118
  {#each Object.entries(toolUpdate.call.parameters ?? {}) as [k, v]}
119
+ {#if v !== null}
120
+ <li>
121
+ <span class="font-semibold">{k}</span>:
122
+ <span>{v}</span>
123
+ </li>
124
+ {/if}
125
  {/each}
126
  </ul>
127
  {:else if toolUpdate.subtype === MessageToolUpdateType.Error}
src/lib/migrations/routines/03-add-tools-in-settings.ts CHANGED
@@ -14,7 +14,7 @@ const addToolsToSettings: Migration = {
14
  {
15
  tools: { $exists: false },
16
  },
17
- { $set: { tools: {} } }
18
  );
19
 
20
  settings
 
14
  {
15
  tools: { $exists: false },
16
  },
17
+ { $set: { tools: [] } }
18
  );
19
 
20
  settings
src/lib/migrations/routines/07-reset-tools-in-settings.ts CHANGED
@@ -8,7 +8,7 @@ const resetTools: Migration = {
8
  up: async () => {
9
  const { settings } = collections;
10
 
11
- await settings.updateMany({}, { $set: { tools: {} } });
12
 
13
  return true;
14
  },
 
8
  up: async () => {
9
  const { settings } = collections;
10
 
11
+ await settings.updateMany({}, { $set: { tools: [] } });
12
 
13
  return true;
14
  },
src/lib/migrations/routines/08-reset-tools-in-settings-2.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Migration } from ".";
2
+ import { collections } from "$lib/server/database";
3
+ import { ObjectId } from "mongodb";
4
+
5
+ const resetTools2: Migration = {
6
+ _id: new ObjectId("000000000008"),
7
+ name: "Reset tools to empty",
8
+ up: async () => {
9
+ const { settings } = collections;
10
+
11
+ await settings.updateMany({}, { $set: { tools: [] } });
12
+
13
+ return true;
14
+ },
15
+ runEveryTime: false,
16
+ runForHuggingChat: "only",
17
+ };
18
+
19
+ export default resetTools2;
src/lib/migrations/routines/index.ts CHANGED
@@ -8,6 +8,7 @@ import updateMessageUpdates from "./04-update-message-updates";
8
  import updateMessageFiles from "./05-update-message-files";
9
  import trimMessageUpdates from "./06-trim-message-updates";
10
  import resetTools from "./07-reset-tools-in-settings";
 
11
 
12
  export interface Migration {
13
  _id: ObjectId;
@@ -27,4 +28,5 @@ export const migrations: Migration[] = [
27
  updateMessageFiles,
28
  trimMessageUpdates,
29
  resetTools,
 
30
  ];
 
8
  import updateMessageFiles from "./05-update-message-files";
9
  import trimMessageUpdates from "./06-trim-message-updates";
10
  import resetTools from "./07-reset-tools-in-settings";
11
+ import resetTools2 from "./08-reset-tools-in-settings-2";
12
 
13
  export interface Migration {
14
  _id: ObjectId;
 
28
  updateMessageFiles,
29
  trimMessageUpdates,
30
  resetTools,
31
+ resetTools2,
32
  ];
src/lib/server/database.ts CHANGED
@@ -13,6 +13,8 @@ import type { ConversationStats } from "$lib/types/ConversationStats";
13
  import type { MigrationResult } from "$lib/types/MigrationResult";
14
  import type { Semaphore } from "$lib/types/Semaphore";
15
  import type { AssistantStats } from "$lib/types/AssistantStats";
 
 
16
  import { logger } from "$lib/server/logger";
17
  import { building } from "$app/environment";
18
  import { onExit } from "./exitHandler";
@@ -83,6 +85,7 @@ export class Database {
83
  const bucket = new GridFSBucket(db, { bucketName: "files" });
84
  const migrationResults = db.collection<MigrationResult>("migrationResults");
85
  const semaphores = db.collection<Semaphore>("semaphores");
 
86
 
87
  return {
88
  conversations,
@@ -99,6 +102,7 @@ export class Database {
99
  bucket,
100
  migrationResults,
101
  semaphores,
 
102
  };
103
  }
104
 
@@ -120,6 +124,7 @@ export class Database {
120
  sessions,
121
  messageEvents,
122
  semaphores,
 
123
  } = this.getCollections();
124
 
125
  conversations
@@ -209,6 +214,10 @@ export class Database {
209
  semaphores
210
  .createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 })
211
  .catch((e) => logger.error(e));
 
 
 
 
212
  }
213
  }
214
 
 
13
  import type { MigrationResult } from "$lib/types/MigrationResult";
14
  import type { Semaphore } from "$lib/types/Semaphore";
15
  import type { AssistantStats } from "$lib/types/AssistantStats";
16
+ import type { CommunityToolDB } from "$lib/types/Tool";
17
+
18
  import { logger } from "$lib/server/logger";
19
  import { building } from "$app/environment";
20
  import { onExit } from "./exitHandler";
 
85
  const bucket = new GridFSBucket(db, { bucketName: "files" });
86
  const migrationResults = db.collection<MigrationResult>("migrationResults");
87
  const semaphores = db.collection<Semaphore>("semaphores");
88
+ const tools = db.collection<CommunityToolDB>("tools");
89
 
90
  return {
91
  conversations,
 
102
  bucket,
103
  migrationResults,
104
  semaphores,
105
+ tools,
106
  };
107
  }
108
 
 
124
  sessions,
125
  messageEvents,
126
  semaphores,
127
+ tools,
128
  } = this.getCollections();
129
 
130
  conversations
 
214
  semaphores
215
  .createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 })
216
  .catch((e) => logger.error(e));
217
+
218
+ tools.createIndex({ createdById: 1, userCount: -1 }).catch((e) => logger.error(e));
219
+ tools.createIndex({ userCount: 1 }).catch((e) => logger.error(e));
220
+ tools.createIndex({ last24HoursCount: 1 }).catch((e) => logger.error(e));
221
  }
222
  }
223
 
src/lib/server/metrics.ts CHANGED
@@ -3,7 +3,6 @@ import express from "express";
3
  import { logger } from "$lib/server/logger";
4
  import { env } from "$env/dynamic/private";
5
  import type { Model } from "$lib/types/Model";
6
- import type { Tool } from "$lib/types/Tool";
7
  import { onExit } from "./exitHandler";
8
  import { promisify } from "util";
9
 
@@ -26,9 +25,9 @@ interface Metrics {
26
  };
27
 
28
  tool: {
29
- toolUseCount: Counter<Tool["name"]>;
30
- toolUseCountError: Counter<Tool["name"]>;
31
- toolUseDuration: Summary<Tool["name"]>;
32
  timeToChooseTools: Summary;
33
  };
34
  }
 
3
  import { logger } from "$lib/server/logger";
4
  import { env } from "$env/dynamic/private";
5
  import type { Model } from "$lib/types/Model";
 
6
  import { onExit } from "./exitHandler";
7
  import { promisify } from "util";
8
 
 
25
  };
26
 
27
  tool: {
28
+ toolUseCount: Counter<string>;
29
+ toolUseCountError: Counter<string>;
30
+ toolUseDuration: Summary<string>;
31
  timeToChooseTools: Summary;
32
  };
33
  }
src/lib/server/models.ts CHANGED
@@ -12,7 +12,7 @@ import type { PreTrainedTokenizer } from "@xenova/transformers";
12
  import JSON5 from "json5";
13
  import { getTokenizer } from "$lib/utils/getTokenizer";
14
  import { logger } from "$lib/server/logger";
15
- import { ToolResultStatus } from "$lib/types/Tool";
16
 
17
  type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
18
 
@@ -96,14 +96,11 @@ async function getChatPromptRender(
96
 
97
  const renderTemplate = ({ messages, preprompt, tools, toolResults }: ChatTemplateInput) => {
98
  let formattedMessages: { role: string; content: string }[] = messages.map((message) => ({
99
- content:
100
- message.files?.length && !tools?.length
101
- ? message.content + `\n This message has ${message.files.length} files attached`
102
- : message.content,
103
  role: message.from,
104
  }));
105
 
106
- if (preprompt) {
107
  formattedMessages = [
108
  {
109
  role: "system",
@@ -195,17 +192,41 @@ async function getChatPromptRender(
195
  );
196
  });
197
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  const output = tokenizer.apply_chat_template(formattedMessages, {
199
  tokenize: false,
200
  add_generation_prompt: true,
201
  chat_template: chatTemplate,
202
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
203
  // @ts-ignore
204
- tools:
205
- tools?.map(({ parameterDefinitions, ...tool }) => ({
206
- parameter_definitions: parameterDefinitions,
207
- ...tool,
208
- })) ?? [],
209
  documents,
210
  });
211
 
 
12
  import JSON5 from "json5";
13
  import { getTokenizer } from "$lib/utils/getTokenizer";
14
  import { logger } from "$lib/server/logger";
15
+ import { ToolResultStatus, type ToolInput } from "$lib/types/Tool";
16
 
17
  type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
18
 
 
96
 
97
  const renderTemplate = ({ messages, preprompt, tools, toolResults }: ChatTemplateInput) => {
98
  let formattedMessages: { role: string; content: string }[] = messages.map((message) => ({
99
+ content: message.content,
 
 
 
100
  role: message.from,
101
  }));
102
 
103
+ if (preprompt && formattedMessages[0].role !== "system") {
104
  formattedMessages = [
105
  {
106
  role: "system",
 
192
  );
193
  });
194
 
195
+ const mappedTools =
196
+ tools?.map((tool) => {
197
+ const inputs: Record<
198
+ string,
199
+ {
200
+ type: ToolInput["type"];
201
+ description: string;
202
+ required: boolean;
203
+ }
204
+ > = {};
205
+
206
+ for (const value of tool.inputs) {
207
+ if (value.paramType !== "fixed") {
208
+ inputs[value.name] = {
209
+ type: value.type,
210
+ description: value.description ?? "",
211
+ required: value.paramType === "required",
212
+ };
213
+ }
214
+ }
215
+
216
+ return {
217
+ name: tool.name,
218
+ description: tool.description,
219
+ parameter_definitions: inputs,
220
+ };
221
+ }) ?? [];
222
+
223
  const output = tokenizer.apply_chat_template(formattedMessages, {
224
  tokenize: false,
225
  add_generation_prompt: true,
226
  chat_template: chatTemplate,
227
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
228
  // @ts-ignore
229
+ tools: mappedTools,
 
 
 
 
230
  documents,
231
  });
232
 
src/lib/server/textGeneration/index.ts CHANGED
@@ -8,7 +8,7 @@ import {
8
  getAssistantById,
9
  processPreprompt,
10
  } from "./assistant";
11
- import { pickTools, runTools } from "./tools";
12
  import type { WebSearch } from "$lib/types/WebSearch";
13
  import {
14
  type MessageUpdate,
@@ -20,7 +20,6 @@ import { mergeAsyncGenerators } from "$lib/utils/mergeAsyncGenerators";
20
  import type { TextGenerationContext } from "./types";
21
  import type { ToolResult } from "$lib/types/Tool";
22
  import { toolHasName } from "../tools/utils";
23
- import directlyAnswer from "../tools/directlyAnswer";
24
 
25
  export async function* textGeneration(ctx: TextGenerationContext) {
26
  yield* mergeAsyncGenerators([
@@ -63,8 +62,8 @@ async function* textGenerationWithoutTitle(
63
  let toolResults: ToolResult[] = [];
64
 
65
  if (model.tools && !conv.assistantId) {
66
- const tools = pickTools(toolsPreference, Boolean(assistant));
67
- const toolCallsRequired = tools.some((tool) => !toolHasName(directlyAnswer.name, tool));
68
  if (toolCallsRequired) toolResults = yield* runTools(ctx, tools, preprompt);
69
  }
70
 
 
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,
 
20
  import type { TextGenerationContext } from "./types";
21
  import type { ToolResult } from "$lib/types/Tool";
22
  import { toolHasName } from "../tools/utils";
 
23
 
24
  export async function* textGeneration(ctx: TextGenerationContext) {
25
  yield* mergeAsyncGenerators([
 
62
  let toolResults: ToolResult[] = [];
63
 
64
  if (model.tools && !conv.assistantId) {
65
+ const tools = await filterToolsOnPreferences(toolsPreference, Boolean(assistant));
66
+ const toolCallsRequired = tools.some((tool) => !toolHasName("directly_answer", tool));
67
  if (toolCallsRequired) toolResults = yield* runTools(ctx, tools, preprompt);
68
  }
69
 
src/lib/server/textGeneration/tools.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { ToolResultStatus, type ToolCall, type ToolResult } from "$lib/types/Tool";
2
  import { v4 as uuidV4 } from "uuid";
3
- import type { BackendTool, BackendToolContext } from "../tools";
4
  import {
5
  MessageToolUpdateType,
6
  MessageUpdateStatus,
@@ -9,48 +9,44 @@ import {
9
  } from "$lib/types/MessageUpdate";
10
  import type { TextGenerationContext } from "./types";
11
 
12
- import { allTools } from "../tools";
13
  import directlyAnswer from "../tools/directlyAnswer";
14
  import websearch from "../tools/web/search";
15
  import { z } from "zod";
16
  import { logger } from "../logger";
17
  import { extractJson, toolHasName } from "../tools/utils";
18
- import type { MessageFile } from "$lib/types/Message";
19
  import { mergeAsyncGenerators } from "$lib/utils/mergeAsyncGenerators";
20
  import { MetricsServer } from "../metrics";
21
  import { stringifyError } from "$lib/utils/stringifyError";
 
 
 
22
 
23
- function makeFilesPrompt(files: MessageFile[], fileMessageIndex: number): string {
24
- if (files.length === 0) {
25
- return "The user has not uploaded any files. Do not attempt to use any tools that require files";
26
- }
27
-
28
- const stringifiedFiles = files
29
- .map(
30
- (file, fileIndex) =>
31
- ` - fileMessageIndex ${fileMessageIndex} | fileIndex ${fileIndex} | ${file.name} (${file.mime})`
32
- )
33
- .join("\n");
34
- return `Attached ${files.length} file${files.length === 1 ? "" : "s"}:\n${stringifiedFiles}`;
35
- }
36
-
37
- export function pickTools(
38
- toolsPreference: Record<string, boolean>,
39
  isAssistant: boolean
40
- ): BackendTool[] {
41
  // if it's an assistant, only support websearch for now
42
  if (isAssistant) return [directlyAnswer, websearch];
43
 
44
  // filter based on tool preferences, add the tools that are on by default
45
- return allTools.filter((el) => {
46
  if (el.isLocked && el.isOnByDefault) return true;
47
- return toolsPreference?.[el.name] ?? el.isOnByDefault;
48
  });
 
 
 
 
 
 
 
 
 
49
  }
50
 
51
  async function* callTool(
52
  ctx: BackendToolContext,
53
- tools: BackendTool[],
54
  call: ToolCall
55
  ): AsyncGenerator<MessageUpdate, ToolResult | undefined, undefined> {
56
  const uuid = uuidV4();
@@ -88,6 +84,8 @@ async function* callTool(
88
  Date.now() - startTime
89
  );
90
 
 
 
91
  return { ...toolResult, call } as ToolResult;
92
  } catch (error) {
93
  MetricsServer.getMetrics().tool.toolUseCountError.inc({ tool: call.name });
@@ -110,28 +108,75 @@ async function* callTool(
110
 
111
  export async function* runTools(
112
  ctx: TextGenerationContext,
113
- tools: BackendTool[],
114
  preprompt?: string
115
  ): AsyncGenerator<MessageUpdate, ToolResult[], undefined> {
116
  const { endpoint, conv, messages, assistant, ip, username } = ctx;
117
  const calls: ToolCall[] = [];
118
 
119
- const messagesWithFilesPrompt = messages.map((message, idx) => {
120
- if (!message.files?.length) return message;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  return {
122
  ...message,
123
- content: `${message.content}\n${makeFilesPrompt(message.files, idx)}`,
124
- };
125
  });
126
 
127
- const pickToolStartTime = Date.now();
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
  // do the function calling bits here
130
  for await (const output of await endpoint({
131
- messages: messagesWithFilesPrompt,
132
  preprompt,
133
  generateSettings: assistant?.generateSettings,
134
- tools,
 
 
 
 
 
 
135
  })) {
136
  // model natively supports tool calls
137
  if (output.token.toolCalls) {
@@ -146,7 +191,7 @@ export async function* runTools(
146
  const rawCalls = await extractJson(output.generated_text);
147
  const newCalls = rawCalls
148
  .filter(isExternalToolCall)
149
- .map(externalToToolCall)
150
  .filter((call) => call !== undefined) as ToolCall[];
151
 
152
  calls.push(...newCalls);
@@ -185,9 +230,10 @@ function isExternalToolCall(call: unknown): call is ExternalToolCall {
185
  return externalToolCall.safeParse(call).success;
186
  }
187
 
188
- function externalToToolCall(call: ExternalToolCall): ToolCall | undefined {
189
  // Convert - to _ since some models insist on using _ instead of -
190
- const tool = allTools.find((tool) => toolHasName(call.tool_name, tool));
 
191
  if (!tool) {
192
  logger.debug(`Model requested tool that does not exist: "${call.tool_name}". Skipping tool...`);
193
  return;
@@ -195,23 +241,27 @@ function externalToToolCall(call: ExternalToolCall): ToolCall | undefined {
195
 
196
  const parametersWithDefaults: Record<string, string> = {};
197
 
198
- for (const [key, definition] of Object.entries(tool.parameterDefinitions)) {
199
- const value = call.parameters[key];
200
 
201
  // Required so ensure it's there, otherwise return undefined
202
- if (definition.required) {
203
  if (value === undefined) {
204
  logger.debug(
205
- `Model requested tool "${call.tool_name}" but was missing required parameter "${key}". Skipping tool...`
206
  );
207
  return;
208
  }
209
- parametersWithDefaults[key] = value;
210
  continue;
211
  }
212
 
213
  // Optional so use default if not there
214
- parametersWithDefaults[key] = value ?? definition.default;
 
 
 
 
215
  }
216
 
217
  return {
 
1
+ import { ToolResultStatus, type ToolCall, type Tool, type ToolResult } from "$lib/types/Tool";
2
  import { v4 as uuidV4 } from "uuid";
3
+ import { getCallMethod, toolFromConfigs, type BackendToolContext } from "../tools";
4
  import {
5
  MessageToolUpdateType,
6
  MessageUpdateStatus,
 
9
  } from "$lib/types/MessageUpdate";
10
  import type { TextGenerationContext } from "./types";
11
 
 
12
  import directlyAnswer from "../tools/directlyAnswer";
13
  import websearch from "../tools/web/search";
14
  import { z } from "zod";
15
  import { logger } from "../logger";
16
  import { extractJson, toolHasName } from "../tools/utils";
 
17
  import { mergeAsyncGenerators } from "$lib/utils/mergeAsyncGenerators";
18
  import { MetricsServer } from "../metrics";
19
  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
 
47
  async function* callTool(
48
  ctx: BackendToolContext,
49
+ tools: Tool[],
50
  call: ToolCall
51
  ): AsyncGenerator<MessageUpdate, ToolResult | undefined, undefined> {
52
  const uuid = uuidV4();
 
84
  Date.now() - startTime
85
  );
86
 
87
+ await collections.tools.findOneAndUpdate({ _id: tool._id }, { $inc: { useCount: 1 } });
88
+
89
  return { ...toolResult, call } as ToolResult;
90
  } catch (error) {
91
  MetricsServer.getMetrics().tool.toolUseCountError.inc({ tool: call.name });
 
108
 
109
  export async function* runTools(
110
  ctx: TextGenerationContext,
111
+ tools: Tool[],
112
  preprompt?: string
113
  ): AsyncGenerator<MessageUpdate, ToolResult[], undefined> {
114
  const { endpoint, conv, messages, assistant, ip, username } = ctx;
115
  const calls: ToolCall[] = [];
116
 
117
+ const pickToolStartTime = Date.now();
118
+ // append a message with the list of all available files
119
+
120
+ const files = messages.reduce((acc, curr, idx) => {
121
+ if (curr.files) {
122
+ const prefix = (curr.from === "user" ? "input" : "ouput") + "_" + idx;
123
+ acc.push(
124
+ ...curr.files.map(
125
+ (file, fileIdx) => `${prefix}_${fileIdx}.${file?.name?.split(".")?.pop()?.toLowerCase()}`
126
+ )
127
+ );
128
+ }
129
+ return acc;
130
+ }, [] as string[]);
131
+
132
+ let formattedMessages = messages.map((message, msgIdx) => {
133
+ let content = message.content;
134
+
135
+ if (message.files && message.files.length > 0) {
136
+ content +=
137
+ "\n\nAdded files: \n - " +
138
+ message.files
139
+ .map((file, fileIdx) => {
140
+ const prefix = message.from === "user" ? "input" : "output";
141
+ const fileName = file.name.split(".").pop()?.toLowerCase();
142
+
143
+ return `${prefix}_${msgIdx}_${fileIdx}.${fileName}`;
144
+ })
145
+ .join("\n - ");
146
+ }
147
+
148
  return {
149
  ...message,
150
+ content,
151
+ } satisfies Message;
152
  });
153
 
154
+ const fileMsg = {
155
+ id: crypto.randomUUID(),
156
+ from: "system",
157
+ content:
158
+ "Here is the list of available filenames that can be used as input for tools. Use the filenames that are in this list. \n The filename structure is as follows : {input for user|output for tool}_{message index in the conversation}_{file index in the list of files}.{file extension} \n - " +
159
+ files.join("\n - ") +
160
+ "\n\n\n",
161
+ } satisfies Message;
162
+
163
+ // put fileMsg before last if files.length > 0
164
+ formattedMessages = files.length
165
+ ? [...formattedMessages.slice(0, -1), fileMsg, ...formattedMessages.slice(-1)]
166
+ : messages;
167
 
168
  // do the function calling bits here
169
  for await (const output of await endpoint({
170
+ messages: formattedMessages,
171
  preprompt,
172
  generateSettings: assistant?.generateSettings,
173
+ tools: tools.map((tool) => ({
174
+ ...tool,
175
+ inputs: tool.inputs.map((input) => ({
176
+ ...input,
177
+ type: input.type === "file" ? "str" : input.type,
178
+ })),
179
+ })),
180
  })) {
181
  // model natively supports tool calls
182
  if (output.token.toolCalls) {
 
191
  const rawCalls = await extractJson(output.generated_text);
192
  const newCalls = rawCalls
193
  .filter(isExternalToolCall)
194
+ .map((call) => externalToToolCall(call, tools))
195
  .filter((call) => call !== undefined) as ToolCall[];
196
 
197
  calls.push(...newCalls);
 
230
  return externalToolCall.safeParse(call).success;
231
  }
232
 
233
+ function externalToToolCall(call: ExternalToolCall, tools: Tool[]): ToolCall | undefined {
234
  // Convert - to _ since some models insist on using _ instead of -
235
+ const tool = tools.find((tool) => toolHasName(call.tool_name, tool));
236
+
237
  if (!tool) {
238
  logger.debug(`Model requested tool that does not exist: "${call.tool_name}". Skipping tool...`);
239
  return;
 
241
 
242
  const parametersWithDefaults: Record<string, string> = {};
243
 
244
+ for (const input of tool.inputs) {
245
+ const value = call.parameters[input.name];
246
 
247
  // Required so ensure it's there, otherwise return undefined
248
+ if (input.paramType === "required") {
249
  if (value === undefined) {
250
  logger.debug(
251
+ `Model requested tool "${call.tool_name}" but was missing required parameter "${input.name}". Skipping tool...`
252
  );
253
  return;
254
  }
255
+ parametersWithDefaults[input.name] = value;
256
  continue;
257
  }
258
 
259
  // Optional so use default if not there
260
+ parametersWithDefaults[input.name] = value;
261
+
262
+ if (input.paramType === "optional") {
263
+ parametersWithDefaults[input.name] ??= input.default.toString();
264
+ }
265
  }
266
 
267
  return {
src/lib/server/textGeneration/types.ts CHANGED
@@ -12,7 +12,7 @@ export interface TextGenerationContext {
12
  assistant?: Pick<Assistant, "rag" | "dynamicPrompt" | "generateSettings">;
13
  isContinue: boolean;
14
  webSearch: boolean;
15
- toolsPreference: Record<string, boolean>;
16
  promptedAt: Date;
17
  ip: string;
18
  username?: string;
 
12
  assistant?: Pick<Assistant, "rag" | "dynamicPrompt" | "generateSettings">;
13
  isContinue: boolean;
14
  webSearch: boolean;
15
+ toolsPreference: Array<string>;
16
  promptedAt: Date;
17
  ip: string;
18
  username?: string;
src/lib/server/tools/calculator.ts CHANGED
@@ -1,29 +1,38 @@
1
- import type { BackendTool } from ".";
 
2
  import vm from "node:vm";
3
 
4
- const calculator: BackendTool = {
5
- name: "query_calculator",
 
 
 
 
6
  displayName: "Calculator",
7
- description:
8
- "A simple calculator, takes a string containing a mathematical expression and returns the answer. Only supports +, -, *, ** (power) and /, as well as parenthesis ().",
9
- parameterDefinitions: {
10
- equation: {
 
 
11
  description:
12
- "The formula to evaluate. EXACTLY as you would plug into a calculator. No words, no letters, only numbers and operators. Letters will make the tool crash.",
13
- type: "formula",
14
- required: true,
15
  },
16
- },
17
- async *call(params) {
 
 
 
18
  try {
19
- const blocks = String(params.equation).split("\n");
20
  const query = blocks[blocks.length - 1].replace(/[^-()\d/*+.]/g, "");
21
 
22
  return {
23
  outputs: [{ calculator: `${query} = ${vm.runInNewContext(query)}` }],
24
  };
25
  } catch (cause) {
26
- throw Error("Invalid expression", { cause });
27
  }
28
  },
29
  };
 
1
+ import type { ConfigTool } from "$lib/types/Tool";
2
+ import { ObjectId } from "mongodb";
3
  import vm from "node:vm";
4
 
5
+ const calculator: ConfigTool = {
6
+ _id: new ObjectId("00000000000000000000000C"),
7
+ type: "config",
8
+ description: "Calculate the result of a mathematical expression",
9
+ color: "blue",
10
+ icon: "code",
11
  displayName: "Calculator",
12
+ name: "calculator",
13
+ endpoint: null,
14
+ inputs: [
15
+ {
16
+ name: "equation",
17
+ type: "str",
18
  description:
19
+ "A mathematical expression to be evaluated. The result of the expression will be returned.",
20
+ paramType: "required",
 
21
  },
22
+ ],
23
+ outputComponent: null,
24
+ outputComponentIdx: null,
25
+ showOutput: false,
26
+ async *call({ equation }) {
27
  try {
28
+ const blocks = String(equation).split("\n");
29
  const query = blocks[blocks.length - 1].replace(/[^-()\d/*+.]/g, "");
30
 
31
  return {
32
  outputs: [{ calculator: `${query} = ${vm.runInNewContext(query)}` }],
33
  };
34
  } catch (cause) {
35
+ throw new Error("Invalid expression", { cause });
36
  }
37
  },
38
  };
src/lib/server/tools/directlyAnswer.ts CHANGED
@@ -1,12 +1,22 @@
1
- import type { BackendTool } from ".";
 
2
 
3
- const directlyAnswer: BackendTool = {
4
- name: "directly_answer",
 
 
 
 
 
5
  isOnByDefault: true,
6
- isHidden: true,
7
  isLocked: true,
8
- description: "Use this tool to let the user know you wish to answer directly",
9
- parameterDefinitions: {},
 
 
 
 
 
10
  async *call() {
11
  return {
12
  outputs: [],
 
1
+ import type { ConfigTool } from "$lib/types/Tool";
2
+ import { ObjectId } from "mongodb";
3
 
4
+ const directlyAnswer: ConfigTool = {
5
+ _id: new ObjectId("00000000000000000000000D"),
6
+ type: "config",
7
+ description: "Answer the user's query directly",
8
+ color: "blue",
9
+ icon: "chat",
10
+ displayName: "Directly Answer",
11
  isOnByDefault: true,
 
12
  isLocked: true,
13
+ isHidden: true,
14
+ name: "directlyAnswer",
15
+ endpoint: null,
16
+ inputs: [],
17
+ outputComponent: null,
18
+ outputComponentIdx: null,
19
+ showOutput: false,
20
  async *call() {
21
  return {
22
  outputs: [],
src/lib/server/tools/documentParser.ts DELETED
@@ -1,60 +0,0 @@
1
- import type { BackendTool } from ".";
2
- import { callSpace, getIpToken } from "./utils";
3
- import { downloadFile } from "$lib/server/files/downloadFile";
4
-
5
- type PdfParserInput = [Blob /* pdf */, string /* filename */];
6
- type PdfParserOutput = [string /* markdown */, Record<string, unknown> /* metadata */];
7
-
8
- const documentParser: BackendTool = {
9
- name: "document_parser",
10
- displayName: "Document Parser",
11
- description: "Use this tool to parse any document and get its content in markdown format.",
12
- mimeTypes: ["application/*", "text/*"],
13
- parameterDefinitions: {
14
- fileMessageIndex: {
15
- description: "Index of the message containing the document file to parse",
16
- type: "number",
17
- required: true,
18
- },
19
- fileIndex: {
20
- description: "Index of the document file to parse",
21
- type: "number",
22
- required: true,
23
- },
24
- },
25
- async *call({ fileMessageIndex, fileIndex }, { conv, messages, ip, username }) {
26
- fileMessageIndex = Number(fileMessageIndex);
27
- fileIndex = Number(fileIndex);
28
-
29
- const message = messages[fileMessageIndex];
30
- const files = message?.files ?? [];
31
- if (!files || files.length === 0) throw Error("User did not provide a pdf to parse");
32
- if (fileIndex >= files.length) throw Error("Model provided an invalid file index");
33
-
34
- const file = files[fileIndex];
35
- const fileBlob = await downloadFile(files[fileIndex].value, conv._id)
36
- .then((file) => fetch(`data:${file.mime};base64,${file.value}`))
37
- .then((res) => res.blob());
38
-
39
- const ipToken = await getIpToken(ip, username);
40
-
41
- const outputs = await callSpace<PdfParserInput, PdfParserOutput>(
42
- "huggingchat/document-parser",
43
- "predict",
44
- [fileBlob, file.name],
45
- ipToken
46
- );
47
-
48
- let documentMarkdown = outputs[0];
49
- // TODO: quick fix for avoiding context limit. eventually should use the tokenizer
50
- if (documentMarkdown.length > 30_000) {
51
- documentMarkdown = documentMarkdown.slice(0, 30_000) + "\n\n... (truncated)";
52
- }
53
- return {
54
- outputs: [{ [file.name]: documentMarkdown }],
55
- display: false,
56
- };
57
- },
58
- };
59
-
60
- export default documentParser;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/server/tools/images/editing.ts DELETED
@@ -1,95 +0,0 @@
1
- import type { BackendTool } from "..";
2
- import { uploadFile } from "../../files/uploadFile";
3
- import { MessageUpdateType } from "$lib/types/MessageUpdate";
4
- import { callSpace, getIpToken, type GradioImage } from "../utils";
5
- import { downloadFile } from "$lib/server/files/downloadFile";
6
-
7
- type ImageEditingInput = [
8
- Blob /* image */,
9
- string /* prompt */,
10
- string /* negative prompt */,
11
- number /* guidance scale */,
12
- number /* steps */
13
- ];
14
- type ImageEditingOutput = [GradioImage];
15
-
16
- const imageEditing: BackendTool = {
17
- name: "image_editing",
18
- displayName: "Image Editing",
19
- description: "Use this tool to edit an image from a prompt.",
20
- mimeTypes: ["image/*"],
21
- parameterDefinitions: {
22
- prompt: {
23
- description:
24
- "A prompt to generate an image from. Describe the image visually in simple terms, separate terms with a comma.",
25
- type: "string",
26
- required: true,
27
- },
28
- fileMessageIndex: {
29
- description: "Index of the message containing the file to edit",
30
- type: "number",
31
- required: true,
32
- },
33
- fileIndex: {
34
- description: "Index of the file to edit",
35
- type: "number",
36
- required: true,
37
- },
38
- },
39
- async *call({ prompt, fileMessageIndex, fileIndex }, { conv, messages, ip, username }) {
40
- prompt = String(prompt);
41
- fileMessageIndex = Number(fileMessageIndex);
42
- fileIndex = Number(fileIndex);
43
-
44
- const message = messages[fileMessageIndex];
45
- const images = message?.files ?? [];
46
- if (!images || images.length === 0) throw Error("User did not provide an image to edit");
47
- if (fileIndex >= images.length) throw Error("Model provided an invalid file index");
48
- if (!images[fileIndex].mime.startsWith("image/")) {
49
- throw Error("Model provided a file idex which is not an image");
50
- }
51
-
52
- // todo: should handle multiple images
53
- const image = await downloadFile(images[fileIndex].value, conv._id)
54
- .then((file) => fetch(`data:${file.mime};base64,${file.value}`))
55
- .then((res) => res.blob());
56
-
57
- const ipToken = await getIpToken(ip, username);
58
-
59
- const outputs = await callSpace<ImageEditingInput, ImageEditingOutput>(
60
- "multimodalart/cosxl",
61
- "run_edit",
62
- [
63
- image,
64
- prompt,
65
- "", // negative prompt
66
- 7, // guidance scale
67
- 20, // steps
68
- ],
69
- ipToken
70
- );
71
-
72
- const outputImage = await fetch(outputs[0].url)
73
- .then((res) => res.blob())
74
- .then((blob) => new File([blob], outputs[0].orig_name, { type: blob.type }))
75
- .then((file) => uploadFile(file, conv));
76
-
77
- yield {
78
- type: MessageUpdateType.File,
79
- name: outputImage.name,
80
- sha: outputImage.value,
81
- mime: outputImage.mime,
82
- };
83
-
84
- return {
85
- outputs: [
86
- {
87
- imageEditing: `An image has been generated for the following prompt: "${prompt}". Answer as if the user can already see the image. Do not try to insert the image or to add space for it. The user can already see the image. Do not try to describe the image as you the model cannot see it. Be concise.`,
88
- },
89
- ],
90
- display: false,
91
- };
92
- },
93
- };
94
-
95
- export default imageEditing;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/server/tools/images/generation.ts DELETED
@@ -1,84 +0,0 @@
1
- import type { BackendTool } from "..";
2
- import { uploadFile } from "../../files/uploadFile";
3
- import { MessageUpdateType } from "$lib/types/MessageUpdate";
4
- import { callSpace, getIpToken, type GradioImage } from "../utils";
5
-
6
- type ImageGenerationInput = [string, string, number, boolean, number, number, number, number];
7
- type ImageGenerationOutput = [GradioImage, unknown];
8
-
9
- const imageGeneration: BackendTool = {
10
- name: "image_generation",
11
- displayName: "Image Generation",
12
- description: "Use this tool to generate an image from a prompt.",
13
- parameterDefinitions: {
14
- prompt: {
15
- description:
16
- "A prompt to generate an image from. Describe the image visually in simple terms, separate terms with a comma.",
17
- type: "string",
18
- required: true,
19
- },
20
- negativePrompt: {
21
- description:
22
- "A prompt for things that should not be in the image. Simple terms, separate terms with a comma.",
23
- type: "string",
24
- required: false,
25
- default: "",
26
- },
27
- width: {
28
- description: "Width of the generated image.",
29
- type: "number",
30
- required: false,
31
- default: 1024,
32
- },
33
- height: {
34
- description: "Height of the generated image.",
35
- type: "number",
36
- required: false,
37
- default: 1024,
38
- },
39
- },
40
- async *call({ prompt, negativePrompt, width, height }, { conv, ip, username }) {
41
- const ipToken = await getIpToken(ip, username);
42
-
43
- const outputs = await callSpace<ImageGenerationInput, ImageGenerationOutput>(
44
- "stabilityai/stable-diffusion-3-medium",
45
- "/infer",
46
- [
47
- String(prompt), // prompt
48
- String(negativePrompt), // negative prompt
49
- Math.floor(Math.random() * 1000), // seed random
50
- true, // randomize seed
51
- Number(width), // number in 'Image Width' Number component
52
- Number(height), // number in 'Image Height' Number component
53
- 5, // guidance scale
54
- 28, // steps
55
- ],
56
- ipToken
57
- );
58
- const image = await fetch(outputs[0].url)
59
- .then((res) => res.blob())
60
- .then(
61
- (blob) =>
62
- new File([blob], `${prompt}.${blob.type.split("/")[1] ?? "png"}`, { type: blob.type })
63
- )
64
- .then((file) => uploadFile(file, conv));
65
-
66
- yield {
67
- type: MessageUpdateType.File,
68
- name: image.name,
69
- sha: image.value,
70
- mime: image.mime,
71
- };
72
-
73
- return {
74
- outputs: [
75
- {
76
- imageGeneration: `An image has been generated for the following prompt: "${prompt}". Answer as if the user can already see the image. Do not try to insert the image or to add space for it. The user can already see the image. Do not try to describe the image as you the model cannot see it. Be concise.`,
77
- },
78
- ],
79
- display: false,
80
- };
81
- },
82
- };
83
-
84
- export default imageGeneration;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/server/tools/index.ts CHANGED
@@ -1,33 +1,294 @@
1
- import type { MessageUpdate } from "$lib/types/MessageUpdate";
2
- import type { Tool, ToolResultSuccess } from "$lib/types/Tool";
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
 
4
  import calculator from "./calculator";
5
  import directlyAnswer from "./directlyAnswer";
6
- import imageEditing from "./images/editing";
7
- import imageGeneration from "./images/generation";
8
- import documentParser from "./documentParser";
9
  import fetchUrl from "./web/url";
10
  import websearch from "./web/search";
11
- import type { TextGenerationContext } from "../textGeneration/types";
 
 
 
 
 
 
12
 
13
  export type BackendToolContext = Pick<
14
  TextGenerationContext,
15
  "conv" | "messages" | "assistant" | "ip" | "username"
16
  > & { preprompt?: string };
17
 
18
- export interface BackendTool extends Tool {
19
- call(
20
- params: Record<string, string | number | boolean>,
21
- context: BackendToolContext
22
- ): AsyncGenerator<MessageUpdate, Omit<ToolResultSuccess, "status" | "call" | "type">, undefined>;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  }
24
 
25
- export const allTools: BackendTool[] = [
26
- directlyAnswer,
27
- websearch,
28
- imageGeneration,
29
- fetchUrl,
30
- imageEditing,
31
- documentParser,
32
- calculator,
33
- ];
 
1
+ import { MessageUpdateType } from "$lib/types/MessageUpdate";
2
+ import {
3
+ ToolColor,
4
+ ToolIcon,
5
+ ToolOutputComponents,
6
+ type BackendCall,
7
+ type BaseTool,
8
+ type ConfigTool,
9
+ type ToolInput,
10
+ } from "$lib/types/Tool";
11
+ import type { TextGenerationContext } from "../textGeneration/types";
12
+
13
+ import { z } from "zod";
14
+ import JSON5 from "json5";
15
+ import { env } from "$env/dynamic/private";
16
 
17
+ import jp from "jsonpath";
18
  import calculator from "./calculator";
19
  import directlyAnswer from "./directlyAnswer";
 
 
 
20
  import fetchUrl from "./web/url";
21
  import websearch from "./web/search";
22
+ import { callSpace, getIpToken } from "./utils";
23
+ import { uploadFile } from "../files/uploadFile";
24
+ import type { MessageFile } from "$lib/types/Message";
25
+ import { sha256 } from "$lib/utils/sha256";
26
+ import { ObjectId } from "mongodb";
27
+ import { isValidOutputComponent, ToolOutputPaths } from "./outputs";
28
+ import { downloadFile } from "../files/downloadFile";
29
 
30
  export type BackendToolContext = Pick<
31
  TextGenerationContext,
32
  "conv" | "messages" | "assistant" | "ip" | "username"
33
  > & { preprompt?: string };
34
 
35
+ const IOType = z.union([z.literal("str"), z.literal("int"), z.literal("float"), z.literal("bool")]);
36
+
37
+ const toolInputBaseSchema = z.union([
38
+ z.object({
39
+ name: z.string().min(1).max(40),
40
+ description: z.string().max(100).optional(),
41
+ paramType: z.literal("required"),
42
+ }),
43
+ z.object({
44
+ name: z.string().min(1).max(40),
45
+ description: z.string().max(100).optional(),
46
+ paramType: z.literal("optional"),
47
+ default: z
48
+ .union([z.string().max(40), z.number(), z.boolean(), z.undefined()])
49
+ .transform((val) => (val === undefined ? "" : val)),
50
+ }),
51
+ z.object({
52
+ name: z.string().min(1).max(40),
53
+ paramType: z.literal("fixed"),
54
+ value: z
55
+ .union([z.string().max(40), z.number(), z.boolean(), z.undefined()])
56
+ .transform((val) => (val === undefined ? "" : val)),
57
+ }),
58
+ ]);
59
+
60
+ const toolInputSchema = toolInputBaseSchema.and(
61
+ z.object({ type: IOType }).or(
62
+ z.object({
63
+ type: z.literal("file"),
64
+ mimeTypes: z.string().nonempty(),
65
+ })
66
+ )
67
+ );
68
+
69
+ export const editableToolSchema = z
70
+ .object({
71
+ name: z.string().min(1).max(40),
72
+ baseUrl: z.string().min(1).max(100),
73
+ endpoint: z.string().min(1).max(100),
74
+ inputs: z.array(toolInputSchema),
75
+ outputComponent: z.string().min(1).max(100),
76
+ showOutput: z.boolean(),
77
+ displayName: z.string().min(1).max(40),
78
+ color: ToolColor,
79
+ icon: ToolIcon,
80
+ description: z.string().min(1).max(100),
81
+ })
82
+ .transform((tool) => ({
83
+ ...tool,
84
+ outputComponentIdx: parseInt(tool.outputComponent.split(";")[0]),
85
+ outputComponent: ToolOutputComponents.parse(tool.outputComponent.split(";")[1]),
86
+ }));
87
+
88
+ export const configTools = z
89
+ .array(
90
+ z
91
+ .object({
92
+ name: z.string(),
93
+ description: z.string(),
94
+ endpoint: z.union([z.string(), z.null()]),
95
+ inputs: z.array(toolInputSchema),
96
+ outputComponent: ToolOutputComponents.or(z.null()),
97
+ outputComponentIdx: z.number().int().default(0),
98
+ showOutput: z.boolean(),
99
+ _id: z
100
+ .string()
101
+ .length(24)
102
+ .regex(/^[0-9a-fA-F]{24}$/)
103
+ .transform((val) => new ObjectId(val)),
104
+ baseUrl: z.string().optional(),
105
+ displayName: z.string(),
106
+ color: ToolColor,
107
+ icon: ToolIcon,
108
+ isOnByDefault: z.optional(z.literal(true)),
109
+ isLocked: z.optional(z.literal(true)),
110
+ isHidden: z.optional(z.literal(true)),
111
+ })
112
+ .transform((val) => ({
113
+ type: "config" as const,
114
+ ...val,
115
+ call: getCallMethod(val),
116
+ }))
117
+ )
118
+ // add the extra hardcoded tools
119
+ .transform((val) => [...val, calculator, directlyAnswer, fetchUrl, websearch]);
120
+
121
+ export function getCallMethod(tool: Omit<BaseTool, "call">): BackendCall {
122
+ return async function* (params, ctx) {
123
+ if (
124
+ tool.endpoint === null ||
125
+ !tool.baseUrl ||
126
+ !tool.outputComponent ||
127
+ tool.outputComponentIdx === null
128
+ ) {
129
+ throw new Error(`Tool function ${tool.name} has no endpoint`);
130
+ }
131
+
132
+ const ipToken = await getIpToken(ctx.ip, ctx.username);
133
+
134
+ function coerceInput(value: unknown, type: ToolInput["type"]) {
135
+ const valueStr = String(value);
136
+ switch (type) {
137
+ case "str":
138
+ return valueStr;
139
+ case "int":
140
+ return parseInt(valueStr);
141
+ case "float":
142
+ return parseFloat(valueStr);
143
+ case "bool":
144
+ return valueStr === "true";
145
+ default:
146
+ throw new Error(`Unsupported type ${type}`);
147
+ }
148
+ }
149
+ const inputs = tool.inputs.map(async (input) => {
150
+ if (input.type === "file" && input.paramType !== "required") {
151
+ throw new Error("File inputs are always required and cannot be optional or fixed");
152
+ }
153
+
154
+ if (input.paramType === "fixed") {
155
+ return coerceInput(input.value, input.type);
156
+ } else if (input.paramType === "optional") {
157
+ return coerceInput(params[input.name] ?? input.default, input.type);
158
+ } else if (input.paramType === "required") {
159
+ if (params[input.name] === undefined) {
160
+ throw new Error(`Missing required input ${input.name}`);
161
+ }
162
+
163
+ if (input.type === "file") {
164
+ // todo: parse file here !
165
+ // structure is {input|output}-{msgIdx}-{fileIdx}-{filename}
166
+
167
+ const filename = params[input.name];
168
+
169
+ if (!filename || typeof filename !== "string") {
170
+ throw new Error(`Filename is not a string`);
171
+ }
172
+
173
+ const messages = ctx.messages;
174
+
175
+ const msgIdx = parseInt(filename.split("_")[1]);
176
+ const fileIdx = parseInt(filename.split("_")[2]);
177
+
178
+ if (Number.isNaN(msgIdx) || Number.isNaN(fileIdx)) {
179
+ throw Error(`Message index or file index is missing`);
180
+ }
181
+
182
+ if (msgIdx >= messages.length) {
183
+ throw Error(`Message index ${msgIdx} is out of bounds`);
184
+ }
185
+
186
+ const file = messages[msgIdx].files?.[fileIdx];
187
+
188
+ if (!file) {
189
+ throw Error(`File index ${fileIdx} is out of bounds`);
190
+ }
191
+
192
+ const blob = await downloadFile(file.value, ctx.conv._id)
193
+ .then((file) => fetch(`data:${file.mime};base64,${file.value}`))
194
+ .then((res) => res.blob())
195
+ .catch((err) => {
196
+ throw Error("Failed to download file", { cause: err });
197
+ });
198
+
199
+ return blob;
200
+ } else {
201
+ return coerceInput(params[input.name], input.type);
202
+ }
203
+ }
204
+ });
205
+
206
+ const outputs = await callSpace(
207
+ tool.baseUrl,
208
+ tool.endpoint,
209
+ await Promise.all(inputs),
210
+ ipToken
211
+ );
212
+
213
+ if (!isValidOutputComponent(tool.outputComponent)) {
214
+ throw new Error(`Tool output component is not defined`);
215
+ }
216
+
217
+ const { type, path } = ToolOutputPaths[tool.outputComponent];
218
+
219
+ if (!path || !type) {
220
+ throw new Error(`Tool output type ${tool.outputComponent} is not supported`);
221
+ }
222
+
223
+ const files: MessageFile[] = [];
224
+
225
+ const toolOutputs: Array<Record<string, string>> = [];
226
+
227
+ if (outputs.length <= tool.outputComponentIdx) {
228
+ throw new Error(`Tool output component index is out of bounds`);
229
+ }
230
+
231
+ // if its not an object, return directly
232
+ if (
233
+ outputs[tool.outputComponentIdx] !== undefined &&
234
+ typeof outputs[tool.outputComponentIdx] !== "object"
235
+ ) {
236
+ return { outputs: [{ [tool.name + "-0"]: outputs[tool.outputComponentIdx] }] };
237
+ }
238
+
239
+ await Promise.all(
240
+ jp
241
+ .query(outputs[tool.outputComponentIdx], path)
242
+ .map(async (output: string | string[], idx) => {
243
+ const arrayedOutput = Array.isArray(output) ? output : [output];
244
+ if (type === "file") {
245
+ // output files are actually URLs
246
+
247
+ await Promise.all(
248
+ arrayedOutput.map(async (output, idx) => {
249
+ await fetch(output)
250
+ .then((res) => res.blob())
251
+ .then(async (blob) => {
252
+ const mimeType = blob.type;
253
+ const fileType = blob.type.split("/")[1] ?? mimeType?.split("/")[1];
254
+ return new File(
255
+ [blob],
256
+ `${idx}-${await sha256(JSON.stringify(params))}.${fileType}`,
257
+ {
258
+ type: fileType,
259
+ }
260
+ );
261
+ })
262
+ .then((file) => uploadFile(file, ctx.conv))
263
+ .then((file) => files.push(file));
264
+ })
265
+ );
266
+
267
+ toolOutputs.push({
268
+ [tool.name + "-" + idx.toString()]:
269
+ "A file has been generated. Answer as if the user can already see the file. Do not try to insert the file. The user can already see the file. Do not try to describe the file as the model cannot interact with it. Be concise.",
270
+ });
271
+ } else {
272
+ for (const output of arrayedOutput) {
273
+ toolOutputs.push({
274
+ [tool.name + "-" + idx.toString()]: output,
275
+ });
276
+ }
277
+ }
278
+ })
279
+ );
280
+
281
+ for (const file of files) {
282
+ yield {
283
+ type: MessageUpdateType.File,
284
+ name: file.name,
285
+ sha: file.value,
286
+ mime: file.mime,
287
+ };
288
+ }
289
+
290
+ return { outputs: toolOutputs, display: tool.showOutput };
291
+ };
292
  }
293
 
294
+ export const toolFromConfigs = configTools.parse(JSON5.parse(env.TOOLS)) satisfies ConfigTool[];
 
 
 
 
 
 
 
 
src/lib/server/tools/outputs.ts ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { ToolIOType, ToolOutputComponents } from "$lib/types/Tool";
2
+
3
+ export const ToolOutputPaths: Record<
4
+ ToolOutputComponents,
5
+ {
6
+ type: ToolIOType;
7
+ path: string;
8
+ }
9
+ > = {
10
+ textbox: {
11
+ type: "str",
12
+ path: "$",
13
+ },
14
+ markdown: {
15
+ type: "str",
16
+ path: "$",
17
+ },
18
+ image: {
19
+ type: "file",
20
+ path: "$.url",
21
+ },
22
+ gallery: {
23
+ type: "file",
24
+ path: "$[*].image.url",
25
+ },
26
+ };
27
+
28
+ export const isValidOutputComponent = (
29
+ outputComponent: string
30
+ ): outputComponent is keyof typeof ToolOutputPaths => {
31
+ return Object.keys(ToolOutputPaths).includes(outputComponent);
32
+ };
src/lib/server/tools/utils.ts CHANGED
@@ -1,6 +1,7 @@
1
  import { env } from "$env/dynamic/private";
2
  import { Client } from "@gradio/client";
3
  import { SignJWT } from "jose";
 
4
  import JSON5 from "json5";
5
 
6
  export type GradioImage = {
@@ -31,13 +32,17 @@ export async function callSpace<TInput extends unknown[], TOutput extends unknow
31
  return super.fetch(input, init);
32
  }
33
  }
34
-
35
  const client = await CustomClient.connect(name, {
36
  hf_token: (env.HF_TOKEN ?? env.HF_ACCESS_TOKEN) as unknown as `hf_${string}`,
37
  });
 
38
  return await client
39
  .predict(func, parameters)
40
- .then((res) => (res as unknown as GradioResponse).data as TOutput);
 
 
 
 
41
  }
42
 
43
  export async function getIpToken(ip: string, username?: string) {
 
1
  import { env } from "$env/dynamic/private";
2
  import { Client } from "@gradio/client";
3
  import { SignJWT } from "jose";
4
+ import { logger } from "../logger";
5
  import JSON5 from "json5";
6
 
7
  export type GradioImage = {
 
32
  return super.fetch(input, init);
33
  }
34
  }
 
35
  const client = await CustomClient.connect(name, {
36
  hf_token: (env.HF_TOKEN ?? env.HF_ACCESS_TOKEN) as unknown as `hf_${string}`,
37
  });
38
+
39
  return await client
40
  .predict(func, parameters)
41
+ .then((res) => (res as unknown as GradioResponse).data as TOutput)
42
+ .catch((e) => {
43
+ logger.error(e);
44
+ throw e;
45
+ });
46
  }
47
 
48
  export async function getIpToken(ip: string, username?: string) {
src/lib/server/tools/web/search.ts CHANGED
@@ -1,19 +1,28 @@
1
- import type { BackendTool } from "..";
 
2
  import { runWebSearch } from "../../websearch/runWebSearch";
3
 
4
- const websearch: BackendTool = {
5
- name: "websearch",
 
 
 
 
6
  displayName: "Web Search",
7
- description:
8
- "Use this tool to search web pages for answers that will help answer the user's query. Only use this tool if you need specific resources from the internet.",
9
- parameterDefinitions: {
10
- query: {
11
- required: true,
12
- type: "string",
13
  description:
14
  "A search query which will be used to fetch the most relevant snippets regarding the user's query",
 
15
  },
16
- },
 
 
 
17
  async *call({ query }, { conv, assistant, messages }) {
18
  const webSearchToolResults = yield* runWebSearch(conv, messages, assistant?.rag, String(query));
19
  const chunks = webSearchToolResults?.contextSources
 
1
+ import type { ConfigTool } from "$lib/types/Tool";
2
+ import { ObjectId } from "mongodb";
3
  import { runWebSearch } from "../../websearch/runWebSearch";
4
 
5
+ const websearch: ConfigTool = {
6
+ _id: new ObjectId("00000000000000000000000A"),
7
+ type: "config",
8
+ description: "Search the web for answers to the user's query",
9
+ color: "blue",
10
+ icon: "wikis",
11
  displayName: "Web Search",
12
+ name: "websearch",
13
+ endpoint: null,
14
+ inputs: [
15
+ {
16
+ name: "query",
17
+ type: "str",
18
  description:
19
  "A search query which will be used to fetch the most relevant snippets regarding the user's query",
20
+ paramType: "required",
21
  },
22
+ ],
23
+ outputComponent: null,
24
+ outputComponentIdx: null,
25
+ showOutput: false,
26
  async *call({ query }, { conv, assistant, messages }) {
27
  const webSearchToolResults = yield* runWebSearch(conv, messages, assistant?.rag, String(query));
28
  const chunks = webSearchToolResults?.contextSources
src/lib/server/tools/web/url.ts CHANGED
@@ -1,23 +1,33 @@
1
  import { stringifyMarkdownElementTree } from "$lib/server/websearch/markdown/utils/stringify";
2
  import { scrapeUrl } from "$lib/server/websearch/scrape/scrape";
3
- import type { BackendTool } from "..";
 
4
 
5
- const fetchUrl: BackendTool = {
6
- name: "fetch_url",
7
- displayName: "URL Fetcher",
8
- description: "A tool that can be used to fetch an URL and return the content directly.",
9
- parameterDefinitions: {
10
- url: {
11
- description: "The url that should be fetched.",
 
 
 
 
 
12
  type: "str",
13
- required: true,
 
14
  },
15
- },
16
- async *call(params) {
17
- const blocks = String(params.url).split("\n");
18
- const url = blocks[blocks.length - 1];
 
 
 
19
 
20
- const { title, markdownTree } = await scrapeUrl(url, Infinity);
21
 
22
  return {
23
  outputs: [{ title, text: stringifyMarkdownElementTree(markdownTree) }],
 
1
  import { stringifyMarkdownElementTree } from "$lib/server/websearch/markdown/utils/stringify";
2
  import { scrapeUrl } from "$lib/server/websearch/scrape/scrape";
3
+ import type { ConfigTool } from "$lib/types/Tool";
4
+ import { ObjectId } from "mongodb";
5
 
6
+ const fetchUrl: ConfigTool = {
7
+ _id: new ObjectId("00000000000000000000000B"),
8
+ type: "config",
9
+ description: "Fetch the contents of a URL",
10
+ color: "blue",
11
+ icon: "cloud",
12
+ displayName: "Fetch URL",
13
+ name: "fetchUrl",
14
+ endpoint: null,
15
+ inputs: [
16
+ {
17
+ name: "url",
18
  type: "str",
19
+ description: "The URL of the webpage to fetch",
20
+ paramType: "required",
21
  },
22
+ ],
23
+ outputComponent: null,
24
+ outputComponentIdx: null,
25
+ showOutput: false,
26
+ async *call({ url }) {
27
+ const blocks = String(url).split("\n");
28
+ const urlStr = blocks[blocks.length - 1];
29
 
30
+ const { title, markdownTree } = await scrapeUrl(urlStr, Infinity);
31
 
32
  return {
33
  outputs: [{ title, text: stringifyMarkdownElementTree(markdownTree) }],
src/lib/server/usageLimits.ts CHANGED
@@ -17,6 +17,7 @@ export const usageLimitsSchema = z
17
  return val;
18
  }, z.coerce.number().optional())
19
  .optional(), // how many messages per minute
 
20
  })
21
  .optional();
22
 
 
17
  return val;
18
  }, z.coerce.number().optional())
19
  .optional(), // how many messages per minute
20
+ tools: z.coerce.number().optional(), // how many tools
21
  })
22
  .optional();
23
 
src/lib/stores/settings.ts CHANGED
@@ -15,7 +15,7 @@ type SettingsStore = {
15
  customPrompts: Record<string, string>;
16
  recentlySaved: boolean;
17
  assistants: Array<ObjectId | string>;
18
- tools?: Record<string, boolean>;
19
  disableStream: boolean;
20
  };
21
 
 
15
  customPrompts: Record<string, string>;
16
  recentlySaved: boolean;
17
  assistants: Array<ObjectId | string>;
18
+ tools?: Array<string>;
19
  disableStream: boolean;
20
  };
21
 
src/lib/types/Report.ts CHANGED
@@ -6,6 +6,7 @@ import type { Timestamps } from "./Timestamps";
6
  export interface Report extends Timestamps {
7
  _id: ObjectId;
8
  createdBy: User["_id"] | string;
9
- assistantId: Assistant["_id"];
 
10
  reason?: string;
11
  }
 
6
  export interface Report extends Timestamps {
7
  _id: ObjectId;
8
  createdBy: User["_id"] | string;
9
+ object: "assistant" | "tool";
10
+ contentId: Assistant["_id"];
11
  reason?: string;
12
  }
src/lib/types/Settings.ts CHANGED
@@ -21,7 +21,7 @@ export interface Settings extends Timestamps {
21
  customPrompts?: Record<string, string>;
22
 
23
  assistants?: Assistant["_id"][];
24
- tools?: Record<string, boolean>;
25
  disableStream: boolean;
26
  }
27
 
@@ -33,6 +33,6 @@ export const DEFAULT_SETTINGS = {
33
  hideEmojiOnSidebar: false,
34
  customPrompts: {},
35
  assistants: [],
36
- tools: {},
37
  disableStream: false,
38
  } satisfies SettingsEditable;
 
21
  customPrompts?: Record<string, string>;
22
 
23
  assistants?: Assistant["_id"][];
24
+ tools?: string[];
25
  disableStream: boolean;
26
  }
27
 
 
33
  hideEmojiOnSidebar: false,
34
  customPrompts: {},
35
  assistants: [],
36
+ tools: [],
37
  disableStream: false,
38
  } satisfies SettingsEditable;
src/lib/types/Tool.ts CHANGED
@@ -1,33 +1,147 @@
1
- type ToolInput =
2
- | {
3
- description: string;
4
- type: string;
5
- required: true;
6
- }
7
- | {
8
- description: string;
9
- type: string;
10
- required: false;
11
- default: string | number | boolean;
12
- };
13
-
14
- export interface Tool {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  name: string;
16
- displayName?: string;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  description: string;
18
- /** List of mime types that tool accepts */
19
- mimeTypes?: string[];
20
- parameterDefinitions: Record<string, ToolInput>;
21
- spec?: string;
22
- isOnByDefault?: true; // will it be toggled if the user hasn't tweaked it in settings ?
23
- isLocked?: true; // can the user enable/disable it ?
24
- isHidden?: true; // should it be hidden from the user ?
25
  }
26
 
27
- export type ToolFront = Pick<
28
- Tool,
29
- "name" | "displayName" | "description" | "isOnByDefault" | "isLocked" | "mimeTypes"
30
- > & { timeToUseMS?: number };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
  export enum ToolResultStatus {
33
  Success = "success",
@@ -51,3 +165,8 @@ export interface ToolCall {
51
  name: string;
52
  parameters: Record<string, string | number | boolean>;
53
  }
 
 
 
 
 
 
1
+ import type { ObjectId } from "mongodb";
2
+ import type { User } from "./User";
3
+ import type { Timestamps } from "./Timestamps";
4
+ import type { BackendToolContext } from "$lib/server/tools";
5
+ import type { MessageUpdate } from "./MessageUpdate";
6
+ import { z } from "zod";
7
+
8
+ export const ToolColor = z.union([
9
+ z.literal("purple"),
10
+ z.literal("blue"),
11
+ z.literal("green"),
12
+ z.literal("yellow"),
13
+ z.literal("red"),
14
+ ]);
15
+
16
+ export const ToolIcon = z.union([
17
+ z.literal("wikis"),
18
+ z.literal("tools"),
19
+ z.literal("camera"),
20
+ z.literal("code"),
21
+ z.literal("email"),
22
+ z.literal("cloud"),
23
+ z.literal("terminal"),
24
+ z.literal("game"),
25
+ z.literal("chat"),
26
+ z.literal("speaker"),
27
+ z.literal("video"),
28
+ ]);
29
+
30
+ export const ToolOutputComponents = z
31
+ .string()
32
+ .toLowerCase()
33
+ .pipe(
34
+ z.union([z.literal("textbox"), z.literal("markdown"), z.literal("image"), z.literal("gallery")])
35
+ );
36
+
37
+ export type ToolOutputComponents = z.infer<typeof ToolOutputComponents>;
38
+
39
+ export type ToolLogoColor = z.infer<typeof ToolColor>;
40
+ export type ToolLogoIcon = z.infer<typeof ToolIcon>;
41
+
42
+ export type ToolIOType = "str" | "int" | "float" | "bool" | "file";
43
+
44
+ export type ToolInputRequired = {
45
+ paramType: "required";
46
+ name: string;
47
+ description?: string;
48
+ };
49
+
50
+ export type ToolInputOptional = {
51
+ paramType: "optional";
52
+ name: string;
53
+ description?: string;
54
+ default: string | number | boolean;
55
+ };
56
+
57
+ export type ToolInputFixed = {
58
+ paramType: "fixed";
59
  name: string;
60
+ value: string | number | boolean;
61
+ };
62
+
63
+ type ToolInputBase = ToolInputRequired | ToolInputOptional | ToolInputFixed;
64
+
65
+ export type ToolInputFile = ToolInputBase & {
66
+ type: "file";
67
+ mimeTypes: string;
68
+ };
69
+
70
+ export type ToolInputSimple = ToolInputBase & {
71
+ type: Exclude<ToolIOType, "file">;
72
+ };
73
+
74
+ export type ToolInput = ToolInputFile | ToolInputSimple;
75
+
76
+ export interface BaseTool {
77
+ _id: ObjectId;
78
+
79
+ name: string; // name that will be shown to the AI
80
+
81
+ baseUrl?: string; // namespace for the tool
82
+ endpoint: string | null; // endpoint to call in gradio, if null we expect to override this function in code
83
+ outputComponent: string | null; // Gradio component type to use for the output
84
+ outputComponentIdx: number | null; // index of the output component
85
+
86
+ inputs: Array<ToolInput>;
87
+ showOutput: boolean; // show output in chat or not
88
+
89
+ call: BackendCall;
90
+
91
+ // for displaying in the UI
92
+ displayName: string;
93
+ color: ToolLogoColor;
94
+ icon: ToolLogoIcon;
95
  description: string;
 
 
 
 
 
 
 
96
  }
97
 
98
+ export interface ConfigTool extends BaseTool {
99
+ type: "config";
100
+ isOnByDefault?: true;
101
+ isLocked?: true;
102
+ isHidden?: true;
103
+ }
104
+
105
+ export interface CommunityTool extends BaseTool, Timestamps {
106
+ type: "community";
107
+
108
+ createdById: User["_id"] | string; // user id or session
109
+ createdByName?: User["username"];
110
+
111
+ // used to compute popular & trending
112
+ useCount: number;
113
+ last24HoursUseCount: number;
114
+
115
+ featured: boolean;
116
+ searchTokens: string[];
117
+ }
118
+
119
+ // no call function in db
120
+ export type CommunityToolDB = Omit<CommunityTool, "call">;
121
+
122
+ export type CommunityToolEditable = Omit<
123
+ CommunityToolDB,
124
+ | "_id"
125
+ | "useCount"
126
+ | "last24HoursUseCount"
127
+ | "createdById"
128
+ | "createdByName"
129
+ | "featured"
130
+ | "searchTokens"
131
+ | "type"
132
+ | "createdAt"
133
+ | "updatedAt"
134
+ >;
135
+
136
+ export type Tool = ConfigTool | CommunityTool;
137
+
138
+ export type ToolFront = Pick<Tool, "type" | "name" | "displayName" | "description"> & {
139
+ _id: string;
140
+ isOnByDefault: boolean;
141
+ isLocked: boolean;
142
+ mimeTypes: string[];
143
+ timeToUseMS?: number;
144
+ };
145
 
146
  export enum ToolResultStatus {
147
  Success = "success",
 
165
  name: string;
166
  parameters: Record<string, string | number | boolean>;
167
  }
168
+
169
+ export type BackendCall = (
170
+ params: Record<string, string | number | boolean>,
171
+ context: BackendToolContext
172
+ ) => AsyncGenerator<MessageUpdate, Omit<ToolResultSuccess, "status" | "call" | "type">, undefined>;
src/lib/utils/getGradioApi.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { base } from "$app/paths";
2
+ import type { Client } from "@gradio/client";
3
+
4
+ export type ApiReturnType = Awaited<ReturnType<typeof Client.prototype.view_api>>;
5
+
6
+ export async function getGradioApi(space: string) {
7
+ const api: ApiReturnType = await fetch(`${base}/api/spaces-config?space=${space}`).then((res) =>
8
+ res.json()
9
+ );
10
+ return api;
11
+ }
src/lib/utils/messageUpdates.ts CHANGED
@@ -48,7 +48,7 @@ type MessageUpdateRequestOptions = {
48
  isRetry: boolean;
49
  isContinue: boolean;
50
  webSearch: boolean;
51
- tools?: Record<string, boolean>;
52
  files?: MessageFile[];
53
  };
54
  export async function fetchMessageUpdates(
 
48
  isRetry: boolean;
49
  isContinue: boolean;
50
  webSearch: boolean;
51
+ tools?: Array<string>;
52
  files?: MessageFile[];
53
  };
54
  export async function fetchMessageUpdates(
src/lib/utils/tools.ts CHANGED
@@ -7,3 +7,19 @@ import type { Tool } from "$lib/types/Tool";
7
  export function toolHasName(name: string, tool: Pick<Tool, "name">): boolean {
8
  return tool.name.replaceAll("-", "_") === name.replaceAll("-", "_");
9
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  export function toolHasName(name: string, tool: Pick<Tool, "name">): boolean {
8
  return tool.name.replaceAll("-", "_") === name.replaceAll("-", "_");
9
  }
10
+
11
+ export const colors = ["purple", "blue", "green", "yellow", "red"] as const;
12
+
13
+ export const icons = [
14
+ "wikis",
15
+ "tools",
16
+ "camera",
17
+ "code",
18
+ "email",
19
+ "cloud",
20
+ "terminal",
21
+ "game",
22
+ "chat",
23
+ "speaker",
24
+ "video",
25
+ ] as const;
src/routes/+layout.server.ts CHANGED
@@ -8,8 +8,9 @@ import { DEFAULT_SETTINGS } from "$lib/types/Settings";
8
  import { env } from "$env/dynamic/private";
9
  import { ObjectId } from "mongodb";
10
  import type { ConvSidebar } from "$lib/types/ConvSidebar";
11
- import { allTools } from "$lib/server/tools";
12
  import { MetricsServer } from "$lib/server/metrics";
 
13
 
14
  export const load: LayoutServerLoad = async ({ locals, depends, request }) => {
15
  depends(UrlDependency.ConversationList);
@@ -107,6 +108,25 @@ export const load: LayoutServerLoad = async ({ locals, depends, request }) => {
107
  }
108
 
109
  const toolUseDuration = (await MetricsServer.getMetrics().tool.toolUseDuration.get()).values;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  return {
111
  conversations: conversations.map((conv) => {
112
  if (settings?.hideEmojiOnSidebar) {
@@ -146,7 +166,11 @@ export const load: LayoutServerLoad = async ({ locals, depends, request }) => {
146
  DEFAULT_SETTINGS.shareConversationsWithModelAuthors,
147
  customPrompts: settings?.customPrompts ?? {},
148
  assistants: userAssistants,
149
- tools: settings?.tools ?? {},
 
 
 
 
150
  disableStream: settings?.disableStream ?? DEFAULT_SETTINGS.disableStream,
151
  },
152
  models: models.map((model) => ({
@@ -171,19 +195,29 @@ export const load: LayoutServerLoad = async ({ locals, depends, request }) => {
171
  unlisted: model.unlisted,
172
  })),
173
  oldModels,
174
- tools: allTools
175
- .filter((tool) => !tool.isHidden)
176
- .map((tool) => ({
177
- name: tool.name,
178
- displayName: tool.displayName,
179
- description: tool.description,
180
- mimeTypes: tool.mimeTypes,
181
- isOnByDefault: tool.isOnByDefault,
182
- isLocked: tool.isLocked,
183
- timeToUseMS:
184
- toolUseDuration.find((el) => el.labels.tool === tool.name && el.labels.quantile === 0.9)
185
- ?.value ?? 15_000,
186
- })),
 
 
 
 
 
 
 
 
 
 
187
  assistants: assistants
188
  .filter((el) => userAssistantsSet.has(el._id.toString()))
189
  .map((el) => ({
 
8
  import { env } from "$env/dynamic/private";
9
  import { ObjectId } from "mongodb";
10
  import type { ConvSidebar } from "$lib/types/ConvSidebar";
11
+ import { toolFromConfigs } from "$lib/server/tools";
12
  import { MetricsServer } from "$lib/server/metrics";
13
+ import type { ToolFront, ToolInputFile } from "$lib/types/Tool";
14
 
15
  export const load: LayoutServerLoad = async ({ locals, depends, request }) => {
16
  depends(UrlDependency.ConversationList);
 
108
  }
109
 
110
  const toolUseDuration = (await MetricsServer.getMetrics().tool.toolUseDuration.get()).values;
111
+
112
+ const configToolIds = toolFromConfigs.map((el) => el._id.toString());
113
+
114
+ const activeCommunityToolIds = (settings?.tools ?? []).filter(
115
+ (key) => !configToolIds.includes(key)
116
+ );
117
+
118
+ const communityTools = await collections.tools
119
+ .find({ _id: { $in: activeCommunityToolIds.map((el) => new ObjectId(el)) } })
120
+ .toArray()
121
+ .then((tools) =>
122
+ tools.map((tool) => ({
123
+ ...tool,
124
+ isHidden: false,
125
+ isOnByDefault: true,
126
+ isLocked: true,
127
+ }))
128
+ );
129
+
130
  return {
131
  conversations: conversations.map((conv) => {
132
  if (settings?.hideEmojiOnSidebar) {
 
166
  DEFAULT_SETTINGS.shareConversationsWithModelAuthors,
167
  customPrompts: settings?.customPrompts ?? {},
168
  assistants: userAssistants,
169
+ tools:
170
+ settings?.tools ??
171
+ toolFromConfigs
172
+ .filter((el) => !el.isHidden && el.isOnByDefault)
173
+ .map((el) => el._id.toString()),
174
  disableStream: settings?.disableStream ?? DEFAULT_SETTINGS.disableStream,
175
  },
176
  models: models.map((model) => ({
 
195
  unlisted: model.unlisted,
196
  })),
197
  oldModels,
198
+ tools: [...toolFromConfigs, ...communityTools]
199
+ .filter((tool) => !tool?.isHidden)
200
+ .map(
201
+ (tool) =>
202
+ ({
203
+ _id: tool._id.toString(),
204
+ type: tool.type,
205
+ displayName: tool.displayName,
206
+ name: tool.name,
207
+ description: tool.description,
208
+ mimeTypes: (tool.inputs ?? [])
209
+ .filter((input): input is ToolInputFile => input.type === "file")
210
+ .map((input) => (input as ToolInputFile).mimeTypes)
211
+ .flat(),
212
+ isOnByDefault: tool.isOnByDefault ?? true,
213
+ isLocked: tool.isLocked ?? true,
214
+ timeToUseMS:
215
+ toolUseDuration.find(
216
+ (el) => el.labels.tool === tool._id.toString() && el.labels.quantile === 0.9
217
+ )?.value ?? 15_000,
218
+ } satisfies ToolFront)
219
+ ),
220
+ communityToolCount: await collections.tools.countDocuments({ type: "community" }),
221
  assistants: assistants
222
  .filter((el) => userAssistantsSet.has(el._id.toString()))
223
  .map((el) => ({
src/routes/api/spaces-config/+server.ts ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Client } from "@gradio/client";
2
+
3
+ export async function GET({ url, locals }) {
4
+ // XXX: feature_flag_tools
5
+ if (!locals.user?.isEarlyAccess) {
6
+ return new Response("Not early access", { status: 403 });
7
+ }
8
+
9
+ const space = url.searchParams.get("space");
10
+
11
+ if (!space) {
12
+ return new Response("Missing space", { status: 400 });
13
+ }
14
+
15
+ try {
16
+ const api = await (await Client.connect(space)).view_api();
17
+ return new Response(JSON.stringify(api), {
18
+ status: 200,
19
+ headers: {
20
+ "Content-Type": "application/json",
21
+ },
22
+ });
23
+ } catch (e) {
24
+ return new Response(JSON.stringify({ error: true, message: "Failed to get space API" }), {
25
+ status: 400,
26
+ headers: {
27
+ "Content-Type": "application/json",
28
+ },
29
+ });
30
+ }
31
+ }
src/routes/assistants/+page.server.ts CHANGED
@@ -55,8 +55,8 @@ export const load = async ({ url, locals }) => {
55
  .assistants.find(filter)
56
  .skip(NUM_PER_PAGE * pageIndex)
57
  .sort({
58
- ...(sort === SortKey.TRENDING && { last24HoursCount: -1 }),
59
- userCount: -1,
60
  })
61
  .limit(NUM_PER_PAGE)
62
  .toArray();
 
55
  .assistants.find(filter)
56
  .skip(NUM_PER_PAGE * pageIndex)
57
  .sort({
58
+ ...(sort === SortKey.TRENDING && { last24HoursUseCount: -1 }),
59
+ useCount: -1,
60
  })
61
  .limit(NUM_PER_PAGE)
62
  .toArray();
src/routes/conversation/[id]/+server.ts CHANGED
@@ -159,12 +159,23 @@ export async function POST({ request, locals, params, getClientAddress }) {
159
  is_continue: z.optional(z.boolean()),
160
  web_search: z.optional(z.boolean()),
161
  tools: z
162
- .record(z.boolean())
163
  .optional()
164
  .transform((tools) =>
165
  // disable tools on huggingchat android app
166
- request.headers.get("user-agent")?.includes("co.huggingface.chat_ui_android") ? {} : tools
167
  ),
 
 
 
 
 
 
 
 
 
 
 
168
  })
169
  .parse(JSON.parse(json));
170
 
@@ -419,7 +430,7 @@ export async function POST({ request, locals, params, getClientAddress }) {
419
  assistant: undefined,
420
  isContinue: isContinue ?? false,
421
  webSearch: webSearch ?? false,
422
- toolsPreference: toolsPreferences ?? {},
423
  promptedAt,
424
  ip: getClientAddress(),
425
  username: locals.user?.username,
 
159
  is_continue: z.optional(z.boolean()),
160
  web_search: z.optional(z.boolean()),
161
  tools: z
162
+ .array(z.string())
163
  .optional()
164
  .transform((tools) =>
165
  // disable tools on huggingchat android app
166
+ request.headers.get("user-agent")?.includes("co.huggingface.chat_ui_android") ? [] : tools
167
  ),
168
+
169
+ files: z.optional(
170
+ z.array(
171
+ z.object({
172
+ type: z.literal("base64").or(z.literal("hash")),
173
+ name: z.string(),
174
+ value: z.string(),
175
+ mime: z.string(),
176
+ })
177
+ )
178
+ ),
179
  })
180
  .parse(JSON.parse(json));
181
 
 
430
  assistant: undefined,
431
  isContinue: isContinue ?? false,
432
  webSearch: webSearch ?? false,
433
+ toolsPreference: toolsPreferences ?? [],
434
  promptedAt,
435
  ip: getClientAddress(),
436
  username: locals.user?.username,
src/routes/settings/(nav)/+server.ts CHANGED
@@ -2,6 +2,7 @@ import { collections } from "$lib/server/database";
2
  import { z } from "zod";
3
  import { authCondition } from "$lib/server/auth";
4
  import { DEFAULT_SETTINGS, type SettingsEditable } from "$lib/types/Settings";
 
5
 
6
  export async function POST({ request, locals }) {
7
  const body = await request.json();
@@ -15,11 +16,19 @@ export async function POST({ request, locals }) {
15
  ethicsModalAccepted: z.boolean().optional(),
16
  activeModel: z.string().default(DEFAULT_SETTINGS.activeModel),
17
  customPrompts: z.record(z.string()).default({}),
18
- tools: z.record(z.boolean()).optional(),
19
  disableStream: z.boolean().default(false),
20
  })
21
  .parse(body) satisfies SettingsEditable;
22
 
 
 
 
 
 
 
 
 
23
  await collections.settings.updateOne(
24
  authCondition(locals),
25
  {
 
2
  import { z } from "zod";
3
  import { authCondition } from "$lib/server/auth";
4
  import { DEFAULT_SETTINGS, type SettingsEditable } from "$lib/types/Settings";
5
+ import { toolFromConfigs } from "$lib/server/tools/index.js";
6
 
7
  export async function POST({ request, locals }) {
8
  const body = await request.json();
 
16
  ethicsModalAccepted: z.boolean().optional(),
17
  activeModel: z.string().default(DEFAULT_SETTINGS.activeModel),
18
  customPrompts: z.record(z.string()).default({}),
19
+ tools: z.array(z.string()).optional(),
20
  disableStream: z.boolean().default(false),
21
  })
22
  .parse(body) satisfies SettingsEditable;
23
 
24
+ // only allow tools to be set to community tools if user is early access
25
+ // XXX: feature_flag_tools
26
+ if (!locals.user?.isEarlyAccess) {
27
+ settings.tools = settings.tools?.filter((toolId) => {
28
+ return toolFromConfigs.some((tool) => tool._id.toString() === toolId);
29
+ });
30
+ }
31
+
32
  await collections.settings.updateOne(
33
  authCondition(locals),
34
  {
src/routes/settings/(nav)/assistants/[assistantId]/+page.server.ts CHANGED
@@ -63,7 +63,8 @@ export const actions: Actions = {
63
  // is there already a report from this user for this model ?
64
  const report = await collections.reports.findOne({
65
  createdBy: locals.user?._id ?? locals.sessionId,
66
- assistantId: new ObjectId(params.assistantId),
 
67
  });
68
 
69
  if (report) {
@@ -79,7 +80,8 @@ export const actions: Actions = {
79
 
80
  const { acknowledged } = await collections.reports.insertOne({
81
  _id: new ObjectId(),
82
- assistantId: new ObjectId(params.assistantId),
 
83
  createdBy: locals.user?._id ?? locals.sessionId,
84
  createdAt: new Date(),
85
  updatedAt: new Date(),
 
63
  // is there already a report from this user for this model ?
64
  const report = await collections.reports.findOne({
65
  createdBy: locals.user?._id ?? locals.sessionId,
66
+ object: "assistant",
67
+ contentId: new ObjectId(params.assistantId),
68
  });
69
 
70
  if (report) {
 
80
 
81
  const { acknowledged } = await collections.reports.insertOne({
82
  _id: new ObjectId(),
83
+ contentId: new ObjectId(params.assistantId),
84
+ object: "assistant",
85
  createdBy: locals.user?._id ?? locals.sessionId,
86
  createdAt: new Date(),
87
  updatedAt: new Date(),
src/routes/settings/(nav)/assistants/[assistantId]/ReportModal.svelte CHANGED
@@ -22,10 +22,10 @@
22
  }}
23
  class="w-full min-w-64 p-4"
24
  >
25
- <span class="mb-1 text-sm font-semibold">Report an assistant</span>
26
 
27
  <p class="text-sm text-gray-500">
28
- Please provide a brief description of why you are reporting this assistant.
29
  </p>
30
 
31
  <textarea
 
22
  }}
23
  class="w-full min-w-64 p-4"
24
  >
25
+ <span class="mb-1 text-sm font-semibold">Report content</span>
26
 
27
  <p class="text-sm text-gray-500">
28
+ Please provide a brief description of why you are reporting this content.
29
  </p>
30
 
31
  <textarea
src/routes/settings/+layout.server.ts CHANGED
@@ -9,9 +9,12 @@ export const load = (async ({ locals, parent }) => {
9
  const createdBy = locals.user?._id ?? locals.sessionId;
10
  if (createdBy) {
11
  const reports = await collections.reports
12
- .find<Pick<Report, "assistantId">>({ createdBy }, { projection: { _id: 0, assistantId: 1 } })
 
 
 
13
  .toArray();
14
- reportsByUser = reports.map((r) => r.assistantId.toString());
15
  }
16
 
17
  return {
 
9
  const createdBy = locals.user?._id ?? locals.sessionId;
10
  if (createdBy) {
11
  const reports = await collections.reports
12
+ .find<Pick<Report, "contentId">>(
13
+ { createdBy, object: "assistant" },
14
+ { projection: { _id: 0, contentId: 1 } }
15
+ )
16
  .toArray();
17
+ reportsByUser = reports.map((r) => r.contentId.toString());
18
  }
19
 
20
  return {
src/routes/tools/+layout.svelte ADDED
@@ -0,0 +1 @@
 
 
1
+ <slot />
src/routes/tools/+layout.ts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // check if user is earlyAccess else redirect to base
2
+
3
+ import { base } from "$app/paths";
4
+ import { redirect } from "@sveltejs/kit";
5
+
6
+ // XXX: feature_flag_tools
7
+ export async function load({ parent }) {
8
+ const { user } = await parent();
9
+
10
+ if (user?.isEarlyAccess) {
11
+ return {};
12
+ }
13
+
14
+ redirect(302, `${base}/`);
15
+ }
src/routes/tools/+page.server.ts ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { authCondition } from "$lib/server/auth.js";
2
+ import { Database, collections } from "$lib/server/database.js";
3
+ import { toolFromConfigs } from "$lib/server/tools/index.js";
4
+ import { SortKey } from "$lib/types/Assistant.js";
5
+ import type { CommunityToolDB } from "$lib/types/Tool.js";
6
+ import type { User } from "$lib/types/User.js";
7
+ import { generateQueryTokens, generateSearchTokens } from "$lib/utils/searchTokens.js";
8
+ import { error } from "@sveltejs/kit";
9
+ import { ObjectId, type Filter } from "mongodb";
10
+
11
+ const NUM_PER_PAGE = 16;
12
+
13
+ export const load = async ({ url, locals }) => {
14
+ // XXX: feature_flag_tools
15
+ if (!locals.user?.isEarlyAccess) {
16
+ error(403, "You need to be an early access user to view tools");
17
+ }
18
+
19
+ const username = url.searchParams.get("user");
20
+ const query = url.searchParams.get("q")?.trim() ?? null;
21
+
22
+ const pageIndex = parseInt(url.searchParams.get("p") ?? "0");
23
+ const sort = url.searchParams.get("sort")?.trim() ?? SortKey.TRENDING;
24
+ const createdByCurrentUser = locals.user?.username && locals.user.username === username;
25
+ const activeOnly = url.searchParams.get("active") === "true";
26
+
27
+ let user: Pick<User, "_id"> | null = null;
28
+ if (username) {
29
+ user = await collections.users.findOne<Pick<User, "_id">>(
30
+ { username },
31
+ { projection: { _id: 1 } }
32
+ );
33
+ if (!user) {
34
+ error(404, `User "${username}" doesn't exist`);
35
+ }
36
+ }
37
+
38
+ const settings = await collections.settings.findOne(authCondition(locals));
39
+
40
+ if (!settings && activeOnly) {
41
+ error(404, "No user settings found");
42
+ }
43
+
44
+ const queryTokens = !!query && generateQueryTokens(query);
45
+
46
+ const filter: Filter<CommunityToolDB> = {
47
+ ...(!createdByCurrentUser && !activeOnly && { featured: true }),
48
+ ...(user && { createdById: user._id }),
49
+ ...(queryTokens && { searchTokens: { $all: queryTokens } }),
50
+ ...(activeOnly && {
51
+ _id: {
52
+ $in: (settings?.tools ?? []).map((key) => {
53
+ return new ObjectId(key);
54
+ }),
55
+ },
56
+ }),
57
+ };
58
+
59
+ const communityTools = await Database.getInstance()
60
+ .getCollections()
61
+ .tools.find(filter)
62
+ .skip(NUM_PER_PAGE * pageIndex)
63
+ .sort({
64
+ ...(sort === SortKey.TRENDING && { last24HoursUseCount: -1 }),
65
+ useCount: -1,
66
+ })
67
+ .limit(NUM_PER_PAGE)
68
+ .toArray();
69
+
70
+ const configTools = toolFromConfigs
71
+ .filter((tool) => !tool?.isHidden)
72
+ .filter((tool) => {
73
+ if (queryTokens) {
74
+ return generateSearchTokens(tool.name).some((token) =>
75
+ queryTokens.some((queryToken) => queryToken.test(token))
76
+ );
77
+ }
78
+ return true;
79
+ });
80
+
81
+ const tools = [...(pageIndex == 0 && !username ? configTools : []), ...communityTools];
82
+
83
+ const numTotalItems =
84
+ (await Database.getInstance().getCollections().tools.countDocuments(filter)) +
85
+ toolFromConfigs.length;
86
+
87
+ return {
88
+ tools: JSON.parse(JSON.stringify(tools)) as CommunityToolDB[],
89
+ numTotalItems,
90
+ numItemsPerPage: NUM_PER_PAGE,
91
+ query,
92
+ sort,
93
+ };
94
+ };