nsarrazin HF Staff victor HF Staff Liam Dyer Mishig commited on
Commit
564e576
·
unverified ·
1 Parent(s): 7ae6b59

Function calling (#996)

Browse files

* Initial work on function calling

* wip

* Add websearch as a tool

* lint

* smol

* Move tools to their own files

* directly answer check

* Add text2img tool

* group tool calls together

* show retry even if no message

* fix trailing urls

* Add image popup

* format

* wip

* clean-up

* better loading indicator

* text colors

* extra example to summarize

* switch default model

* Add fetchUrl tool

* wip

* Add latest gradio

* fix types

* version bump sharp

* Basic tools menu

* working menu

* fix menu positioning

* deps fix

* add deps

* cleanup

* more cleanup

* cleanup

* package cleanups

* moar cleanup

* ui update

* Update ChatMessage.svelte

* upgrade gradio dep

* lint

* refactor and pass results to cohere

* feat: code interpreter tool

* fix: add e2b dependency

* feat: working TGI endpoint with tool results

* feat: allow image model to expand to 90dvw

* fix: dont block on title gen

* bump sharp

* misc cleanup

* fix url fetcher, adjust tool typing

* migrate to new MessageUpdate schema

* fix lint errors

* image editing, pdf upload + parsing

* feat: file preview for non-images

* feat: image prompting, file ui, file migration, many fixes

* feat: multiple files and tool file indices

* feat: add back remote keylogging prevention

* minor nit

* minor

* resolve nits

* feat: use node vm for calculator and improve prompt

* more nits

* feat: add index to tools settings

* chore: update invalid package lock

* feat: bump gradio client to 0.19.4

* feat: move pdf to markdown to huggingchat

* fix: uploaded file width

* feat: prompt the model when no files available

* Feat functions: UI update (#1157)

* details: close on click outside

* drag for tools

* tools ordering

* add link to community discussion

* misc

* calculator listed as last

* file style

* images with different ratios

* page scraper: longer timeout



@Saghen
@mishig25 seems to work better with this

* vertical gap

* Revert "drag for tools"

This reverts commit 8eeed3b77c164325bec8ab4f5827641acb6cc72f.

* chat padding on desktop

* file colors

* larger gap for all messages on xl screens

* feat: update pdf to markdown schema

* fix: uploaded file container width

* fix: tool name check

* fix: cohere endpoint

* fix: use most recent message for files

* feat: allow tools to access previous files

* feat: support all file types on document parser

* fix: use document parser in default tools

* feat: rename url fetcher internal name

* feat: resolve type error

* feat: enable tools on prod command r+

* feat: truncate document markdown

* feat: bump @huggingface/inference

* feat: resolve type errors

* nit

* Feat functions misc update 2 (#1158)

* update examples

a

* add tool indicator

* llama 3 description update

* image quick fix

* mobile

* icon purple

---------

Co-authored-by: Victor Mustar <[email protected]>
Co-authored-by: Liam Dyer <[email protected]>
Co-authored-by: Mishig <[email protected]>

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env +1 -0
  2. .eslintrc.cjs +1 -1
  3. chart/env/prod.yaml +7 -6
  4. package-lock.json +528 -70
  5. package.json +4 -3
  6. src/lib/actions/clickOutside.ts +1 -1
  7. src/lib/buildPrompt.ts +11 -1
  8. src/lib/components/OpenWebSearchResults.svelte +14 -8
  9. src/lib/components/ToolsMenu.svelte +75 -0
  10. src/lib/components/UploadBtn.svelte +13 -8
  11. src/lib/components/chat/ChatMessage.svelte +177 -28
  12. src/lib/components/chat/ChatWindow.svelte +22 -25
  13. src/lib/components/chat/UploadedFile.svelte +54 -0
  14. src/lib/components/icons/IconTool.svelte +16 -0
  15. src/lib/migrations/routines/03-add-tools-in-settings.ts +29 -0
  16. src/lib/migrations/routines/04-update-message-updates.ts +190 -0
  17. src/lib/migrations/routines/05-update-message-files.ts +56 -0
  18. src/lib/migrations/routines/index.ts +10 -1
  19. src/lib/server/endpoints/cohere/endpointCohere.ts +77 -14
  20. src/lib/server/endpoints/endpoints.ts +10 -2
  21. src/lib/server/endpoints/tgi/endpointTgi.ts +11 -1
  22. src/lib/server/files/downloadFile.ts +16 -19
  23. src/lib/server/files/uploadFile.ts +3 -1
  24. src/lib/server/models.ts +66 -3
  25. src/lib/server/textGeneration/assistant.ts +53 -0
  26. src/lib/server/textGeneration/generate.ts +51 -0
  27. src/lib/server/textGeneration/index.ts +64 -0
  28. src/lib/server/{summarize.ts → textGeneration/title.ts} +31 -2
  29. src/lib/server/textGeneration/tools.ts +198 -0
  30. src/lib/server/textGeneration/types.ts +17 -0
  31. src/lib/server/tools/calculator.ts +37 -0
  32. src/lib/server/tools/directlyAnswer.ts +20 -0
  33. src/lib/server/tools/documentParser.ts +69 -0
  34. src/lib/server/tools/images/editing.ts +107 -0
  35. src/lib/server/tools/images/generation.ts +92 -0
  36. src/lib/server/tools/index.ts +37 -0
  37. src/lib/server/tools/utils.ts +29 -0
  38. src/lib/server/tools/web/search.ts +33 -0
  39. src/lib/server/tools/web/url.ts +32 -0
  40. src/lib/server/websearch/markdown/utils/stringify.ts +7 -0
  41. src/lib/server/websearch/runWebSearch.ts +30 -33
  42. src/lib/server/websearch/scrape/playwright.ts +4 -3
  43. src/lib/server/websearch/scrape/scrape.ts +33 -7
  44. src/lib/server/websearch/search/search.ts +14 -9
  45. src/lib/server/websearch/update.ts +46 -0
  46. src/lib/stores/settings.ts +1 -0
  47. src/lib/types/Message.ts +1 -0
  48. src/lib/types/MessageUpdate.ts +102 -37
  49. src/lib/types/Model.ts +1 -0
  50. src/lib/types/Settings.ts +2 -0
.env CHANGED
@@ -154,6 +154,7 @@ WEBHOOK_URL_REPORT_ASSISTANT=#provide webhook url to get notified when an assist
154
  ALLOWED_USER_EMAILS=`[]` # if it's defined, only these emails will be allowed to use the app
155
 
156
  USAGE_LIMITS=`{}`
 
157
  ALLOW_INSECURE_COOKIES=false # recommended to keep this to false but set to true if you need to run over http without tls
158
  METRICS_PORT=
159
  LOG_LEVEL=info
 
154
  ALLOWED_USER_EMAILS=`[]` # if it's defined, only these emails will be allowed to use the app
155
 
156
  USAGE_LIMITS=`{}`
157
+
158
  ALLOW_INSECURE_COOKIES=false # recommended to keep this to false but set to true if you need to run over http without tls
159
  METRICS_PORT=
160
  LOG_LEVEL=info
.eslintrc.cjs CHANGED
@@ -24,7 +24,7 @@ module.exports = {
24
  extraFileExtensions: [".svelte"],
25
  },
26
  rules: {
27
- "no-shadow": ["error"],
28
  "@typescript-eslint/no-explicit-any": "error",
29
  "@typescript-eslint/no-non-null-assertion": "error",
30
  "@typescript-eslint/no-unused-vars": [
 
24
  extraFileExtensions: [".svelte"],
25
  },
26
  rules: {
27
+ "require-yield": "off",
28
  "@typescript-eslint/no-explicit-any": "error",
29
  "@typescript-eslint/no-non-null-assertion": "error",
30
  "@typescript-eslint/no-unused-vars": [
chart/env/prod.yaml CHANGED
@@ -39,6 +39,7 @@ envVars:
39
  "modelUrl": "https://huggingface.co/CohereForAI/c4ai-command-r-plus",
40
  "websiteUrl": "https://docs.cohere.com/docs/command-r-plus",
41
  "logoUrl": "https://huggingface.co/datasets/huggingchat/models-logo/resolve/main/cohere-logo.png",
 
42
  "parameters": {
43
  "stop": ["<|END_OF_TURN_TOKEN|>"],
44
  "truncate" : 28672,
@@ -47,20 +48,20 @@ envVars:
47
  },
48
  "promptExamples" : [
49
  {
50
- "title": "Write an email from bullet list",
51
- "prompt": "As a restaurant owner, write a professional email to the supplier to get these products every week: \n\n- Wine (x10)\n- Eggs (x24)\n- Bread (x12)"
 
 
 
52
  }, {
53
  "title": "Code a snake game",
54
  "prompt": "Code a basic snake game in python, give explanations for each step."
55
- }, {
56
- "title": "Assist in a task",
57
- "prompt": "How do I make a delicious lemon cheesecake?"
58
  }
59
  ]
60
  },
61
  {
62
  "name" : "meta-llama/Meta-Llama-3-70B-Instruct",
63
- "description": "Generation over generation, Meta Llama 3 demonstrates state-of-the-art performance on a wide range of industry benchmarks and offers new capabilities, including improved reasoning.",
64
  "logoUrl": "https://huggingface.co/datasets/huggingchat/models-logo/resolve/main/meta-logo.png",
65
  "modelUrl": "https://huggingface.co/meta-llama/Meta-Llama-3-70B-Instruct",
66
  "websiteUrl": "https://llama.meta.com/llama3/",
 
39
  "modelUrl": "https://huggingface.co/CohereForAI/c4ai-command-r-plus",
40
  "websiteUrl": "https://docs.cohere.com/docs/command-r-plus",
41
  "logoUrl": "https://huggingface.co/datasets/huggingchat/models-logo/resolve/main/cohere-logo.png",
42
+ "tools": true,
43
  "parameters": {
44
  "stop": ["<|END_OF_TURN_TOKEN|>"],
45
  "truncate" : 28672,
 
48
  },
49
  "promptExamples" : [
50
  {
51
+ "title": "Generate a mouse portrait",
52
+ "prompt": "Generate the portrait of a scientific mouse in its laboratory."
53
+ }, {
54
+ "title": "Review a pull request",
55
+ "prompt": "Review this pull request: https://github.com/huggingface/chat-ui/pull/1131/files"
56
  }, {
57
  "title": "Code a snake game",
58
  "prompt": "Code a basic snake game in python, give explanations for each step."
 
 
 
59
  }
60
  ]
61
  },
62
  {
63
  "name" : "meta-llama/Meta-Llama-3-70B-Instruct",
64
+ "description": "Meta Llama 3 delivers top performance on various benchmarks and introduces new features like better reasoning.",
65
  "logoUrl": "https://huggingface.co/datasets/huggingchat/models-logo/resolve/main/meta-logo.png",
66
  "modelUrl": "https://huggingface.co/meta-llama/Meta-Llama-3-70B-Instruct",
67
  "websiteUrl": "https://llama.meta.com/llama3/",
package-lock.json CHANGED
@@ -9,8 +9,9 @@
9
  "version": "0.8.4",
10
  "dependencies": {
11
  "@cliqz/adblocker-playwright": "^1.27.2",
 
12
  "@huggingface/hub": "^0.5.1",
13
- "@huggingface/inference": "^2.6.3",
14
  "@iconify-json/bi": "^1.1.21",
15
  "@playwright/browser-chromium": "^1.43.1",
16
  "@resvg/resvg-js": "^2.6.2",
@@ -42,7 +43,7 @@
42
  "satori-html": "^0.3.2",
43
  "sbd": "^1.0.19",
44
  "serpapi": "^1.1.1",
45
- "sharp": "^0.33.3",
46
  "tailwind-scrollbar": "^3.0.0",
47
  "tailwindcss": "^3.4.0",
48
  "uuid": "^9.0.1",
@@ -174,6 +175,22 @@
174
  "google-auth-library": "^9.4.2"
175
  }
176
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  "node_modules/@cliqz/adblocker": {
178
  "version": "1.27.2",
179
  "resolved": "https://registry.npmjs.org/@cliqz/adblocker/-/adblocker-1.27.2.tgz",
@@ -706,6 +723,23 @@
706
  "node": ">=18.0.0"
707
  }
708
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
709
  "node_modules/@huggingface/hub": {
710
  "version": "0.5.1",
711
  "resolved": "https://registry.npmjs.org/@huggingface/hub/-/hub-0.5.1.tgz",
@@ -718,9 +752,12 @@
718
  }
719
  },
720
  "node_modules/@huggingface/inference": {
721
- "version": "2.6.3",
722
- "resolved": "https://registry.npmjs.org/@huggingface/inference/-/inference-2.6.3.tgz",
723
- "integrity": "sha512-KK6xNrEldjjopiGqwaBCkA+4tEyuIz0qHsD5SVYaQ65HSlmBbntJieSw4NRWT+S5bK/Bf/GFCixW0NshAOcBqA==",
 
 
 
724
  "engines": {
725
  "node": ">=18"
726
  }
@@ -733,6 +770,11 @@
733
  "node": ">=18"
734
  }
735
  },
 
 
 
 
 
736
  "node_modules/@humanwhocodes/config-array": {
737
  "version": "0.11.8",
738
  "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
@@ -812,9 +854,9 @@
812
  }
813
  },
814
  "node_modules/@img/sharp-darwin-arm64": {
815
- "version": "0.33.3",
816
- "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.3.tgz",
817
- "integrity": "sha512-FaNiGX1MrOuJ3hxuNzWgsT/mg5OHG/Izh59WW2mk1UwYHUwtfbhk5QNKYZgxf0pLOhx9ctGiGa2OykD71vOnSw==",
818
  "cpu": [
819
  "arm64"
820
  ],
@@ -837,9 +879,9 @@
837
  }
838
  },
839
  "node_modules/@img/sharp-darwin-x64": {
840
- "version": "0.33.3",
841
- "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.3.tgz",
842
- "integrity": "sha512-2QeSl7QDK9ru//YBT4sQkoq7L0EAJZA3rtV+v9p8xTKl4U1bUqTIaCnoC7Ctx2kCjQgwFXDasOtPTCT8eCTXvw==",
843
  "cpu": [
844
  "x64"
845
  ],
@@ -1030,9 +1072,9 @@
1030
  }
1031
  },
1032
  "node_modules/@img/sharp-linux-arm": {
1033
- "version": "0.33.3",
1034
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.3.tgz",
1035
- "integrity": "sha512-Q7Ee3fFSC9P7vUSqVEF0zccJsZ8GiiCJYGWDdhEjdlOeS9/jdkyJ6sUSPj+bL8VuOYFSbofrW0t/86ceVhx32w==",
1036
  "cpu": [
1037
  "arm"
1038
  ],
@@ -1055,9 +1097,9 @@
1055
  }
1056
  },
1057
  "node_modules/@img/sharp-linux-arm64": {
1058
- "version": "0.33.3",
1059
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.3.tgz",
1060
- "integrity": "sha512-Zf+sF1jHZJKA6Gor9hoYG2ljr4wo9cY4twaxgFDvlG0Xz9V7sinsPp8pFd1XtlhTzYo0IhDbl3rK7P6MzHpnYA==",
1061
  "cpu": [
1062
  "arm64"
1063
  ],
@@ -1080,9 +1122,9 @@
1080
  }
1081
  },
1082
  "node_modules/@img/sharp-linux-s390x": {
1083
- "version": "0.33.3",
1084
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.3.tgz",
1085
- "integrity": "sha512-vFk441DKRFepjhTEH20oBlFrHcLjPfI8B0pMIxGm3+yilKyYeHEVvrZhYFdqIseSclIqbQ3SnZMwEMWonY5XFA==",
1086
  "cpu": [
1087
  "s390x"
1088
  ],
@@ -1091,7 +1133,7 @@
1091
  "linux"
1092
  ],
1093
  "engines": {
1094
- "glibc": ">=2.28",
1095
  "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
1096
  "npm": ">=9.6.5",
1097
  "pnpm": ">=7.1.0",
@@ -1105,9 +1147,9 @@
1105
  }
1106
  },
1107
  "node_modules/@img/sharp-linux-x64": {
1108
- "version": "0.33.3",
1109
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.3.tgz",
1110
- "integrity": "sha512-Q4I++herIJxJi+qmbySd072oDPRkCg/SClLEIDh5IL9h1zjhqjv82H0Seupd+q2m0yOfD+/fJnjSoDFtKiHu2g==",
1111
  "cpu": [
1112
  "x64"
1113
  ],
@@ -1130,9 +1172,9 @@
1130
  }
1131
  },
1132
  "node_modules/@img/sharp-linuxmusl-arm64": {
1133
- "version": "0.33.3",
1134
- "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.3.tgz",
1135
- "integrity": "sha512-qnDccehRDXadhM9PM5hLvcPRYqyFCBN31kq+ErBSZtZlsAc1U4Z85xf/RXv1qolkdu+ibw64fUDaRdktxTNP9A==",
1136
  "cpu": [
1137
  "arm64"
1138
  ],
@@ -1155,9 +1197,9 @@
1155
  }
1156
  },
1157
  "node_modules/@img/sharp-linuxmusl-x64": {
1158
- "version": "0.33.3",
1159
- "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.3.tgz",
1160
- "integrity": "sha512-Jhchim8kHWIU/GZ+9poHMWRcefeaxFIs9EBqf9KtcC14Ojk6qua7ghKiPs0sbeLbLj/2IGBtDcxHyjCdYWkk2w==",
1161
  "cpu": [
1162
  "x64"
1163
  ],
@@ -1180,15 +1222,15 @@
1180
  }
1181
  },
1182
  "node_modules/@img/sharp-wasm32": {
1183
- "version": "0.33.3",
1184
- "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.3.tgz",
1185
- "integrity": "sha512-68zivsdJ0koE96stdUfM+gmyaK/NcoSZK5dV5CAjES0FUXS9lchYt8LAB5rTbM7nlWtxaU/2GON0HVN6/ZYJAQ==",
1186
  "cpu": [
1187
  "wasm32"
1188
  ],
1189
  "optional": true,
1190
  "dependencies": {
1191
- "@emnapi/runtime": "^1.1.0"
1192
  },
1193
  "engines": {
1194
  "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
@@ -1201,9 +1243,9 @@
1201
  }
1202
  },
1203
  "node_modules/@img/sharp-win32-ia32": {
1204
- "version": "0.33.3",
1205
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.3.tgz",
1206
- "integrity": "sha512-CyimAduT2whQD8ER4Ux7exKrtfoaUiVr7HG0zZvO0XTFn2idUWljjxv58GxNTkFb8/J9Ub9AqITGkJD6ZginxQ==",
1207
  "cpu": [
1208
  "ia32"
1209
  ],
@@ -1222,9 +1264,9 @@
1222
  }
1223
  },
1224
  "node_modules/@img/sharp-win32-x64": {
1225
- "version": "0.33.3",
1226
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.3.tgz",
1227
- "integrity": "sha512-viT4fUIDKnli3IfOephGnolMzhz5VaTvDRkYqtZxOMIoMQ4MrAziO7pT1nVnOt2FAm7qW5aa+CCc13aEY6Le0g==",
1228
  "cpu": [
1229
  "x64"
1230
  ],
@@ -1242,6 +1284,76 @@
1242
  "url": "https://opencollective.com/libvips"
1243
  }
1244
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1245
  "node_modules/@jridgewell/gen-mapping": {
1246
  "version": "0.3.3",
1247
  "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
@@ -1294,6 +1406,30 @@
1294
  "sparse-bitfield": "^3.0.3"
1295
  }
1296
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1297
  "node_modules/@nodelib/fs.scandir": {
1298
  "version": "2.1.5",
1299
  "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1326,6 +1462,25 @@
1326
  "node": ">= 8"
1327
  }
1328
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1329
  "node_modules/@opentelemetry/api": {
1330
  "version": "1.8.0",
1331
  "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.8.0.tgz",
@@ -2168,6 +2323,11 @@
2168
  "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
2169
  "dev": true
2170
  },
 
 
 
 
 
2171
  "node_modules/@types/express": {
2172
  "version": "4.17.21",
2173
  "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
@@ -2266,6 +2426,14 @@
2266
  "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==",
2267
  "dev": true
2268
  },
 
 
 
 
 
 
 
 
2269
  "node_modules/@types/node": {
2270
  "version": "18.13.0",
2271
  "resolved": "https://registry.npmjs.org/@types/node/-/node-18.13.0.tgz",
@@ -2356,6 +2524,11 @@
2356
  "@types/send": "*"
2357
  }
2358
  },
 
 
 
 
 
2359
  "node_modules/@types/tough-cookie": {
2360
  "version": "4.0.2",
2361
  "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz",
@@ -2382,6 +2555,11 @@
2382
  "@types/webidl-conversions": "*"
2383
  }
2384
  },
 
 
 
 
 
2385
  "node_modules/@typescript-eslint/eslint-plugin": {
2386
  "version": "6.7.4",
2387
  "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.4.tgz",
@@ -2799,11 +2977,35 @@
2799
  "url": "https://github.com/sponsors/epoberezkin"
2800
  }
2801
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2802
  "node_modules/ansi-regex": {
2803
  "version": "5.0.1",
2804
  "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
2805
  "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
2806
- "dev": true,
2807
  "engines": {
2808
  "node": ">=8"
2809
  }
@@ -2812,7 +3014,6 @@
2812
  "version": "4.3.0",
2813
  "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
2814
  "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
2815
- "dev": true,
2816
  "dependencies": {
2817
  "color-convert": "^2.0.1"
2818
  },
@@ -3195,8 +3396,6 @@
3195
  "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.7.tgz",
3196
  "integrity": "sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==",
3197
  "hasInstallScript": true,
3198
- "optional": true,
3199
- "peer": true,
3200
  "dependencies": {
3201
  "node-gyp-build": "^4.3.0"
3202
  },
@@ -3317,7 +3516,6 @@
3317
  "version": "4.1.2",
3318
  "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
3319
  "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
3320
- "dev": true,
3321
  "dependencies": {
3322
  "ansi-styles": "^4.1.0",
3323
  "supports-color": "^7.1.0"
@@ -3389,6 +3587,54 @@
3389
  "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
3390
  "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
3391
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3392
  "node_modules/code-red": {
3393
  "version": "1.0.4",
3394
  "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz",
@@ -3538,7 +3784,6 @@
3538
  "version": "0.5.0",
3539
  "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
3540
  "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
3541
- "dev": true,
3542
  "engines": {
3543
  "node": ">= 0.6"
3544
  }
@@ -4351,6 +4596,14 @@
4351
  "node": ">=0.8.x"
4352
  }
4353
  },
 
 
 
 
 
 
 
 
4354
  "node_modules/execa": {
4355
  "version": "5.1.1",
4356
  "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@@ -4803,6 +5056,14 @@
4803
  "node": ">=14"
4804
  }
4805
  },
 
 
 
 
 
 
 
 
4806
  "node_modules/get-func-name": {
4807
  "version": "2.0.2",
4808
  "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
@@ -4971,6 +5232,14 @@
4971
  "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
4972
  "dev": true
4973
  },
 
 
 
 
 
 
 
 
4974
  "node_modules/gtoken": {
4975
  "version": "7.1.0",
4976
  "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
@@ -5013,7 +5282,6 @@
5013
  "version": "4.0.0",
5014
  "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
5015
  "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
5016
- "dev": true,
5017
  "engines": {
5018
  "node": ">=8"
5019
  }
@@ -5067,6 +5335,11 @@
5067
  "node": ">= 0.4"
5068
  }
5069
  },
 
 
 
 
 
5070
  "node_modules/help-me": {
5071
  "version": "5.0.0",
5072
  "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
@@ -5356,6 +5629,14 @@
5356
  "node": ">=0.10.0"
5357
  }
5358
  },
 
 
 
 
 
 
 
 
5359
  "node_modules/is-glob": {
5360
  "version": "4.0.3",
5361
  "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@@ -5373,6 +5654,11 @@
5373
  "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
5374
  "dev": true
5375
  },
 
 
 
 
 
5376
  "node_modules/is-number": {
5377
  "version": "7.0.0",
5378
  "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -6101,6 +6387,77 @@
6101
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
6102
  "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
6103
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6104
  "node_modules/mz": {
6105
  "version": "2.7.0",
6106
  "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
@@ -6233,8 +6590,6 @@
6233
  "version": "4.6.1",
6234
  "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.1.tgz",
6235
  "integrity": "sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==",
6236
- "optional": true,
6237
- "peer": true,
6238
  "bin": {
6239
  "node-gyp-build": "bin.js",
6240
  "node-gyp-build-optional": "optional.js",
@@ -6473,6 +6828,11 @@
6473
  "node": ">= 0.8.0"
6474
  }
6475
  },
 
 
 
 
 
6476
  "node_modules/p-limit": {
6477
  "version": "3.1.0",
6478
  "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -6866,11 +7226,11 @@
6866
  "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="
6867
  },
6868
  "node_modules/playwright": {
6869
- "version": "1.43.1",
6870
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.43.1.tgz",
6871
- "integrity": "sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==",
6872
  "dependencies": {
6873
- "playwright-core": "1.43.1"
6874
  },
6875
  "bin": {
6876
  "playwright": "cli.js"
@@ -6906,6 +7266,17 @@
6906
  "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
6907
  }
6908
  },
 
 
 
 
 
 
 
 
 
 
 
6909
  "node_modules/postcss": {
6910
  "version": "8.4.35",
6911
  "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz",
@@ -7542,6 +7913,14 @@
7542
  "node": ">= 12.13.0"
7543
  }
7544
  },
 
 
 
 
 
 
 
 
7545
  "node_modules/requires-port": {
7546
  "version": "1.0.0",
7547
  "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
@@ -7783,6 +8162,14 @@
7783
  "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
7784
  "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="
7785
  },
 
 
 
 
 
 
 
 
7786
  "node_modules/semver": {
7787
  "version": "7.6.2",
7788
  "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
@@ -7885,9 +8272,9 @@
7885
  "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
7886
  },
7887
  "node_modules/sharp": {
7888
- "version": "0.33.3",
7889
- "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.3.tgz",
7890
- "integrity": "sha512-vHUeXJU1UvlO/BNwTpT0x/r53WkLUVxrmb5JTgW92fdFCFk0ispLMAeu/jPO2vjkXM1fYUi3K7/qcLF47pwM1A==",
7891
  "hasInstallScript": true,
7892
  "dependencies": {
7893
  "color": "^4.2.3",
@@ -7902,8 +8289,8 @@
7902
  "url": "https://opencollective.com/libvips"
7903
  },
7904
  "optionalDependencies": {
7905
- "@img/sharp-darwin-arm64": "0.33.3",
7906
- "@img/sharp-darwin-x64": "0.33.3",
7907
  "@img/sharp-libvips-darwin-arm64": "1.0.2",
7908
  "@img/sharp-libvips-darwin-x64": "1.0.2",
7909
  "@img/sharp-libvips-linux-arm": "1.0.2",
@@ -7912,15 +8299,15 @@
7912
  "@img/sharp-libvips-linux-x64": "1.0.2",
7913
  "@img/sharp-libvips-linuxmusl-arm64": "1.0.2",
7914
  "@img/sharp-libvips-linuxmusl-x64": "1.0.2",
7915
- "@img/sharp-linux-arm": "0.33.3",
7916
- "@img/sharp-linux-arm64": "0.33.3",
7917
- "@img/sharp-linux-s390x": "0.33.3",
7918
- "@img/sharp-linux-x64": "0.33.3",
7919
- "@img/sharp-linuxmusl-arm64": "0.33.3",
7920
- "@img/sharp-linuxmusl-x64": "0.33.3",
7921
- "@img/sharp-wasm32": "0.33.3",
7922
- "@img/sharp-win32-ia32": "0.33.3",
7923
- "@img/sharp-win32-x64": "0.33.3"
7924
  }
7925
  },
7926
  "node_modules/shebang-command": {
@@ -8163,6 +8550,11 @@
8163
  "queue-tick": "^1.0.1"
8164
  }
8165
  },
 
 
 
 
 
8166
  "node_modules/string_decoder": {
8167
  "version": "1.3.0",
8168
  "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -8171,6 +8563,24 @@
8171
  "safe-buffer": "~5.2.0"
8172
  }
8173
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8174
  "node_modules/string.prototype.codepointat": {
8175
  "version": "0.2.1",
8176
  "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz",
@@ -8180,7 +8590,6 @@
8180
  "version": "6.0.1",
8181
  "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
8182
  "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
8183
- "dev": true,
8184
  "dependencies": {
8185
  "ansi-regex": "^5.0.1"
8186
  },
@@ -8292,7 +8701,6 @@
8292
  "version": "7.2.0",
8293
  "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
8294
  "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
8295
- "dev": true,
8296
  "dependencies": {
8297
  "has-flag": "^4.0.0"
8298
  },
@@ -8952,7 +9360,6 @@
8952
  "version": "5.2.2",
8953
  "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
8954
  "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
8955
- "devOptional": true,
8956
  "bin": {
8957
  "tsc": "bin/tsc",
8958
  "tsserver": "bin/tsserver"
@@ -8995,6 +9402,11 @@
8995
  "node": ">=14.0"
8996
  }
8997
  },
 
 
 
 
 
8998
  "node_modules/unicode-trie": {
8999
  "version": "2.0.0",
9000
  "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
@@ -9960,6 +10372,19 @@
9960
  "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
9961
  "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="
9962
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
9963
  "node_modules/wrappy": {
9964
  "version": "1.0.2",
9965
  "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -9998,6 +10423,14 @@
9998
  "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
9999
  "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
10000
  },
 
 
 
 
 
 
 
 
10001
  "node_modules/yallist": {
10002
  "version": "4.0.0",
10003
  "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
@@ -10012,6 +10445,31 @@
10012
  "node": ">= 6"
10013
  }
10014
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10015
  "node_modules/yn": {
10016
  "version": "3.1.1",
10017
  "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
 
9
  "version": "0.8.4",
10
  "dependencies": {
11
  "@cliqz/adblocker-playwright": "^1.27.2",
12
+ "@gradio/client": "^0.19.4",
13
  "@huggingface/hub": "^0.5.1",
14
+ "@huggingface/inference": "^2.7.0",
15
  "@iconify-json/bi": "^1.1.21",
16
  "@playwright/browser-chromium": "^1.43.1",
17
  "@resvg/resvg-js": "^2.6.2",
 
43
  "satori-html": "^0.3.2",
44
  "sbd": "^1.0.19",
45
  "serpapi": "^1.1.1",
46
+ "sharp": "^0.33.4",
47
  "tailwind-scrollbar": "^3.0.0",
48
  "tailwindcss": "^3.4.0",
49
  "uuid": "^9.0.1",
 
175
  "google-auth-library": "^9.4.2"
176
  }
177
  },
178
+ "node_modules/@bundled-es-modules/cookie": {
179
+ "version": "2.0.0",
180
+ "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.0.tgz",
181
+ "integrity": "sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==",
182
+ "dependencies": {
183
+ "cookie": "^0.5.0"
184
+ }
185
+ },
186
+ "node_modules/@bundled-es-modules/statuses": {
187
+ "version": "1.0.1",
188
+ "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz",
189
+ "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==",
190
+ "dependencies": {
191
+ "statuses": "^2.0.1"
192
+ }
193
+ },
194
  "node_modules/@cliqz/adblocker": {
195
  "version": "1.27.2",
196
  "resolved": "https://registry.npmjs.org/@cliqz/adblocker/-/adblocker-1.27.2.tgz",
 
723
  "node": ">=18.0.0"
724
  }
725
  },
726
+ "node_modules/@gradio/client": {
727
+ "version": "0.19.4",
728
+ "resolved": "https://registry.npmjs.org/@gradio/client/-/client-0.19.4.tgz",
729
+ "integrity": "sha512-O7bSkgoGL7Fe180UTB9IF9ZlIMdiIhWkInE22NDCsH3Lyt7QdOxDSRKZUEYTPELUIBrejWiuQ1ADNJQzDTBvBg==",
730
+ "dependencies": {
731
+ "@types/eventsource": "^1.1.15",
732
+ "bufferutil": "^4.0.7",
733
+ "eventsource": "^2.0.2",
734
+ "msw": "^2.2.1",
735
+ "semiver": "^1.1.0",
736
+ "typescript": "^5.0.0",
737
+ "ws": "^8.13.0"
738
+ },
739
+ "engines": {
740
+ "node": ">=18.0.0"
741
+ }
742
+ },
743
  "node_modules/@huggingface/hub": {
744
  "version": "0.5.1",
745
  "resolved": "https://registry.npmjs.org/@huggingface/hub/-/hub-0.5.1.tgz",
 
752
  }
753
  },
754
  "node_modules/@huggingface/inference": {
755
+ "version": "2.7.0",
756
+ "resolved": "https://registry.npmjs.org/@huggingface/inference/-/inference-2.7.0.tgz",
757
+ "integrity": "sha512-u7Fn637Q3f7nUB1tajM4CgzhvoFQkOQr5W5Fm+2wT9ETgGoLBh25BLlYPTJRjAd2WY01s71v0lqAwNvHHCc3mg==",
758
+ "dependencies": {
759
+ "@huggingface/tasks": "^0.10.0"
760
+ },
761
  "engines": {
762
  "node": ">=18"
763
  }
 
770
  "node": ">=18"
771
  }
772
  },
773
+ "node_modules/@huggingface/tasks": {
774
+ "version": "0.10.8",
775
+ "resolved": "https://registry.npmjs.org/@huggingface/tasks/-/tasks-0.10.8.tgz",
776
+ "integrity": "sha512-oIp9912FwByyyyxkB/CIiW203QxPeYzBG7dydLmqZdcW0ma0if1uklGumqkJ6y/rJlv7AR/jJ9wbee0Wl5YZTw=="
777
+ },
778
  "node_modules/@humanwhocodes/config-array": {
779
  "version": "0.11.8",
780
  "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
 
854
  }
855
  },
856
  "node_modules/@img/sharp-darwin-arm64": {
857
+ "version": "0.33.4",
858
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.4.tgz",
859
+ "integrity": "sha512-p0suNqXufJs9t3RqLBO6vvrgr5OhgbWp76s5gTRvdmxmuv9E1rcaqGUsl3l4mKVmXPkTkTErXediAui4x+8PSA==",
860
  "cpu": [
861
  "arm64"
862
  ],
 
879
  }
880
  },
881
  "node_modules/@img/sharp-darwin-x64": {
882
+ "version": "0.33.4",
883
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.4.tgz",
884
+ "integrity": "sha512-0l7yRObwtTi82Z6ebVI2PnHT8EB2NxBgpK2MiKJZJ7cz32R4lxd001ecMhzzsZig3Yv9oclvqqdV93jo9hy+Dw==",
885
  "cpu": [
886
  "x64"
887
  ],
 
1072
  }
1073
  },
1074
  "node_modules/@img/sharp-linux-arm": {
1075
+ "version": "0.33.4",
1076
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.4.tgz",
1077
+ "integrity": "sha512-RUgBD1c0+gCYZGCCe6mMdTiOFS0Zc/XrN0fYd6hISIKcDUbAW5NtSQW9g/powkrXYm6Vzwd6y+fqmExDuCdHNQ==",
1078
  "cpu": [
1079
  "arm"
1080
  ],
 
1097
  }
1098
  },
1099
  "node_modules/@img/sharp-linux-arm64": {
1100
+ "version": "0.33.4",
1101
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.4.tgz",
1102
+ "integrity": "sha512-2800clwVg1ZQtxwSoTlHvtm9ObgAax7V6MTAB/hDT945Tfyy3hVkmiHpeLPCKYqYR1Gcmv1uDZ3a4OFwkdBL7Q==",
1103
  "cpu": [
1104
  "arm64"
1105
  ],
 
1122
  }
1123
  },
1124
  "node_modules/@img/sharp-linux-s390x": {
1125
+ "version": "0.33.4",
1126
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.4.tgz",
1127
+ "integrity": "sha512-h3RAL3siQoyzSoH36tUeS0PDmb5wINKGYzcLB5C6DIiAn2F3udeFAum+gj8IbA/82+8RGCTn7XW8WTFnqag4tQ==",
1128
  "cpu": [
1129
  "s390x"
1130
  ],
 
1133
  "linux"
1134
  ],
1135
  "engines": {
1136
+ "glibc": ">=2.31",
1137
  "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
1138
  "npm": ">=9.6.5",
1139
  "pnpm": ">=7.1.0",
 
1147
  }
1148
  },
1149
  "node_modules/@img/sharp-linux-x64": {
1150
+ "version": "0.33.4",
1151
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.4.tgz",
1152
+ "integrity": "sha512-GoR++s0XW9DGVi8SUGQ/U4AeIzLdNjHka6jidVwapQ/JebGVQIpi52OdyxCNVRE++n1FCLzjDovJNozif7w/Aw==",
1153
  "cpu": [
1154
  "x64"
1155
  ],
 
1172
  }
1173
  },
1174
  "node_modules/@img/sharp-linuxmusl-arm64": {
1175
+ "version": "0.33.4",
1176
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.4.tgz",
1177
+ "integrity": "sha512-nhr1yC3BlVrKDTl6cO12gTpXMl4ITBUZieehFvMntlCXFzH2bvKG76tBL2Y/OqhupZt81pR7R+Q5YhJxW0rGgQ==",
1178
  "cpu": [
1179
  "arm64"
1180
  ],
 
1197
  }
1198
  },
1199
  "node_modules/@img/sharp-linuxmusl-x64": {
1200
+ "version": "0.33.4",
1201
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.4.tgz",
1202
+ "integrity": "sha512-uCPTku0zwqDmZEOi4ILyGdmW76tH7dm8kKlOIV1XC5cLyJ71ENAAqarOHQh0RLfpIpbV5KOpXzdU6XkJtS0daw==",
1203
  "cpu": [
1204
  "x64"
1205
  ],
 
1222
  }
1223
  },
1224
  "node_modules/@img/sharp-wasm32": {
1225
+ "version": "0.33.4",
1226
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.4.tgz",
1227
+ "integrity": "sha512-Bmmauh4sXUsUqkleQahpdNXKvo+wa1V9KhT2pDA4VJGKwnKMJXiSTGphn0gnJrlooda0QxCtXc6RX1XAU6hMnQ==",
1228
  "cpu": [
1229
  "wasm32"
1230
  ],
1231
  "optional": true,
1232
  "dependencies": {
1233
+ "@emnapi/runtime": "^1.1.1"
1234
  },
1235
  "engines": {
1236
  "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
 
1243
  }
1244
  },
1245
  "node_modules/@img/sharp-win32-ia32": {
1246
+ "version": "0.33.4",
1247
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.4.tgz",
1248
+ "integrity": "sha512-99SJ91XzUhYHbx7uhK3+9Lf7+LjwMGQZMDlO/E/YVJ7Nc3lyDFZPGhjwiYdctoH2BOzW9+TnfqcaMKt0jHLdqw==",
1249
  "cpu": [
1250
  "ia32"
1251
  ],
 
1264
  }
1265
  },
1266
  "node_modules/@img/sharp-win32-x64": {
1267
+ "version": "0.33.4",
1268
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.4.tgz",
1269
+ "integrity": "sha512-3QLocdTRVIrFNye5YocZl+KKpYKP+fksi1QhmOArgx7GyhIbQp/WrJRu176jm8IxromS7RIkzMiMINVdBtC8Aw==",
1270
  "cpu": [
1271
  "x64"
1272
  ],
 
1284
  "url": "https://opencollective.com/libvips"
1285
  }
1286
  },
1287
+ "node_modules/@inquirer/confirm": {
1288
+ "version": "3.1.7",
1289
+ "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-3.1.7.tgz",
1290
+ "integrity": "sha512-BZjjj19W8gnh5UGFTdP5ZxpgMNRjy03Dzq3k28sB2MDlEUFrcyTkMEoGgvBmGpUw0vNBoCJkTcbHZ3e9tb+d+w==",
1291
+ "dependencies": {
1292
+ "@inquirer/core": "^8.2.0",
1293
+ "@inquirer/type": "^1.3.1"
1294
+ },
1295
+ "engines": {
1296
+ "node": ">=18"
1297
+ }
1298
+ },
1299
+ "node_modules/@inquirer/core": {
1300
+ "version": "8.2.0",
1301
+ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-8.2.0.tgz",
1302
+ "integrity": "sha512-pexNF9j2orvMMTgoQ/uKOw8V6/R7x/sIDwRwXRhl4i0pPSh6paRzFehpFKpfMbqix1/+gzCekhYTmVbQpWkVjQ==",
1303
+ "dependencies": {
1304
+ "@inquirer/figures": "^1.0.1",
1305
+ "@inquirer/type": "^1.3.1",
1306
+ "@types/mute-stream": "^0.0.4",
1307
+ "@types/node": "^20.12.11",
1308
+ "@types/wrap-ansi": "^3.0.0",
1309
+ "ansi-escapes": "^4.3.2",
1310
+ "chalk": "^4.1.2",
1311
+ "cli-spinners": "^2.9.2",
1312
+ "cli-width": "^4.1.0",
1313
+ "mute-stream": "^1.0.0",
1314
+ "signal-exit": "^4.1.0",
1315
+ "strip-ansi": "^6.0.1",
1316
+ "wrap-ansi": "^6.2.0"
1317
+ },
1318
+ "engines": {
1319
+ "node": ">=18"
1320
+ }
1321
+ },
1322
+ "node_modules/@inquirer/core/node_modules/@types/node": {
1323
+ "version": "20.12.12",
1324
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
1325
+ "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
1326
+ "dependencies": {
1327
+ "undici-types": "~5.26.4"
1328
+ }
1329
+ },
1330
+ "node_modules/@inquirer/core/node_modules/signal-exit": {
1331
+ "version": "4.1.0",
1332
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
1333
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
1334
+ "engines": {
1335
+ "node": ">=14"
1336
+ },
1337
+ "funding": {
1338
+ "url": "https://github.com/sponsors/isaacs"
1339
+ }
1340
+ },
1341
+ "node_modules/@inquirer/figures": {
1342
+ "version": "1.0.1",
1343
+ "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.1.tgz",
1344
+ "integrity": "sha512-mtup3wVKia3ZwULPHcbs4Mor8Voi+iIXEWD7wCNbIO6lYR62oPCTQyrddi5OMYVXHzeCSoneZwJuS8sBvlEwDw==",
1345
+ "engines": {
1346
+ "node": ">=18"
1347
+ }
1348
+ },
1349
+ "node_modules/@inquirer/type": {
1350
+ "version": "1.3.1",
1351
+ "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.3.1.tgz",
1352
+ "integrity": "sha512-Pe3PFccjPVJV1vtlfVvm9OnlbxqdnP5QcscFEFEnK5quChf1ufZtM0r8mR5ToWHMxZOh0s8o/qp9ANGRTo/DAw==",
1353
+ "engines": {
1354
+ "node": ">=18"
1355
+ }
1356
+ },
1357
  "node_modules/@jridgewell/gen-mapping": {
1358
  "version": "0.3.3",
1359
  "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
 
1406
  "sparse-bitfield": "^3.0.3"
1407
  }
1408
  },
1409
+ "node_modules/@mswjs/cookies": {
1410
+ "version": "1.1.0",
1411
+ "resolved": "https://registry.npmjs.org/@mswjs/cookies/-/cookies-1.1.0.tgz",
1412
+ "integrity": "sha512-0ZcCVQxifZmhwNBoQIrystCb+2sWBY2Zw8lpfJBPCHGCA/HWqehITeCRVIv4VMy8MPlaHo2w2pTHFV2pFfqKPw==",
1413
+ "engines": {
1414
+ "node": ">=18"
1415
+ }
1416
+ },
1417
+ "node_modules/@mswjs/interceptors": {
1418
+ "version": "0.29.1",
1419
+ "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.29.1.tgz",
1420
+ "integrity": "sha512-3rDakgJZ77+RiQUuSK69t1F0m8BQKA8Vh5DCS5V0DWvNY67zob2JhhQrhCO0AKLGINTRSFd1tBaHcJTkhefoSw==",
1421
+ "dependencies": {
1422
+ "@open-draft/deferred-promise": "^2.2.0",
1423
+ "@open-draft/logger": "^0.3.0",
1424
+ "@open-draft/until": "^2.0.0",
1425
+ "is-node-process": "^1.2.0",
1426
+ "outvariant": "^1.2.1",
1427
+ "strict-event-emitter": "^0.5.1"
1428
+ },
1429
+ "engines": {
1430
+ "node": ">=18"
1431
+ }
1432
+ },
1433
  "node_modules/@nodelib/fs.scandir": {
1434
  "version": "2.1.5",
1435
  "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
 
1462
  "node": ">= 8"
1463
  }
1464
  },
1465
+ "node_modules/@open-draft/deferred-promise": {
1466
+ "version": "2.2.0",
1467
+ "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz",
1468
+ "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA=="
1469
+ },
1470
+ "node_modules/@open-draft/logger": {
1471
+ "version": "0.3.0",
1472
+ "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz",
1473
+ "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==",
1474
+ "dependencies": {
1475
+ "is-node-process": "^1.2.0",
1476
+ "outvariant": "^1.4.0"
1477
+ }
1478
+ },
1479
+ "node_modules/@open-draft/until": {
1480
+ "version": "2.1.0",
1481
+ "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz",
1482
+ "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="
1483
+ },
1484
  "node_modules/@opentelemetry/api": {
1485
  "version": "1.8.0",
1486
  "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.8.0.tgz",
 
2323
  "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
2324
  "dev": true
2325
  },
2326
+ "node_modules/@types/eventsource": {
2327
+ "version": "1.1.15",
2328
+ "resolved": "https://registry.npmjs.org/@types/eventsource/-/eventsource-1.1.15.tgz",
2329
+ "integrity": "sha512-XQmGcbnxUNa06HR3VBVkc9+A2Vpi9ZyLJcdS5dwaQQ/4ZMWFO+5c90FnMUpbtMZwB/FChoYHwuVg8TvkECacTA=="
2330
+ },
2331
  "node_modules/@types/express": {
2332
  "version": "4.17.21",
2333
  "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
 
2426
  "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==",
2427
  "dev": true
2428
  },
2429
+ "node_modules/@types/mute-stream": {
2430
+ "version": "0.0.4",
2431
+ "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz",
2432
+ "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==",
2433
+ "dependencies": {
2434
+ "@types/node": "*"
2435
+ }
2436
+ },
2437
  "node_modules/@types/node": {
2438
  "version": "18.13.0",
2439
  "resolved": "https://registry.npmjs.org/@types/node/-/node-18.13.0.tgz",
 
2524
  "@types/send": "*"
2525
  }
2526
  },
2527
+ "node_modules/@types/statuses": {
2528
+ "version": "2.0.5",
2529
+ "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.5.tgz",
2530
+ "integrity": "sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A=="
2531
+ },
2532
  "node_modules/@types/tough-cookie": {
2533
  "version": "4.0.2",
2534
  "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz",
 
2555
  "@types/webidl-conversions": "*"
2556
  }
2557
  },
2558
+ "node_modules/@types/wrap-ansi": {
2559
+ "version": "3.0.0",
2560
+ "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz",
2561
+ "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g=="
2562
+ },
2563
  "node_modules/@typescript-eslint/eslint-plugin": {
2564
  "version": "6.7.4",
2565
  "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.4.tgz",
 
2977
  "url": "https://github.com/sponsors/epoberezkin"
2978
  }
2979
  },
2980
+ "node_modules/ansi-escapes": {
2981
+ "version": "4.3.2",
2982
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
2983
+ "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
2984
+ "dependencies": {
2985
+ "type-fest": "^0.21.3"
2986
+ },
2987
+ "engines": {
2988
+ "node": ">=8"
2989
+ },
2990
+ "funding": {
2991
+ "url": "https://github.com/sponsors/sindresorhus"
2992
+ }
2993
+ },
2994
+ "node_modules/ansi-escapes/node_modules/type-fest": {
2995
+ "version": "0.21.3",
2996
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
2997
+ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
2998
+ "engines": {
2999
+ "node": ">=10"
3000
+ },
3001
+ "funding": {
3002
+ "url": "https://github.com/sponsors/sindresorhus"
3003
+ }
3004
+ },
3005
  "node_modules/ansi-regex": {
3006
  "version": "5.0.1",
3007
  "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
3008
  "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
 
3009
  "engines": {
3010
  "node": ">=8"
3011
  }
 
3014
  "version": "4.3.0",
3015
  "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
3016
  "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
 
3017
  "dependencies": {
3018
  "color-convert": "^2.0.1"
3019
  },
 
3396
  "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.7.tgz",
3397
  "integrity": "sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==",
3398
  "hasInstallScript": true,
 
 
3399
  "dependencies": {
3400
  "node-gyp-build": "^4.3.0"
3401
  },
 
3516
  "version": "4.1.2",
3517
  "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
3518
  "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
 
3519
  "dependencies": {
3520
  "ansi-styles": "^4.1.0",
3521
  "supports-color": "^7.1.0"
 
3587
  "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
3588
  "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
3589
  },
3590
+ "node_modules/cli-spinners": {
3591
+ "version": "2.9.2",
3592
+ "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
3593
+ "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
3594
+ "engines": {
3595
+ "node": ">=6"
3596
+ },
3597
+ "funding": {
3598
+ "url": "https://github.com/sponsors/sindresorhus"
3599
+ }
3600
+ },
3601
+ "node_modules/cli-width": {
3602
+ "version": "4.1.0",
3603
+ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
3604
+ "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
3605
+ "engines": {
3606
+ "node": ">= 12"
3607
+ }
3608
+ },
3609
+ "node_modules/cliui": {
3610
+ "version": "8.0.1",
3611
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
3612
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
3613
+ "dependencies": {
3614
+ "string-width": "^4.2.0",
3615
+ "strip-ansi": "^6.0.1",
3616
+ "wrap-ansi": "^7.0.0"
3617
+ },
3618
+ "engines": {
3619
+ "node": ">=12"
3620
+ }
3621
+ },
3622
+ "node_modules/cliui/node_modules/wrap-ansi": {
3623
+ "version": "7.0.0",
3624
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
3625
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
3626
+ "dependencies": {
3627
+ "ansi-styles": "^4.0.0",
3628
+ "string-width": "^4.1.0",
3629
+ "strip-ansi": "^6.0.0"
3630
+ },
3631
+ "engines": {
3632
+ "node": ">=10"
3633
+ },
3634
+ "funding": {
3635
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
3636
+ }
3637
+ },
3638
  "node_modules/code-red": {
3639
  "version": "1.0.4",
3640
  "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz",
 
3784
  "version": "0.5.0",
3785
  "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
3786
  "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
 
3787
  "engines": {
3788
  "node": ">= 0.6"
3789
  }
 
4596
  "node": ">=0.8.x"
4597
  }
4598
  },
4599
+ "node_modules/eventsource": {
4600
+ "version": "2.0.2",
4601
+ "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz",
4602
+ "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==",
4603
+ "engines": {
4604
+ "node": ">=12.0.0"
4605
+ }
4606
+ },
4607
  "node_modules/execa": {
4608
  "version": "5.1.1",
4609
  "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
 
5056
  "node": ">=14"
5057
  }
5058
  },
5059
+ "node_modules/get-caller-file": {
5060
+ "version": "2.0.5",
5061
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
5062
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
5063
+ "engines": {
5064
+ "node": "6.* || 8.* || >= 10.*"
5065
+ }
5066
+ },
5067
  "node_modules/get-func-name": {
5068
  "version": "2.0.2",
5069
  "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
 
5232
  "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
5233
  "dev": true
5234
  },
5235
+ "node_modules/graphql": {
5236
+ "version": "16.8.1",
5237
+ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz",
5238
+ "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==",
5239
+ "engines": {
5240
+ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
5241
+ }
5242
+ },
5243
  "node_modules/gtoken": {
5244
  "version": "7.1.0",
5245
  "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
 
5282
  "version": "4.0.0",
5283
  "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
5284
  "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
 
5285
  "engines": {
5286
  "node": ">=8"
5287
  }
 
5335
  "node": ">= 0.4"
5336
  }
5337
  },
5338
+ "node_modules/headers-polyfill": {
5339
+ "version": "4.0.3",
5340
+ "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz",
5341
+ "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="
5342
+ },
5343
  "node_modules/help-me": {
5344
  "version": "5.0.0",
5345
  "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
 
5629
  "node": ">=0.10.0"
5630
  }
5631
  },
5632
+ "node_modules/is-fullwidth-code-point": {
5633
+ "version": "3.0.0",
5634
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
5635
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
5636
+ "engines": {
5637
+ "node": ">=8"
5638
+ }
5639
+ },
5640
  "node_modules/is-glob": {
5641
  "version": "4.0.3",
5642
  "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
 
5654
  "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
5655
  "dev": true
5656
  },
5657
+ "node_modules/is-node-process": {
5658
+ "version": "1.2.0",
5659
+ "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz",
5660
+ "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw=="
5661
+ },
5662
  "node_modules/is-number": {
5663
  "version": "7.0.0",
5664
  "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
 
6387
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
6388
  "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
6389
  },
6390
+ "node_modules/msw": {
6391
+ "version": "2.3.0",
6392
+ "resolved": "https://registry.npmjs.org/msw/-/msw-2.3.0.tgz",
6393
+ "integrity": "sha512-cDr1q/QTMzaWhY8n9lpGhceY209k29UZtdTgJ3P8Bzne3TSMchX2EM/ldvn4ATLOktpCefCU2gcEgzHc31GTPw==",
6394
+ "hasInstallScript": true,
6395
+ "dependencies": {
6396
+ "@bundled-es-modules/cookie": "^2.0.0",
6397
+ "@bundled-es-modules/statuses": "^1.0.1",
6398
+ "@inquirer/confirm": "^3.0.0",
6399
+ "@mswjs/cookies": "^1.1.0",
6400
+ "@mswjs/interceptors": "^0.29.0",
6401
+ "@open-draft/until": "^2.1.0",
6402
+ "@types/cookie": "^0.6.0",
6403
+ "@types/statuses": "^2.0.4",
6404
+ "chalk": "^4.1.2",
6405
+ "graphql": "^16.8.1",
6406
+ "headers-polyfill": "^4.0.2",
6407
+ "is-node-process": "^1.2.0",
6408
+ "outvariant": "^1.4.2",
6409
+ "path-to-regexp": "^6.2.0",
6410
+ "strict-event-emitter": "^0.5.1",
6411
+ "type-fest": "^4.9.0",
6412
+ "yargs": "^17.7.2"
6413
+ },
6414
+ "bin": {
6415
+ "msw": "cli/index.js"
6416
+ },
6417
+ "engines": {
6418
+ "node": ">=18"
6419
+ },
6420
+ "funding": {
6421
+ "url": "https://github.com/sponsors/mswjs"
6422
+ },
6423
+ "peerDependencies": {
6424
+ "typescript": ">= 4.7.x"
6425
+ },
6426
+ "peerDependenciesMeta": {
6427
+ "typescript": {
6428
+ "optional": true
6429
+ }
6430
+ }
6431
+ },
6432
+ "node_modules/msw/node_modules/@types/cookie": {
6433
+ "version": "0.6.0",
6434
+ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
6435
+ "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="
6436
+ },
6437
+ "node_modules/msw/node_modules/path-to-regexp": {
6438
+ "version": "6.2.2",
6439
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz",
6440
+ "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw=="
6441
+ },
6442
+ "node_modules/msw/node_modules/type-fest": {
6443
+ "version": "4.18.2",
6444
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.18.2.tgz",
6445
+ "integrity": "sha512-+suCYpfJLAe4OXS6+PPXjW3urOS4IoP9waSiLuXfLgqZODKw/aWwASvzqE886wA0kQgGy0mIWyhd87VpqIy6Xg==",
6446
+ "engines": {
6447
+ "node": ">=16"
6448
+ },
6449
+ "funding": {
6450
+ "url": "https://github.com/sponsors/sindresorhus"
6451
+ }
6452
+ },
6453
+ "node_modules/mute-stream": {
6454
+ "version": "1.0.0",
6455
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz",
6456
+ "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==",
6457
+ "engines": {
6458
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
6459
+ }
6460
+ },
6461
  "node_modules/mz": {
6462
  "version": "2.7.0",
6463
  "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
 
6590
  "version": "4.6.1",
6591
  "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.1.tgz",
6592
  "integrity": "sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==",
 
 
6593
  "bin": {
6594
  "node-gyp-build": "bin.js",
6595
  "node-gyp-build-optional": "optional.js",
 
6828
  "node": ">= 0.8.0"
6829
  }
6830
  },
6831
+ "node_modules/outvariant": {
6832
+ "version": "1.4.2",
6833
+ "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.2.tgz",
6834
+ "integrity": "sha512-Ou3dJ6bA/UJ5GVHxah4LnqDwZRwAmWxrG3wtrHrbGnP4RnLCtA64A4F+ae7Y8ww660JaddSoArUR5HjipWSHAQ=="
6835
+ },
6836
  "node_modules/p-limit": {
6837
  "version": "3.1.0",
6838
  "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
 
7226
  "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="
7227
  },
7228
  "node_modules/playwright": {
7229
+ "version": "1.40.0",
7230
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.40.0.tgz",
7231
+ "integrity": "sha512-gyHAgQjiDf1m34Xpwzaqb76KgfzYrhK7iih+2IzcOCoZWr/8ZqmdBw+t0RU85ZmfJMgtgAiNtBQ/KS2325INXw==",
7232
  "dependencies": {
7233
+ "playwright-core": "1.40.0"
7234
  },
7235
  "bin": {
7236
  "playwright": "cli.js"
 
7266
  "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
7267
  }
7268
  },
7269
+ "node_modules/playwright/node_modules/playwright-core": {
7270
+ "version": "1.40.0",
7271
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.40.0.tgz",
7272
+ "integrity": "sha512-fvKewVJpGeca8t0ipM56jkVSU6Eo0RmFvQ/MaCQNDYm+sdvKkMBBWTE1FdeMqIdumRaXXjZChWHvIzCGM/tA/Q==",
7273
+ "bin": {
7274
+ "playwright-core": "cli.js"
7275
+ },
7276
+ "engines": {
7277
+ "node": ">=16"
7278
+ }
7279
+ },
7280
  "node_modules/postcss": {
7281
  "version": "8.4.35",
7282
  "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz",
 
7913
  "node": ">= 12.13.0"
7914
  }
7915
  },
7916
+ "node_modules/require-directory": {
7917
+ "version": "2.1.1",
7918
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
7919
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
7920
+ "engines": {
7921
+ "node": ">=0.10.0"
7922
+ }
7923
+ },
7924
  "node_modules/requires-port": {
7925
  "version": "1.0.0",
7926
  "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
 
8162
  "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
8163
  "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="
8164
  },
8165
+ "node_modules/semiver": {
8166
+ "version": "1.1.0",
8167
+ "resolved": "https://registry.npmjs.org/semiver/-/semiver-1.1.0.tgz",
8168
+ "integrity": "sha512-QNI2ChmuioGC1/xjyYwyZYADILWyW6AmS1UH6gDj/SFUUUS4MBAWs/7mxnkRPc/F4iHezDP+O8t0dO8WHiEOdg==",
8169
+ "engines": {
8170
+ "node": ">=6"
8171
+ }
8172
+ },
8173
  "node_modules/semver": {
8174
  "version": "7.6.2",
8175
  "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
 
8272
  "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
8273
  },
8274
  "node_modules/sharp": {
8275
+ "version": "0.33.4",
8276
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.4.tgz",
8277
+ "integrity": "sha512-7i/dt5kGl7qR4gwPRD2biwD2/SvBn3O04J77XKFgL2OnZtQw+AG9wnuS/csmu80nPRHLYE9E41fyEiG8nhH6/Q==",
8278
  "hasInstallScript": true,
8279
  "dependencies": {
8280
  "color": "^4.2.3",
 
8289
  "url": "https://opencollective.com/libvips"
8290
  },
8291
  "optionalDependencies": {
8292
+ "@img/sharp-darwin-arm64": "0.33.4",
8293
+ "@img/sharp-darwin-x64": "0.33.4",
8294
  "@img/sharp-libvips-darwin-arm64": "1.0.2",
8295
  "@img/sharp-libvips-darwin-x64": "1.0.2",
8296
  "@img/sharp-libvips-linux-arm": "1.0.2",
 
8299
  "@img/sharp-libvips-linux-x64": "1.0.2",
8300
  "@img/sharp-libvips-linuxmusl-arm64": "1.0.2",
8301
  "@img/sharp-libvips-linuxmusl-x64": "1.0.2",
8302
+ "@img/sharp-linux-arm": "0.33.4",
8303
+ "@img/sharp-linux-arm64": "0.33.4",
8304
+ "@img/sharp-linux-s390x": "0.33.4",
8305
+ "@img/sharp-linux-x64": "0.33.4",
8306
+ "@img/sharp-linuxmusl-arm64": "0.33.4",
8307
+ "@img/sharp-linuxmusl-x64": "0.33.4",
8308
+ "@img/sharp-wasm32": "0.33.4",
8309
+ "@img/sharp-win32-ia32": "0.33.4",
8310
+ "@img/sharp-win32-x64": "0.33.4"
8311
  }
8312
  },
8313
  "node_modules/shebang-command": {
 
8550
  "queue-tick": "^1.0.1"
8551
  }
8552
  },
8553
+ "node_modules/strict-event-emitter": {
8554
+ "version": "0.5.1",
8555
+ "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz",
8556
+ "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="
8557
+ },
8558
  "node_modules/string_decoder": {
8559
  "version": "1.3.0",
8560
  "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
 
8563
  "safe-buffer": "~5.2.0"
8564
  }
8565
  },
8566
+ "node_modules/string-width": {
8567
+ "version": "4.2.3",
8568
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
8569
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
8570
+ "dependencies": {
8571
+ "emoji-regex": "^8.0.0",
8572
+ "is-fullwidth-code-point": "^3.0.0",
8573
+ "strip-ansi": "^6.0.1"
8574
+ },
8575
+ "engines": {
8576
+ "node": ">=8"
8577
+ }
8578
+ },
8579
+ "node_modules/string-width/node_modules/emoji-regex": {
8580
+ "version": "8.0.0",
8581
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
8582
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
8583
+ },
8584
  "node_modules/string.prototype.codepointat": {
8585
  "version": "0.2.1",
8586
  "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz",
 
8590
  "version": "6.0.1",
8591
  "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
8592
  "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
 
8593
  "dependencies": {
8594
  "ansi-regex": "^5.0.1"
8595
  },
 
8701
  "version": "7.2.0",
8702
  "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
8703
  "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
 
8704
  "dependencies": {
8705
  "has-flag": "^4.0.0"
8706
  },
 
9360
  "version": "5.2.2",
9361
  "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
9362
  "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
 
9363
  "bin": {
9364
  "tsc": "bin/tsc",
9365
  "tsserver": "bin/tsserver"
 
9402
  "node": ">=14.0"
9403
  }
9404
  },
9405
+ "node_modules/undici-types": {
9406
+ "version": "5.26.5",
9407
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
9408
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
9409
+ },
9410
  "node_modules/unicode-trie": {
9411
  "version": "2.0.0",
9412
  "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
 
10372
  "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
10373
  "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="
10374
  },
10375
+ "node_modules/wrap-ansi": {
10376
+ "version": "6.2.0",
10377
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
10378
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
10379
+ "dependencies": {
10380
+ "ansi-styles": "^4.0.0",
10381
+ "string-width": "^4.1.0",
10382
+ "strip-ansi": "^6.0.0"
10383
+ },
10384
+ "engines": {
10385
+ "node": ">=8"
10386
+ }
10387
+ },
10388
  "node_modules/wrappy": {
10389
  "version": "1.0.2",
10390
  "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
 
10423
  "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
10424
  "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
10425
  },
10426
+ "node_modules/y18n": {
10427
+ "version": "5.0.8",
10428
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
10429
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
10430
+ "engines": {
10431
+ "node": ">=10"
10432
+ }
10433
+ },
10434
  "node_modules/yallist": {
10435
  "version": "4.0.0",
10436
  "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
 
10445
  "node": ">= 6"
10446
  }
10447
  },
10448
+ "node_modules/yargs": {
10449
+ "version": "17.7.2",
10450
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
10451
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
10452
+ "dependencies": {
10453
+ "cliui": "^8.0.1",
10454
+ "escalade": "^3.1.1",
10455
+ "get-caller-file": "^2.0.5",
10456
+ "require-directory": "^2.1.1",
10457
+ "string-width": "^4.2.3",
10458
+ "y18n": "^5.0.5",
10459
+ "yargs-parser": "^21.1.1"
10460
+ },
10461
+ "engines": {
10462
+ "node": ">=12"
10463
+ }
10464
+ },
10465
+ "node_modules/yargs-parser": {
10466
+ "version": "21.1.1",
10467
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
10468
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
10469
+ "engines": {
10470
+ "node": ">=12"
10471
+ }
10472
+ },
10473
  "node_modules/yn": {
10474
  "version": "3.1.1",
10475
  "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
package.json CHANGED
@@ -54,11 +54,12 @@
54
  "type": "module",
55
  "dependencies": {
56
  "@cliqz/adblocker-playwright": "^1.27.2",
 
57
  "@huggingface/hub": "^0.5.1",
58
- "@huggingface/inference": "^2.6.3",
59
  "@iconify-json/bi": "^1.1.21",
60
- "@resvg/resvg-js": "^2.6.2",
61
  "@playwright/browser-chromium": "^1.43.1",
 
62
  "@xenova/transformers": "^2.16.1",
63
  "autoprefixer": "^10.4.14",
64
  "browser-image-resizer": "^2.4.1",
@@ -87,7 +88,7 @@
87
  "satori-html": "^0.3.2",
88
  "sbd": "^1.0.19",
89
  "serpapi": "^1.1.1",
90
- "sharp": "^0.33.3",
91
  "tailwind-scrollbar": "^3.0.0",
92
  "tailwindcss": "^3.4.0",
93
  "uuid": "^9.0.1",
 
54
  "type": "module",
55
  "dependencies": {
56
  "@cliqz/adblocker-playwright": "^1.27.2",
57
+ "@gradio/client": "^0.19.4",
58
  "@huggingface/hub": "^0.5.1",
59
+ "@huggingface/inference": "^2.7.0",
60
  "@iconify-json/bi": "^1.1.21",
 
61
  "@playwright/browser-chromium": "^1.43.1",
62
+ "@resvg/resvg-js": "^2.6.2",
63
  "@xenova/transformers": "^2.16.1",
64
  "autoprefixer": "^10.4.14",
65
  "browser-image-resizer": "^2.4.1",
 
88
  "satori-html": "^0.3.2",
89
  "sbd": "^1.0.19",
90
  "serpapi": "^1.1.1",
91
+ "sharp": "^0.33.4",
92
  "tailwind-scrollbar": "^3.0.0",
93
  "tailwindcss": "^3.4.0",
94
  "uuid": "^9.0.1",
src/lib/actions/clickOutside.ts CHANGED
@@ -1,4 +1,4 @@
1
- export function clickOutside(element: HTMLDialogElement, callbackFunction: () => void) {
2
  function onClick(event: MouseEvent) {
3
  if (!element.contains(event.target as Node)) {
4
  callbackFunction();
 
1
+ export function clickOutside(element: HTMLElement, callbackFunction: () => void) {
2
  function onClick(event: MouseEvent) {
3
  if (!element.contains(event.target as Node)) {
4
  callbackFunction();
src/lib/buildPrompt.ts CHANGED
@@ -1,8 +1,11 @@
1
  import type { EndpointParameters } from "./server/endpoints/endpoints";
2
  import type { BackendModel } from "./server/models";
 
3
 
4
  type buildPromptOptions = Pick<EndpointParameters, "messages" | "preprompt" | "continueMessage"> & {
5
  model: BackendModel;
 
 
6
  };
7
 
8
  export async function buildPrompt({
@@ -10,11 +13,18 @@ export async function buildPrompt({
10
  model,
11
  preprompt,
12
  continueMessage,
 
 
13
  }: buildPromptOptions): Promise<string> {
14
  const filteredMessages = messages.filter((m) => m.from !== "system");
15
 
16
  let prompt = model
17
- .chatPromptRender({ messages: filteredMessages, preprompt })
 
 
 
 
 
18
  // Not super precise, but it's truncated in the model's backend anyway
19
  .split(" ")
20
  .slice(-(model.parameters?.truncate ?? 0))
 
1
  import type { EndpointParameters } from "./server/endpoints/endpoints";
2
  import type { BackendModel } from "./server/models";
3
+ import type { Tool, ToolResult } from "./types/Tool";
4
 
5
  type buildPromptOptions = Pick<EndpointParameters, "messages" | "preprompt" | "continueMessage"> & {
6
  model: BackendModel;
7
+ tools?: Tool[];
8
+ toolResults?: ToolResult[];
9
  };
10
 
11
  export async function buildPrompt({
 
13
  model,
14
  preprompt,
15
  continueMessage,
16
+ tools,
17
+ toolResults,
18
  }: buildPromptOptions): Promise<string> {
19
  const filteredMessages = messages.filter((m) => m.from !== "system");
20
 
21
  let prompt = model
22
+ .chatPromptRender({
23
+ messages: filteredMessages,
24
+ preprompt,
25
+ tools,
26
+ toolResults,
27
+ })
28
  // Not super precise, but it's truncated in the model's backend anyway
29
  .split(" ")
30
  .slice(-(model.parameters?.truncate ?? 0))
src/lib/components/OpenWebSearchResults.svelte CHANGED
@@ -1,16 +1,22 @@
1
  <script lang="ts">
2
- import type { WebSearchUpdate } from "$lib/types/MessageUpdate";
 
 
 
 
3
 
4
  import CarbonError from "~icons/carbon/error-filled";
5
  import EosIconsLoading from "~icons/eos-icons/loading";
6
  import IconInternet from "./icons/IconInternet.svelte";
7
 
8
  export let classNames = "";
9
- export let webSearchMessages: WebSearchUpdate[] = [];
10
 
11
- $: sources = webSearchMessages.find((m) => m.sources)?.sources;
12
- $: lastMessage = webSearchMessages.filter((m) => m.messageType !== "sources").slice(-1)[0];
13
- $: loading = !sources && lastMessage.messageType !== "error";
 
 
14
  </script>
15
 
16
  <details
@@ -47,7 +53,7 @@
47
  {#if sources}
48
  Completed
49
  {:else}
50
- {lastMessage.message}
51
  {/if}
52
  </dt>
53
  </dl>
@@ -61,7 +67,7 @@
61
  {:else}
62
  <ol>
63
  {#each webSearchMessages as message}
64
- {#if message.messageType === "update"}
65
  <li class="group border-l pb-6 last:!border-transparent last:pb-0 dark:border-gray-800">
66
  <div class="flex items-start">
67
  <div
@@ -79,7 +85,7 @@
79
  </p>
80
  {/if}
81
  </li>
82
- {:else if message.messageType === "error"}
83
  <li class="group border-l pb-6 last:!border-transparent last:pb-0 dark:border-gray-800">
84
  <div class="flex items-start">
85
  <CarbonError
 
1
  <script lang="ts">
2
+ import {
3
+ MessageWebSearchUpdateType,
4
+ type MessageWebSearchUpdate,
5
+ } from "$lib/types/MessageUpdate";
6
+ import { isMessageWebSearchSourcesUpdate } from "$lib/utils/messageUpdates";
7
 
8
  import CarbonError from "~icons/carbon/error-filled";
9
  import EosIconsLoading from "~icons/eos-icons/loading";
10
  import IconInternet from "./icons/IconInternet.svelte";
11
 
12
  export let classNames = "";
13
+ export let webSearchMessages: MessageWebSearchUpdate[] = [];
14
 
15
+ $: sources = webSearchMessages.find(isMessageWebSearchSourcesUpdate)?.sources;
16
+ $: lastMessage = webSearchMessages
17
+ .filter((update) => update.subtype !== MessageWebSearchUpdateType.Sources)
18
+ .at(-1) as MessageWebSearchUpdate;
19
+ $: loading = !sources && lastMessage.subtype !== MessageWebSearchUpdateType.Error;
20
  </script>
21
 
22
  <details
 
53
  {#if sources}
54
  Completed
55
  {:else}
56
+ {"message" in lastMessage ? lastMessage.message : "An error occurred"}
57
  {/if}
58
  </dt>
59
  </dl>
 
67
  {:else}
68
  <ol>
69
  {#each webSearchMessages as message}
70
+ {#if message.subtype === MessageWebSearchUpdateType.Update}
71
  <li class="group border-l pb-6 last:!border-transparent last:pb-0 dark:border-gray-800">
72
  <div class="flex items-start">
73
  <div
 
85
  </p>
86
  {/if}
87
  </li>
88
+ {:else if message.subtype === MessageWebSearchUpdateType.Error}
89
  <li class="group border-l pb-6 last:!border-transparent last:pb-0 dark:border-gray-800">
90
  <div class="flex items-start">
91
  <CarbonError
src/lib/components/ToolsMenu.svelte ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { page } from "$app/stores";
3
+ import { clickOutside } from "$lib/actions/clickOutside";
4
+ import { useSettingsStore } from "$lib/stores/settings";
5
+ import type { ToolFront } from "$lib/types/Tool";
6
+ import { isHuggingChat } from "$lib/utils/isHuggingChat";
7
+ import IconTool from "./icons/IconTool.svelte";
8
+ import CarbonInformation from "~icons/carbon/information";
9
+
10
+ export let loading = false;
11
+ const settings = useSettingsStore();
12
+
13
+ let detailsEl: HTMLDetailsElement;
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
+ </script>
20
+
21
+ <details
22
+ class="group relative bottom-0 h-full min-h-8"
23
+ bind:this={detailsEl}
24
+ use:clickOutside={() => {
25
+ if (detailsEl.hasAttribute("open")) {
26
+ detailsEl.removeAttribute("open");
27
+ }
28
+ }}
29
+ >
30
+ <summary
31
+ class="absolute bottom-0 flex h-8
32
+ cursor-pointer select-none items-center gap-1 rounded-lg border bg-white px-2 py-1.5 shadow-sm hover:shadow-none dark:border-gray-800 dark:bg-gray-900"
33
+ >
34
+ <IconTool classNames="dark:text-purple-600" />
35
+ Tools
36
+ <span class="text-gray-400 dark:text-gray-500"> ({activeToolCount}) </span>
37
+ </summary>
38
+ <div
39
+ class="absolute bottom-10 h-max w-max select-none items-center gap-1 rounded-lg border bg-white p-0.5 shadow-sm dark:border-gray-800 dark:bg-gray-900"
40
+ >
41
+ <div class="grid grid-cols-2 gap-x-6 gap-y-1 p-3">
42
+ <div class="col-span-2 mb-1 flex items-center gap-1.5 text-sm text-gray-500">
43
+ Available tools
44
+ {#if isHuggingChat}
45
+ <a
46
+ href="https://huggingface.co/spaces/huggingchat/chat-ui/discussions/454"
47
+ target="_blank"
48
+ class="hover:brightness-0 dark:hover:brightness-200"
49
+ ><CarbonInformation class="text-xs" /></a
50
+ >
51
+ {/if}
52
+ </div>
53
+ {#each $page.data.tools as tool}
54
+ {@const isChecked = $settings?.tools?.[tool.name] ?? tool.isOnByDefault}
55
+ <div class="flex items-center gap-1.5">
56
+ <input
57
+ type="checkbox"
58
+ id={tool.name}
59
+ checked={isChecked}
60
+ disabled={loading}
61
+ on:click={async () => {
62
+ await settings.instantSet({
63
+ tools: {
64
+ ...$settings.tools,
65
+ [tool.name]: !isChecked,
66
+ },
67
+ });
68
+ }}
69
+ />
70
+ <label class="cursor-pointer" for={tool.name}>{tool.displayName ?? tool.name} </label>
71
+ </div>
72
+ {/each}
73
+ </div>
74
+ </div>
75
+ </details>
src/lib/components/UploadBtn.svelte CHANGED
@@ -3,21 +3,26 @@
3
 
4
  export let classNames = "";
5
  export let files: File[];
6
- let filelist: FileList;
7
 
8
- $: if (filelist) {
9
- files = Array.from(filelist);
10
- }
 
 
 
 
 
 
11
  </script>
12
 
13
  <button
14
- class="btn relative h-8 rounded-lg border bg-white px-3 py-1 text-sm text-gray-500 shadow-sm transition-all hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 {classNames}"
15
  >
16
  <input
17
- bind:files={filelist}
18
  class="absolute w-full cursor-pointer opacity-0"
19
  type="file"
20
- accept="image/*"
 
21
  />
22
- <CarbonUpload class="mr-2 text-xs " /> Upload image
23
  </button>
 
3
 
4
  export let classNames = "";
5
  export let files: File[];
 
6
 
7
+ /**
8
+ * Due to a bug with Svelte, we cannot use bind:files with multiple
9
+ * So we use this workaround
10
+ **/
11
+ const onFileChange = (e: Event) => {
12
+ if (!e.target) return;
13
+ const target = e.target as HTMLInputElement;
14
+ files = [...files, ...(target.files ?? [])];
15
+ };
16
  </script>
17
 
18
  <button
19
+ class="btn relative h-8 rounded-lg border bg-white px-3 py-1 text-sm text-gray-500 shadow-sm hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 {classNames}"
20
  >
21
  <input
 
22
  class="absolute w-full cursor-pointer opacity-0"
23
  type="file"
24
+ on:change={onFileChange}
25
+ accept="*/*"
26
  />
27
+ <CarbonUpload class="mr-2 text-xxs" /> Upload file
28
  </button>
src/lib/components/chat/ChatMessage.svelte CHANGED
@@ -1,7 +1,7 @@
1
  <script lang="ts">
2
  import { marked, type MarkedOptions } from "marked";
3
  import markedKatex from "marked-katex-extension";
4
- import type { Message } from "$lib/types/Message";
5
  import { afterUpdate, createEventDispatcher, tick } from "svelte";
6
  import { deepestChild } from "$lib/utils/deepestChild";
7
  import { page } from "$app/stores";
@@ -16,14 +16,25 @@
16
  import CarbonPen from "~icons/carbon/pen";
17
  import CarbonChevronLeft from "~icons/carbon/chevron-left";
18
  import CarbonChevronRight from "~icons/carbon/chevron-right";
19
-
20
  import { PUBLIC_SEP_TOKEN } from "$lib/constants/publicSepToken";
21
  import type { Model } from "$lib/types/Model";
 
22
 
23
  import OpenWebSearchResults from "../OpenWebSearchResults.svelte";
24
- import type { WebSearchUpdate } from "$lib/types/MessageUpdate";
 
 
 
 
 
 
 
 
25
  import { base } from "$app/paths";
26
  import { useConvTreeStore } from "$lib/stores/convTree";
 
 
27
 
28
  function sanitizeMd(md: string) {
29
  let ret = md
@@ -58,6 +69,8 @@
58
 
59
  $: message = messages.find((m) => m.id === id) ?? ({} as Message);
60
 
 
 
61
  const dispatch = createEventDispatcher<{
62
  retry: { content?: string; id: Message["id"] };
63
  vote: { score: Message["score"]; id: Message["id"] };
@@ -129,19 +142,33 @@
129
  }
130
 
131
  $: searchUpdates = (message.updates?.filter(({ type }) => type === "webSearch") ??
132
- []) as WebSearchUpdate[];
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
- $: downloadLink =
135
- message.from === "user" ? `${$page.url.pathname}/message/${message.id}/prompt` : undefined;
136
 
137
  let webSearchIsDone = true;
138
 
139
- $: webSearchIsDone =
140
- searchUpdates.length > 0 && searchUpdates[searchUpdates.length - 1].messageType === "sources";
 
141
 
142
- $: webSearchSources =
143
- searchUpdates &&
144
- searchUpdates?.filter(({ messageType }) => messageType === "sources")?.[0]?.sources;
 
145
 
146
  $: if (isCopied) {
147
  setTimeout(() => {
@@ -177,8 +204,32 @@
177
  const convTreeStore = useConvTreeStore();
178
 
179
  $: if (message.children?.length === 0) $convTreeStore.leaf = message.id;
 
 
 
 
180
  </script>
181
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  {#if message.from === "assistant"}
183
  <div
184
  class="group relative -mb-6 flex items-start justify-start gap-4 pb-4 leading-relaxed"
@@ -202,6 +253,29 @@
202
  <div
203
  class="relative min-h-[calc(2rem+theme(spacing[3.5])*2)] min-w-[60px] break-words rounded-2xl border border-gray-100 bg-gradient-to-br from-gray-50 px-5 py-3.5 text-gray-600 prose-pre:my-2 dark:border-gray-800 dark:from-gray-800/40 dark:text-gray-300"
204
  >
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  {#if searchUpdates && searchUpdates.length > 0}
206
  <OpenWebSearchResults
207
  classNames={tokens.length ? "mb-3.5" : ""}
@@ -209,6 +283,72 @@
209
  />
210
  {/if}
211
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  <div
213
  class="prose max-w-none max-sm:prose-sm dark:prose-invert prose-headings:font-semibold prose-h1:text-lg prose-h2:text-base prose-h3:text-base prose-pre:bg-gray-800 dark:prose-pre:bg-gray-900"
214
  bind:this={contentEl}
@@ -244,7 +384,7 @@
244
  </div>
245
  {/if}
246
  </div>
247
- {#if !loading && message.content}
248
  <div
249
  class="absolute bottom-1 right-0 -mb-4 flex max-md:transition-all md:bottom-0 md:group-hover:visible md:group-hover:opacity-100
250
  {message.score ? 'visible opacity-100' : 'invisible max-md:-translate-y-4 max-md:opacity-0'}
@@ -304,24 +444,16 @@
304
  on:click={() => (isTapped = !isTapped)}
305
  on:keydown={() => (isTapped = !isTapped)}
306
  >
307
- <div class="flex w-full flex-col">
308
- {#if message.files && message.files.length > 0}
309
- <div class="mx-auto grid w-fit grid-cols-2 gap-5 px-5">
310
  {#each message.files as file}
311
- <!-- handle the case where this is a hash that points to an image in the db -->
312
- {#if file.type === "hash"}
313
- <img
314
- src={$page.url.pathname + "/output/" + file.value}
315
- alt="input from user"
316
- class="my-2 aspect-auto max-h-48 rounded-lg shadow-lg"
317
- />
318
  {:else}
319
- <!-- handle the case where this is a base64 encoded image -->
320
- <img
321
- src={`data:${file.mime};base64,${file.value}`}
322
- alt="input from user"
323
- class="my-2 aspect-auto max-h-48 rounded-lg shadow-lg"
324
- />
325
  {/if}
326
  {/each}
327
  </div>
@@ -458,3 +590,20 @@
458
  </svelte:fragment>
459
  </svelte:self>
460
  {/if}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  <script lang="ts">
2
  import { marked, type MarkedOptions } from "marked";
3
  import markedKatex from "marked-katex-extension";
4
+ import type { Message, MessageFile } from "$lib/types/Message";
5
  import { afterUpdate, createEventDispatcher, tick } from "svelte";
6
  import { deepestChild } from "$lib/utils/deepestChild";
7
  import { page } from "$app/stores";
 
16
  import CarbonPen from "~icons/carbon/pen";
17
  import CarbonChevronLeft from "~icons/carbon/chevron-left";
18
  import CarbonChevronRight from "~icons/carbon/chevron-right";
19
+ import CarbonTools from "~icons/carbon/tools";
20
  import { PUBLIC_SEP_TOKEN } from "$lib/constants/publicSepToken";
21
  import type { Model } from "$lib/types/Model";
22
+ import UploadedFile from "./UploadedFile.svelte";
23
 
24
  import OpenWebSearchResults from "../OpenWebSearchResults.svelte";
25
+ import {
26
+ MessageToolUpdateType,
27
+ MessageWebSearchUpdateType,
28
+ type MessageToolUpdate,
29
+ type MessageWebSearchSourcesUpdate,
30
+ type MessageWebSearchUpdate,
31
+ } from "$lib/types/MessageUpdate";
32
+ import { isMessageToolCallUpdate, isMessageToolResultUpdate } from "$lib/utils/messageUpdates";
33
+ import type { ToolFront } from "$lib/types/Tool";
34
  import { base } from "$app/paths";
35
  import { useConvTreeStore } from "$lib/stores/convTree";
36
+ import Modal from "../Modal.svelte";
37
+ import { toolHasName } from "$lib/utils/tools";
38
 
39
  function sanitizeMd(md: string) {
40
  let ret = md
 
69
 
70
  $: message = messages.find((m) => m.id === id) ?? ({} as Message);
71
 
72
+ $: urlNotTrailing = $page.url.pathname.replace(/\/$/, "");
73
+
74
  const dispatch = createEventDispatcher<{
75
  retry: { content?: string; id: Message["id"] };
76
  vote: { score: Message["score"]; id: Message["id"] };
 
142
  }
143
 
144
  $: searchUpdates = (message.updates?.filter(({ type }) => type === "webSearch") ??
145
+ []) as MessageWebSearchUpdate[];
146
+
147
+ // filter all updates with type === "tool" then group them by uuid field
148
+
149
+ $: toolUpdates = message.updates
150
+ ?.filter(({ type }) => type === "tool")
151
+ .reduce((acc, update) => {
152
+ if (update.type !== "tool") {
153
+ return acc;
154
+ }
155
+ acc[update.uuid] = acc[update.uuid] ?? [];
156
+ acc[update.uuid].push(update);
157
+ return acc;
158
+ }, {} as Record<string, MessageToolUpdate[]>);
159
 
160
+ $: downloadLink = urlNotTrailing + `/message/${message.id}/prompt`;
 
161
 
162
  let webSearchIsDone = true;
163
 
164
+ $: webSearchIsDone = searchUpdates.some(
165
+ (update) => update.subtype === MessageWebSearchUpdateType.Finished
166
+ );
167
 
168
+ $: webSearchSources = searchUpdates?.find(
169
+ (update): update is MessageWebSearchSourcesUpdate =>
170
+ update.subtype === MessageWebSearchUpdateType.Sources
171
+ )?.sources;
172
 
173
  $: if (isCopied) {
174
  setTimeout(() => {
 
204
  const convTreeStore = useConvTreeStore();
205
 
206
  $: if (message.children?.length === 0) $convTreeStore.leaf = message.id;
207
+
208
+ $: modalImageToShow = null as MessageFile | null;
209
+
210
+ const availableTools: ToolFront[] = $page.data.tools;
211
  </script>
212
 
213
+ {#if modalImageToShow}
214
+ <!-- show the image file full screen, click outside to exit -->
215
+ <Modal width="sm:max-w-[500px]" on:close={() => (modalImageToShow = null)}>
216
+ {#if modalImageToShow.type === "hash"}
217
+ <img
218
+ src={urlNotTrailing + "/output/" + modalImageToShow.value}
219
+ alt="input from user"
220
+ class="aspect-auto"
221
+ />
222
+ {:else}
223
+ <!-- handle the case where this is a base64 encoded image -->
224
+ <img
225
+ src={`data:${modalImageToShow.mime};base64,${modalImageToShow.value}`}
226
+ alt="input from user"
227
+ class="aspect-auto"
228
+ />
229
+ {/if}
230
+ </Modal>
231
+ {/if}
232
+
233
  {#if message.from === "assistant"}
234
  <div
235
  class="group relative -mb-6 flex items-start justify-start gap-4 pb-4 leading-relaxed"
 
253
  <div
254
  class="relative min-h-[calc(2rem+theme(spacing[3.5])*2)] min-w-[60px] break-words rounded-2xl border border-gray-100 bg-gradient-to-br from-gray-50 px-5 py-3.5 text-gray-600 prose-pre:my-2 dark:border-gray-800 dark:from-gray-800/40 dark:text-gray-300"
255
  >
256
+ {#if message.files?.length}
257
+ <div class="flex h-fit flex-wrap gap-x-5 gap-y-2">
258
+ {#each message.files as file}
259
+ <!-- handle the case where this is a hash that points to an image in the db, hash is always 64 char long -->
260
+ <button on:click={() => (modalImageToShow = file)}>
261
+ {#if file.type === "hash"}
262
+ <img
263
+ src={urlNotTrailing + "/output/" + file.value}
264
+ alt="output from assistant"
265
+ class="my-2 aspect-auto max-h-48 cursor-pointer rounded-lg shadow-lg"
266
+ />
267
+ {:else}
268
+ <!-- handle the case where this is a base64 encoded image -->
269
+ <img
270
+ src={`data:${file.mime};base64,${file.value}`}
271
+ alt="output from assistant"
272
+ class="my-2 aspect-auto max-h-48 cursor-pointer rounded-lg shadow-lg"
273
+ />
274
+ {/if}
275
+ </button>
276
+ {/each}
277
+ </div>
278
+ {/if}
279
  {#if searchUpdates && searchUpdates.length > 0}
280
  <OpenWebSearchResults
281
  classNames={tokens.length ? "mb-3.5" : ""}
 
283
  />
284
  {/if}
285
 
286
+ {#if toolUpdates}
287
+ {#each Object.values(toolUpdates) as tool}
288
+ {#if tool.length}
289
+ {@const toolName = tool.find(isMessageToolCallUpdate)?.call.name}
290
+ {@const toolDone = tool.some(isMessageToolResultUpdate)}
291
+ {#if toolName && toolName !== "websearch"}
292
+ <details
293
+ 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
294
+ 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"
295
+ >
296
+ <summary
297
+ class="flex select-none list-none items-center gap-1.5 py-1 group-open/tool:text-purple-700 group-open/tool:dark:text-purple-300"
298
+ >
299
+ <div
300
+ class="relative grid size-[22px] place-items-center rounded bg-purple-600/10 dark:bg-purple-600/20"
301
+ >
302
+ <svg
303
+ class="absolute inset-0 text-purple-500/40 transition-opacity"
304
+ class:invisible={toolDone}
305
+ width="22"
306
+ height="22"
307
+ viewBox="0 0 38 38"
308
+ fill="none"
309
+ xmlns="http://www.w3.org/2000/svg"
310
+ >
311
+ <path
312
+ class="loading-path"
313
+ d="M8 2.5H30C30 2.5 35.5 2.5 35.5 8V30C35.5 30 35.5 35.5 30 35.5H8C8 35.5 2.5 35.5 2.5 30V8C2.5 8 2.5 2.5 8 2.5Z"
314
+ stroke="currentColor"
315
+ stroke-width="1"
316
+ stroke-linecap="round"
317
+ id="shape"
318
+ />
319
+ </svg>
320
+ <CarbonTools class="text-xs text-purple-700 dark:text-purple-500" />
321
+ </div>
322
+
323
+ <span>
324
+ {toolDone ? "Called" : "Calling"} tool
325
+ <span class="font-semibold"
326
+ >{availableTools.find((el) => toolHasName(toolName, el))?.displayName}</span
327
+ >
328
+ </span>
329
+ </summary>
330
+ {#each tool as toolUpdate}
331
+ {#if toolUpdate.subtype === MessageToolUpdateType.Call}
332
+ <div class="mt-1 flex items-center gap-2 opacity-80">
333
+ <h3 class="text-sm">Parameters</h3>
334
+ <div class="h-px flex-1 bg-gradient-to-r from-gray-500/20" />
335
+ </div>
336
+ <ul class="py-1 text-sm">
337
+ {#each Object.entries(toolUpdate.call.parameters ?? {}) as [k, v]}
338
+ <li>
339
+ <span class="font-semibold">{k}</span>:
340
+ <span>{v}</span>
341
+ </li>
342
+ {/each}
343
+ </ul>
344
+ {/if}
345
+ {/each}
346
+ </details>
347
+ {/if}
348
+ {/if}
349
+ {/each}
350
+ {/if}
351
+
352
  <div
353
  class="prose max-w-none max-sm:prose-sm dark:prose-invert prose-headings:font-semibold prose-h1:text-lg prose-h2:text-base prose-h3:text-base prose-pre:bg-gray-800 dark:prose-pre:bg-gray-900"
354
  bind:this={contentEl}
 
384
  </div>
385
  {/if}
386
  </div>
387
+ {#if !loading && (message.content || toolUpdates)}
388
  <div
389
  class="absolute bottom-1 right-0 -mb-4 flex max-md:transition-all md:bottom-0 md:group-hover:visible md:group-hover:opacity-100
390
  {message.score ? 'visible opacity-100' : 'invisible max-md:-translate-y-4 max-md:opacity-0'}
 
444
  on:click={() => (isTapped = !isTapped)}
445
  on:keydown={() => (isTapped = !isTapped)}
446
  >
447
+ <div class="flex w-full flex-col gap-2">
448
+ {#if message.files?.length}
449
+ <div class="flex w-fit gap-4 px-5">
450
  {#each message.files as file}
451
+ {#if file.mime.startsWith("image/")}
452
+ <button on:click={() => (modalImageToShow = file)}>
453
+ <UploadedFile {file} canClose={false} />
454
+ </button>
 
 
 
455
  {:else}
456
+ <UploadedFile {file} canClose={false} />
 
 
 
 
 
457
  {/if}
458
  {/each}
459
  </div>
 
590
  </svelte:fragment>
591
  </svelte:self>
592
  {/if}
593
+
594
+ <style>
595
+ details summary::-webkit-details-marker {
596
+ display: none;
597
+ }
598
+
599
+ .loading-path {
600
+ stroke-dasharray: 61.45;
601
+ animation: loading 2s linear infinite;
602
+ }
603
+
604
+ @keyframes loading {
605
+ to {
606
+ stroke-dashoffset: 122.9;
607
+ }
608
+ }
609
+ </style>
src/lib/components/chat/ChatWindow.svelte CHANGED
@@ -1,11 +1,10 @@
1
  <script lang="ts">
2
- import type { Message } from "$lib/types/Message";
3
  import { createEventDispatcher, onDestroy, tick } from "svelte";
4
 
5
  import CarbonSendAltFilled from "~icons/carbon/send-alt-filled";
6
  import CarbonExport from "~icons/carbon/export";
7
  import CarbonStopFilledAlt from "~icons/carbon/stop-filled-alt";
8
- import CarbonClose from "~icons/carbon/close";
9
  import CarbonCheckmark from "~icons/carbon/checkmark";
10
  import CarbonCaretDown from "~icons/carbon/caret-down";
11
 
@@ -15,6 +14,7 @@
15
  import StopGeneratingBtn from "../StopGeneratingBtn.svelte";
16
  import type { Model } from "$lib/types/Model";
17
  import WebSearchToggle from "../WebSearchToggle.svelte";
 
18
  import LoginModal from "../LoginModal.svelte";
19
  import { page } from "$app/stores";
20
  import FileDropzone from "./FileDropzone.svelte";
@@ -32,6 +32,7 @@
32
  import SystemPromptModal from "../SystemPromptModal.svelte";
33
  import ChatIntroduction from "./ChatIntroduction.svelte";
34
  import { useConvTreeStore } from "$lib/stores/convTree";
 
35
 
36
  export let messages: Message[] = [];
37
  export let loading = false;
@@ -92,8 +93,8 @@
92
  (lastMessage.from === "user" ||
93
  lastMessage.updates?.findIndex((u) => u.type === "status" && u.status === "error") !== -1);
94
 
95
- $: sources = files?.map((file) =>
96
- file2base64(file).then((value) => ({ type: "base64", value, mime: file.type }))
97
  );
98
 
99
  function onShare() {
@@ -139,7 +140,9 @@
139
  use:snapScrollToBottom={messages.length ? [...messages] : false}
140
  bind:this={chatContainer}
141
  >
142
- <div class="mx-auto flex h-full max-w-3xl flex-col gap-6 px-5 pt-6 sm:gap-8 xl:max-w-4xl">
 
 
143
  {#if $page.data?.assistant && !!messages.length}
144
  <a
145
  class="mx-auto flex items-center gap-1.5 rounded-full border border-gray-100 bg-gray-50 py-1 pl-1 pr-3 text-sm text-gray-800 hover:bg-gray-100 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700"
@@ -167,7 +170,7 @@
167
  {/if}
168
 
169
  {#if messages.length > 0}
170
- <div class="flex h-max flex-col gap-6 pb-52">
171
  <ChatMessage
172
  {loading}
173
  {messages}
@@ -235,22 +238,12 @@
235
  <div class="flex flex-row flex-wrap justify-center gap-2.5 max-md:pb-3">
236
  {#each sources as source, index}
237
  {#await source then src}
238
- <div class="relative h-16 w-16 overflow-hidden rounded-lg shadow-lg">
239
- <img
240
- src={`data:${src.mime};base64,${src.value}`}
241
- alt="input content"
242
- class="h-full w-full rounded-lg bg-gray-400 object-cover dark:bg-gray-900"
243
- />
244
- <!-- add a button on top that deletes this image from sources -->
245
- <button
246
- class="absolute left-1 top-1"
247
- on:click={() => {
248
- files = files.filter((_, i) => i !== index);
249
- }}
250
- >
251
- <CarbonClose class="text-md font-black text-gray-300 hover:text-gray-100" />
252
- </button>
253
- </div>
254
  {/await}
255
  {/each}
256
  </div>
@@ -258,8 +251,12 @@
258
 
259
  <div class="w-full">
260
  <div class="flex w-full pb-3">
261
- {#if $page.data.settings?.searchEnabled && !assistant}
262
- <WebSearchToggle />
 
 
 
 
263
  {/if}
264
  {#if loading}
265
  <StopGeneratingBtn classNames="ml-auto" on:click={() => dispatch("stop")} />
@@ -276,7 +273,7 @@
276
  />
277
  {:else}
278
  <div class="ml-auto gap-2">
279
- {#if currentModel.multimodal}
280
  <UploadBtn bind:files classNames="ml-auto" />
281
  {/if}
282
  {#if messages && lastMessage && lastMessage.interrupted && !isReadOnly}
 
1
  <script lang="ts">
2
+ import type { Message, MessageFile } from "$lib/types/Message";
3
  import { createEventDispatcher, onDestroy, tick } from "svelte";
4
 
5
  import CarbonSendAltFilled from "~icons/carbon/send-alt-filled";
6
  import CarbonExport from "~icons/carbon/export";
7
  import CarbonStopFilledAlt from "~icons/carbon/stop-filled-alt";
 
8
  import CarbonCheckmark from "~icons/carbon/checkmark";
9
  import CarbonCaretDown from "~icons/carbon/caret-down";
10
 
 
14
  import StopGeneratingBtn from "../StopGeneratingBtn.svelte";
15
  import type { Model } from "$lib/types/Model";
16
  import WebSearchToggle from "../WebSearchToggle.svelte";
17
+ import ToolsMenu from "../ToolsMenu.svelte";
18
  import LoginModal from "../LoginModal.svelte";
19
  import { page } from "$app/stores";
20
  import FileDropzone from "./FileDropzone.svelte";
 
32
  import SystemPromptModal from "../SystemPromptModal.svelte";
33
  import ChatIntroduction from "./ChatIntroduction.svelte";
34
  import { useConvTreeStore } from "$lib/stores/convTree";
35
+ import UploadedFile from "./UploadedFile.svelte";
36
 
37
  export let messages: Message[] = [];
38
  export let loading = false;
 
93
  (lastMessage.from === "user" ||
94
  lastMessage.updates?.findIndex((u) => u.type === "status" && u.status === "error") !== -1);
95
 
96
+ $: sources = files?.map<Promise<MessageFile>>((file) =>
97
+ file2base64(file).then((value) => ({ type: "base64", value, mime: file.type, name: file.name }))
98
  );
99
 
100
  function onShare() {
 
140
  use:snapScrollToBottom={messages.length ? [...messages] : false}
141
  bind:this={chatContainer}
142
  >
143
+ <div
144
+ class="mx-auto flex h-full max-w-3xl flex-col gap-6 px-5 pt-6 sm:gap-8 xl:max-w-4xl xl:pt-10"
145
+ >
146
  {#if $page.data?.assistant && !!messages.length}
147
  <a
148
  class="mx-auto flex items-center gap-1.5 rounded-full border border-gray-100 bg-gray-50 py-1 pl-1 pr-3 text-sm text-gray-800 hover:bg-gray-100 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700"
 
170
  {/if}
171
 
172
  {#if messages.length > 0}
173
+ <div class="flex h-max flex-col gap-6 pb-52 2xl:gap-7">
174
  <ChatMessage
175
  {loading}
176
  {messages}
 
238
  <div class="flex flex-row flex-wrap justify-center gap-2.5 max-md:pb-3">
239
  {#each sources as source, index}
240
  {#await source then src}
241
+ <UploadedFile
242
+ file={src}
243
+ on:close={() => {
244
+ files = files.filter((_, i) => i !== index);
245
+ }}
246
+ />
 
 
 
 
 
 
 
 
 
 
247
  {/await}
248
  {/each}
249
  </div>
 
251
 
252
  <div class="w-full">
253
  <div class="flex w-full pb-3">
254
+ {#if !assistant}
255
+ {#if currentModel.tools}
256
+ <ToolsMenu {loading} />
257
+ {:else if $page.data.settings?.searchEnabled}
258
+ <WebSearchToggle />
259
+ {/if}
260
  {/if}
261
  {#if loading}
262
  <StopGeneratingBtn classNames="ml-auto" on:click={() => dispatch("stop")} />
 
273
  />
274
  {:else}
275
  <div class="ml-auto gap-2">
276
+ {#if currentModel.multimodal || currentModel.tools}
277
  <UploadBtn bind:files classNames="ml-auto" />
278
  {/if}
279
  {#if messages && lastMessage && lastMessage.interrupted && !isReadOnly}
src/lib/components/chat/UploadedFile.svelte ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { createEventDispatcher } from "svelte";
3
+ import { page } from "$app/stores";
4
+ import type { MessageFile } from "$lib/types/Message";
5
+ import CarbonClose from "~icons/carbon/close";
6
+ import CarbonDocumentBlank from "~icons/carbon/document-blank";
7
+
8
+ export let file: MessageFile;
9
+ export let canClose = true;
10
+ const dispatch = createEventDispatcher<{ close: void }>();
11
+ </script>
12
+
13
+ <div
14
+ class="group relative flex items-center rounded-xl shadow-sm"
15
+ class:w-24={file.mime.startsWith("image/")}
16
+ class:w-72={!file.mime.startsWith("image/")}
17
+ >
18
+ {#if file.mime.startsWith("image/")}
19
+ <div class="size-24 overflow-hidden rounded-xl">
20
+ <img
21
+ src={file.type === "base64"
22
+ ? `data:${file.mime};base64,${file.value}`
23
+ : $page.url.pathname + "/output/" + file.value}
24
+ alt={file.name}
25
+ class="h-full w-full bg-gray-200 object-cover dark:bg-gray-800"
26
+ />
27
+ </div>
28
+ {:else}
29
+ <div
30
+ class="flex h-14 w-72 items-center gap-2 overflow-hidden rounded-xl border border-gray-200 bg-white p-2 dark:border-gray-800 dark:bg-gray-900"
31
+ >
32
+ <div
33
+ class="grid size-10 flex-none place-items-center rounded-lg bg-gray-100 dark:bg-gray-800"
34
+ >
35
+ <CarbonDocumentBlank class="text-base text-gray-700 dark:text-gray-300" />
36
+ </div>
37
+ <dl class="flex flex-col truncate leading-tight">
38
+ <dd class="text-sm">
39
+ {file.name}
40
+ </dd>
41
+ <dt class="text-xs text-gray-400">{file.mime.split("/")[1].toUpperCase()}</dt>
42
+ </dl>
43
+ </div>
44
+ {/if}
45
+ <!-- add a button on top that removes the image -->
46
+ {#if canClose}
47
+ <button
48
+ class="invisible absolute -right-2 -top-2 grid size-6 place-items-center rounded-full border bg-black group-hover:visible dark:border-gray-700"
49
+ on:click={() => dispatch("close")}
50
+ >
51
+ <CarbonClose class=" text-xs text-white" />
52
+ </button>
53
+ {/if}
54
+ </div>
src/lib/components/icons/IconTool.svelte ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = "";
3
+ </script>
4
+
5
+ <svg
6
+ xmlns="http://www.w3.org/2000/svg"
7
+ class={classNames}
8
+ width="1em"
9
+ height="1em"
10
+ fill="none"
11
+ viewBox="0 0 15 15"
12
+ ><path
13
+ fill="currentColor"
14
+ d="M8.33 6.56c0 .11-.04.22-.1.31a.55.55 0 0 1-.26.2l-.71.23c-.2.07-.38.18-.53.33-.15.15-.26.33-.33.53l-.24.7a.55.55 0 0 1-.83.26.55.55 0 0 1-.19-.27l-.23-.7a1.37 1.37 0 0 0-.85-.85l-.7-.24a.52.52 0 0 1-.27-.2.56.56 0 0 1 .27-.82l.7-.23a1.4 1.4 0 0 0 .86-.86l.23-.7c.03-.1.1-.19.18-.26a.55.55 0 0 1 .63-.02c.1.06.17.15.2.26l.24.72a1.4 1.4 0 0 0 .86.86l.7.24c.11.04.2.1.26.2.07.09.1.2.1.31ZM13.94 10.14c0 .12-.03.24-.1.34a.55.55 0 0 1-.3.21l-.96.32a2.01 2.01 0 0 0-1.27 1.27l-.33.96a.55.55 0 0 1-.2.28c-.1.07-.23.11-.35.11a.56.56 0 0 1-.57-.4l-.32-.96c-.1-.3-.26-.57-.48-.79-.22-.21-.49-.38-.78-.48l-.97-.32a.62.62 0 0 1-.28-.2.61.61 0 0 1-.02-.7c.07-.1.18-.18.3-.22l.96-.32a1.99 1.99 0 0 0 1.29-1.28l.32-.95a.56.56 0 0 1 .53-.4c.12 0 .24.02.34.09.1.07.18.17.23.28l.32.98a1.99 1.99 0 0 0 1.29 1.28l.95.34c.12.04.22.11.29.21.07.1.11.22.11.35ZM11.48 3.84c.06-.09.1-.19.1-.29a.5.5 0 0 0-.1-.29.55.55 0 0 0-.23-.16l-.35-.12a.55.55 0 0 1-.18-.11.55.55 0 0 1-.11-.19l-.12-.36a.54.54 0 0 0-.18-.23A.55.55 0 0 0 10 2c-.1.01-.2.05-.27.1a.55.55 0 0 0-.16.23l-.12.35a.55.55 0 0 1-.11.19.55.55 0 0 1-.19.1l-.34.12a.5.5 0 0 0-.24.17.55.55 0 0 0-.1.29c0 .1.04.2.1.28.06.08.14.14.23.18l.35.11c.07.02.13.06.18.11.05.05.1.11.11.18l.12.35c.04.1.1.19.18.25.09.05.18.08.28.08.1 0 .2-.03.29-.1a.55.55 0 0 0 .16-.22l.13-.35a.47.47 0 0 1 .29-.3l.35-.11c.1-.03.17-.1.23-.17Z"
15
+ /></svg
16
+ >
src/lib/migrations/routines/03-add-tools-in-settings.ts ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Migration } from ".";
2
+ import { collections } from "$lib/server/database";
3
+ import { ObjectId } from "mongodb";
4
+ import { logger } from "$lib/server/logger";
5
+
6
+ const addToolsToSettings: Migration = {
7
+ _id: new ObjectId("5c9c4c4c4c4c4c4c4c4c4c4c"),
8
+ name: "Add empty 'tools' record in settings",
9
+ up: async () => {
10
+ const { settings } = collections;
11
+
12
+ // Find all assistants whose modelId is not in modelIds, and update it to use defaultModelId
13
+ await settings.updateMany(
14
+ {
15
+ tools: { $exists: false },
16
+ },
17
+ { $set: { tools: {} } }
18
+ );
19
+
20
+ settings
21
+ .createIndex({ tools: 1 })
22
+ .catch((e) => logger.error("Error creating index during tools migration", e));
23
+
24
+ return true;
25
+ },
26
+ runEveryTime: false,
27
+ };
28
+
29
+ export default addToolsToSettings;
src/lib/migrations/routines/04-update-message-updates.ts ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Migration } from ".";
2
+ import { collections } from "$lib/server/database";
3
+ import { ObjectId, type WithId } from "mongodb";
4
+ import type { Conversation } from "$lib/types/Conversation";
5
+ import type { WebSearchSource } from "$lib/types/WebSearch";
6
+ import {
7
+ MessageUpdateStatus,
8
+ MessageUpdateType,
9
+ MessageWebSearchUpdateType,
10
+ type MessageUpdate,
11
+ type MessageWebSearchFinishedUpdate,
12
+ } from "$lib/types/MessageUpdate";
13
+ import type { Message } from "$lib/types/Message";
14
+ import { isMessageWebSearchSourcesUpdate } from "$lib/utils/messageUpdates";
15
+
16
+ // -----------
17
+ // Copy of the previous message update types
18
+ export type FinalAnswer = {
19
+ type: "finalAnswer";
20
+ text: string;
21
+ };
22
+
23
+ export type TextStreamUpdate = {
24
+ type: "stream";
25
+ token: string;
26
+ };
27
+
28
+ type WebSearchUpdate = {
29
+ type: "webSearch";
30
+ messageType: "update" | "error" | "sources";
31
+ message: string;
32
+ args?: string[];
33
+ sources?: WebSearchSource[];
34
+ };
35
+
36
+ type StatusUpdate = {
37
+ type: "status";
38
+ status: "started" | "pending" | "finished" | "error" | "title";
39
+ message?: string;
40
+ };
41
+
42
+ type ErrorUpdate = {
43
+ type: "error";
44
+ message: string;
45
+ name: string;
46
+ };
47
+
48
+ type FileUpdate = {
49
+ type: "file";
50
+ sha: string;
51
+ };
52
+
53
+ type OldMessageUpdate =
54
+ | FinalAnswer
55
+ | TextStreamUpdate
56
+ | WebSearchUpdate
57
+ | StatusUpdate
58
+ | ErrorUpdate
59
+ | FileUpdate;
60
+
61
+ /** Converts the old message update to the new schema */
62
+ function convertMessageUpdate(message: Message, update: OldMessageUpdate): MessageUpdate | null {
63
+ try {
64
+ // Text and files
65
+ if (update.type === "finalAnswer") {
66
+ return {
67
+ type: MessageUpdateType.FinalAnswer,
68
+ text: update.text,
69
+ interrupted: message.interrupted ?? false,
70
+ };
71
+ } else if (update.type === "stream") {
72
+ return {
73
+ type: MessageUpdateType.Stream,
74
+ token: update.token,
75
+ };
76
+ } else if (update.type === "file") {
77
+ return {
78
+ type: MessageUpdateType.File,
79
+ name: "Unknown",
80
+ sha: update.sha,
81
+ // assume jpeg but could be any image. should be harmless
82
+ mime: "image/jpeg",
83
+ };
84
+ }
85
+
86
+ // Status
87
+ else if (update.type === "status") {
88
+ if (update.status === "title") {
89
+ return {
90
+ type: MessageUpdateType.Title,
91
+ title: update.message ?? "New Chat",
92
+ };
93
+ }
94
+ if (update.status === "pending") return null;
95
+
96
+ const status =
97
+ update.status === "started"
98
+ ? MessageUpdateStatus.Started
99
+ : update.status === "finished"
100
+ ? MessageUpdateStatus.Finished
101
+ : MessageUpdateStatus.Error;
102
+ return {
103
+ type: MessageUpdateType.Status,
104
+ status,
105
+ message: update.message,
106
+ };
107
+ } else if (update.type === "error") {
108
+ // Treat it as an error status update
109
+ return {
110
+ type: MessageUpdateType.Status,
111
+ status: MessageUpdateStatus.Error,
112
+ message: update.message,
113
+ };
114
+ }
115
+
116
+ // Web Search
117
+ else if (update.type === "webSearch") {
118
+ if (update.messageType === "update") {
119
+ return {
120
+ type: MessageUpdateType.WebSearch,
121
+ subtype: MessageWebSearchUpdateType.Update,
122
+ message: update.message,
123
+ args: update.args,
124
+ };
125
+ } else if (update.messageType === "error") {
126
+ return {
127
+ type: MessageUpdateType.WebSearch,
128
+ subtype: MessageWebSearchUpdateType.Error,
129
+ message: update.message,
130
+ args: update.args,
131
+ };
132
+ } else if (update.messageType === "sources") {
133
+ return {
134
+ type: MessageUpdateType.WebSearch,
135
+ subtype: MessageWebSearchUpdateType.Sources,
136
+ message: update.message,
137
+ sources: update.sources ?? [],
138
+ };
139
+ }
140
+ }
141
+ console.warn("Unknown message update during migration:", update);
142
+ return null;
143
+ } catch (error) {
144
+ console.error("Error converting message update during migration. Skipping it... Error:", error);
145
+ return null;
146
+ }
147
+ }
148
+
149
+ const updateMessageUpdates: Migration = {
150
+ _id: new ObjectId("5f9f4f4f4f4f4f4f4f4f4f4f"),
151
+ name: "Convert message updates to the new schema",
152
+ up: async () => {
153
+ const allConversations = collections.conversations.find({}, { projection: { messages: 1 } });
154
+
155
+ let conversation: WithId<Pick<Conversation, "messages">> | null = null;
156
+ while ((conversation = await allConversations.tryNext())) {
157
+ const messages = conversation.messages.map((message) => {
158
+ // Convert all of the existing updates to the new schema
159
+ const updates = message.updates
160
+ ?.map((update) => convertMessageUpdate(message, update as OldMessageUpdate))
161
+ .filter((update): update is MessageUpdate => Boolean(update));
162
+
163
+ // Add the new web search finished update if the sources update exists and webSearch is defined
164
+ const webSearchSourcesUpdateIndex = updates?.findIndex(isMessageWebSearchSourcesUpdate);
165
+ if (
166
+ message.webSearch &&
167
+ updates &&
168
+ webSearchSourcesUpdateIndex &&
169
+ webSearchSourcesUpdateIndex !== -1
170
+ ) {
171
+ const webSearchFinishedUpdate: MessageWebSearchFinishedUpdate = {
172
+ type: MessageUpdateType.WebSearch,
173
+ subtype: MessageWebSearchUpdateType.Finished,
174
+ webSearch: message.webSearch,
175
+ };
176
+ updates.splice(webSearchSourcesUpdateIndex + 1, 0, webSearchFinishedUpdate);
177
+ }
178
+ return { ...message, updates };
179
+ });
180
+
181
+ // Set the new messages array
182
+ await collections.conversations.updateOne({ _id: conversation._id }, { $set: { messages } });
183
+ }
184
+
185
+ return true;
186
+ },
187
+ runEveryTime: false,
188
+ };
189
+
190
+ export default updateMessageUpdates;
src/lib/migrations/routines/05-update-message-files.ts ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ObjectId, type WithId } from "mongodb";
2
+ import { collections } from "$lib/server/database";
3
+
4
+ import type { Migration } from ".";
5
+ import type { Conversation } from "$lib/types/Conversation";
6
+ import type { MessageFile } from "$lib/types/Message";
7
+
8
+ const updateMessageFiles: Migration = {
9
+ _id: new ObjectId("5f9f5f5f5f5f5f5f5f5f5f5f"),
10
+ name: "Convert message files to the new schema",
11
+ up: async () => {
12
+ const allConversations = collections.conversations.find({}, { projection: { messages: 1 } });
13
+
14
+ let conversation: WithId<Pick<Conversation, "messages">> | null = null;
15
+ while ((conversation = await allConversations.tryNext())) {
16
+ const messages = conversation.messages.map((message) => {
17
+ const files = (message.files as string[] | undefined)?.map<MessageFile>((file) => {
18
+ // File is already in the new format
19
+ if (typeof file !== "string") return file;
20
+
21
+ // File was a hash pointing to a file in the bucket
22
+ if (file.length === 64) {
23
+ return {
24
+ type: "hash",
25
+ name: "unknown.jpg",
26
+ value: file,
27
+ mime: "image/jpeg",
28
+ };
29
+ }
30
+ // File was a base64 string
31
+ else {
32
+ return {
33
+ type: "base64",
34
+ name: "unknown.jpg",
35
+ value: file,
36
+ mime: "image/jpeg",
37
+ };
38
+ }
39
+ });
40
+
41
+ return {
42
+ ...message,
43
+ files,
44
+ };
45
+ });
46
+
47
+ // Set the new messages array
48
+ await collections.conversations.updateOne({ _id: conversation._id }, { $set: { messages } });
49
+ }
50
+
51
+ return true;
52
+ },
53
+ runEveryTime: false,
54
+ };
55
+
56
+ export default updateMessageFiles;
src/lib/migrations/routines/index.ts CHANGED
@@ -3,6 +3,9 @@ import type { ObjectId } from "mongodb";
3
  import updateSearchAssistant from "./01-update-search-assistants";
4
  import updateAssistantsModels from "./02-update-assistants-models";
5
  import type { Database } from "$lib/server/database";
 
 
 
6
 
7
  export interface Migration {
8
  _id: ObjectId;
@@ -14,4 +17,10 @@ export interface Migration {
14
  runEveryTime?: boolean;
15
  }
16
 
17
- export const migrations: Migration[] = [updateSearchAssistant, updateAssistantsModels];
 
 
 
 
 
 
 
3
  import updateSearchAssistant from "./01-update-search-assistants";
4
  import updateAssistantsModels from "./02-update-assistants-models";
5
  import type { Database } from "$lib/server/database";
6
+ import addToolsToSettings from "./03-add-tools-in-settings";
7
+ import updateMessageUpdates from "./04-update-message-updates";
8
+ import updateMessageFiles from "./05-update-message-files";
9
 
10
  export interface Migration {
11
  _id: ObjectId;
 
17
  runEveryTime?: boolean;
18
  }
19
 
20
+ export const migrations: Migration[] = [
21
+ updateSearchAssistant,
22
+ updateAssistantsModels,
23
+ addToolsToSettings,
24
+ updateMessageUpdates,
25
+ updateMessageFiles,
26
+ ];
src/lib/server/endpoints/cohere/endpointCohere.ts CHANGED
@@ -4,6 +4,9 @@ import type { Endpoint } from "../endpoints";
4
  import type { TextGenerationStreamOutput } from "@huggingface/inference";
5
  import type { Cohere, CohereClient } from "cohere-ai";
6
  import { buildPrompt } from "$lib/buildPrompt";
 
 
 
7
 
8
  export const endpointCohereParametersSchema = z.object({
9
  weight: z.number().int().positive().default(1),
@@ -28,12 +31,18 @@ export async function endpointCohere(
28
  throw new Error("Failed to import cohere-ai", { cause: e });
29
  }
30
 
31
- return async ({ messages, preprompt, generateSettings, continueMessage }) => {
32
  let system = preprompt;
33
  if (messages?.[0]?.from === "system") {
34
  system = messages[0].content;
35
  }
36
 
 
 
 
 
 
 
37
  const parameters = { ...model.parameters, ...generateSettings };
38
 
39
  return (async function* () {
@@ -42,10 +51,12 @@ export async function endpointCohere(
42
 
43
  if (raw) {
44
  const prompt = await buildPrompt({
45
- messages: messages.filter((message) => message.from !== "system"),
46
  model,
47
  preprompt: system,
48
  continueMessage,
 
 
49
  });
50
 
51
  stream = await cohere.chatStream({
@@ -67,18 +78,35 @@ export async function endpointCohere(
67
  message: message.content,
68
  })) satisfies Cohere.ChatMessage[];
69
 
70
- stream = await cohere.chatStream({
71
- model: model.id ?? model.name,
72
- chatHistory: formattedMessages.slice(0, -1),
73
- message: formattedMessages[formattedMessages.length - 1].message,
74
- preamble: system,
75
- p: parameters?.top_p,
76
- k: parameters?.top_k,
77
- maxTokens: parameters?.max_new_tokens,
78
- temperature: parameters?.temperature,
79
- stopSequences: parameters?.stop,
80
- frequencyPenalty: parameters?.frequency_penalty,
81
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  }
83
 
84
  for await (const output of stream) {
@@ -93,6 +121,18 @@ export async function endpointCohere(
93
  generated_text: null,
94
  details: null,
95
  } satisfies TextGenerationStreamOutput;
 
 
 
 
 
 
 
 
 
 
 
 
96
  } else if (output.eventType === "stream-end") {
97
  if (["ERROR", "ERROR_TOXIC", "ERROR_LIMIT"].includes(output.finishReason)) {
98
  throw new Error(output.finishReason);
@@ -112,3 +152,26 @@ export async function endpointCohere(
112
  })();
113
  };
114
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  import type { TextGenerationStreamOutput } from "@huggingface/inference";
5
  import type { Cohere, CohereClient } from "cohere-ai";
6
  import { buildPrompt } from "$lib/buildPrompt";
7
+ import { ToolResultStatus, type ToolCall } from "$lib/types/Tool";
8
+ import { pipeline, Writable, Readable } from "node:stream";
9
+ import { toolHasName } from "$lib/utils/tools";
10
 
11
  export const endpointCohereParametersSchema = z.object({
12
  weight: z.number().int().positive().default(1),
 
31
  throw new Error("Failed to import cohere-ai", { cause: e });
32
  }
33
 
34
+ return async ({ messages, preprompt, generateSettings, continueMessage, tools, toolResults }) => {
35
  let system = preprompt;
36
  if (messages?.[0]?.from === "system") {
37
  system = messages[0].content;
38
  }
39
 
40
+ // Tools must use [A-z_] for their names and directly_answer is banned
41
+ // It's safe to convert the tool names because we treat - and _ the same
42
+ tools = tools
43
+ ?.filter((tool) => !toolHasName("directly_answer", tool))
44
+ .map((tool) => ({ ...tool, name: tool.name.replaceAll("-", "_") }));
45
+
46
  const parameters = { ...model.parameters, ...generateSettings };
47
 
48
  return (async function* () {
 
51
 
52
  if (raw) {
53
  const prompt = await buildPrompt({
54
+ messages,
55
  model,
56
  preprompt: system,
57
  continueMessage,
58
+ tools,
59
+ toolResults,
60
  });
61
 
62
  stream = await cohere.chatStream({
 
78
  message: message.content,
79
  })) satisfies Cohere.ChatMessage[];
80
 
81
+ stream = await cohere
82
+ .chatStream({
83
+ model: model.id ?? model.name,
84
+ chatHistory: formattedMessages.slice(0, -1),
85
+ message: formattedMessages[formattedMessages.length - 1].message,
86
+ preamble: system,
87
+ p: parameters?.top_p,
88
+ k: parameters?.top_k,
89
+ maxTokens: parameters?.max_new_tokens,
90
+ temperature: parameters?.temperature,
91
+ stopSequences: parameters?.stop,
92
+ frequencyPenalty: parameters?.frequency_penalty,
93
+ tools,
94
+ toolResults: toolResults?.map((toolResult) => {
95
+ if (toolResult.status === ToolResultStatus.Error) {
96
+ return { call: toolResult.call, outputs: [{ error: toolResult.message }] };
97
+ }
98
+ return { call: toolResult.call, outputs: toolResult.outputs };
99
+ }),
100
+ })
101
+ .catch(async (err) => {
102
+ if (!err.body) throw err;
103
+
104
+ // Decode the error message and throw
105
+ const message = await convertStreamToBuffer(err.body).catch(() => {
106
+ throw err;
107
+ });
108
+ throw Error(message, { cause: err });
109
+ });
110
  }
111
 
112
  for await (const output of stream) {
 
121
  generated_text: null,
122
  details: null,
123
  } satisfies TextGenerationStreamOutput;
124
+ } else if (output.eventType === "tool-calls-generation") {
125
+ yield {
126
+ token: {
127
+ id: tokenId++,
128
+ text: "",
129
+ logprob: 0,
130
+ special: true,
131
+ toolCalls: output.toolCalls as ToolCall[],
132
+ },
133
+ generated_text: null,
134
+ details: null,
135
+ };
136
  } else if (output.eventType === "stream-end") {
137
  if (["ERROR", "ERROR_TOXIC", "ERROR_LIMIT"].includes(output.finishReason)) {
138
  throw new Error(output.finishReason);
 
152
  })();
153
  };
154
  }
155
+
156
+ async function convertStreamToBuffer(webReadableStream: Readable) {
157
+ return new Promise<string>((resolve, reject) => {
158
+ const chunks: Buffer[] = [];
159
+
160
+ pipeline(
161
+ webReadableStream,
162
+ new Writable({
163
+ write(chunk, _, callback) {
164
+ chunks.push(chunk);
165
+ callback();
166
+ },
167
+ }),
168
+ (err) => {
169
+ if (err) {
170
+ reject(err);
171
+ } else {
172
+ resolve(Buffer.concat(chunks).toString("utf-8"));
173
+ }
174
+ }
175
+ );
176
+ });
177
+ }
src/lib/server/endpoints/endpoints.ts CHANGED
@@ -1,6 +1,6 @@
1
  import type { Conversation } from "$lib/types/Conversation";
2
  import type { Message } from "$lib/types/Message";
3
- import type { TextGenerationStreamOutput } from "@huggingface/inference";
4
  import { endpointTgi, endpointTgiParametersSchema } from "./tgi/endpointTgi";
5
  import { z } from "zod";
6
  import endpointAws, { endpointAwsParametersSchema } from "./aws/endpointAws";
@@ -26,23 +26,31 @@ import endpointLangserve, {
26
  endpointLangserveParametersSchema,
27
  } from "./langserve/endpointLangserve";
28
 
 
 
29
  export type EndpointMessage = Omit<Message, "id">;
 
30
  // parameters passed when generating text
31
  export interface EndpointParameters {
32
  messages: EndpointMessage[];
33
  preprompt?: Conversation["preprompt"];
34
  continueMessage?: boolean; // used to signal that the last message will be extended
35
  generateSettings?: Partial<Model["parameters"]>;
 
 
36
  isMultimodal?: boolean;
37
  }
38
 
39
  interface CommonEndpoint {
40
  weight: number;
41
  }
 
 
 
42
  // type signature for the endpoint
43
  export type Endpoint = (
44
  params: EndpointParameters
45
- ) => Promise<AsyncGenerator<TextGenerationStreamOutput, void, void>>;
46
 
47
  // generator function that takes in parameters for defining the endpoint and return the endpoint
48
  export type EndpointGenerator<T extends CommonEndpoint> = (parameters: T) => Endpoint;
 
1
  import type { Conversation } from "$lib/types/Conversation";
2
  import type { Message } from "$lib/types/Message";
3
+ import type { TextGenerationStreamOutput, TextGenerationStreamToken } from "@huggingface/inference";
4
  import { endpointTgi, endpointTgiParametersSchema } from "./tgi/endpointTgi";
5
  import { z } from "zod";
6
  import endpointAws, { endpointAwsParametersSchema } from "./aws/endpointAws";
 
26
  endpointLangserveParametersSchema,
27
  } from "./langserve/endpointLangserve";
28
 
29
+ import type { Tool, ToolCall, ToolResult } from "$lib/types/Tool";
30
+
31
  export type EndpointMessage = Omit<Message, "id">;
32
+
33
  // parameters passed when generating text
34
  export interface EndpointParameters {
35
  messages: EndpointMessage[];
36
  preprompt?: Conversation["preprompt"];
37
  continueMessage?: boolean; // used to signal that the last message will be extended
38
  generateSettings?: Partial<Model["parameters"]>;
39
+ tools?: Tool[];
40
+ toolResults?: ToolResult[];
41
  isMultimodal?: boolean;
42
  }
43
 
44
  interface CommonEndpoint {
45
  weight: number;
46
  }
47
+ type TextGenerationStreamOutputWithTools = TextGenerationStreamOutput & {
48
+ token: TextGenerationStreamToken & { toolCalls?: ToolCall[] };
49
+ };
50
  // type signature for the endpoint
51
  export type Endpoint = (
52
  params: EndpointParameters
53
+ ) => Promise<AsyncGenerator<TextGenerationStreamOutputWithTools, void, void>>;
54
 
55
  // generator function that takes in parameters for defining the endpoint and return the endpoint
56
  export type EndpointGenerator<T extends CommonEndpoint> = (parameters: T) => Endpoint;
src/lib/server/endpoints/tgi/endpointTgi.ts CHANGED
@@ -35,7 +35,15 @@ export function endpointTgi(input: z.input<typeof endpointTgiParametersSchema>):
35
  endpointTgiParametersSchema.parse(input);
36
  const imageProcessor = makeImageProcessor(multimodal.image);
37
 
38
- return async ({ messages, preprompt, continueMessage, generateSettings, isMultimodal }) => {
 
 
 
 
 
 
 
 
39
  const messagesWithResizedFiles = await Promise.all(
40
  messages.map((message) => prepareMessage(Boolean(isMultimodal), message, imageProcessor))
41
  );
@@ -45,6 +53,8 @@ export function endpointTgi(input: z.input<typeof endpointTgiParametersSchema>):
45
  preprompt,
46
  model,
47
  continueMessage,
 
 
48
  });
49
 
50
  return textGenerationStream(
 
35
  endpointTgiParametersSchema.parse(input);
36
  const imageProcessor = makeImageProcessor(multimodal.image);
37
 
38
+ return async ({
39
+ messages,
40
+ preprompt,
41
+ continueMessage,
42
+ generateSettings,
43
+ tools,
44
+ toolResults,
45
+ isMultimodal,
46
+ }) => {
47
  const messagesWithResizedFiles = await Promise.all(
48
  messages.map((message) => prepareMessage(Boolean(isMultimodal), message, imageProcessor))
49
  );
 
53
  preprompt,
54
  model,
55
  continueMessage,
56
+ tools,
57
+ toolResults,
58
  });
59
 
60
  return textGenerationStream(
src/lib/server/files/downloadFile.ts CHANGED
@@ -9,29 +9,26 @@ export async function downloadFile(
9
  convId: Conversation["_id"] | SharedConversation["_id"]
10
  ): Promise<MessageFile & { type: "base64" }> {
11
  const fileId = collections.bucket.find({ filename: `${convId.toString()}-${sha256}` });
12
- let mime = "";
13
 
14
- const buffer = await fileId.next().then(async (file) => {
15
- if (!file) {
16
- throw error(404, "File not found");
17
- }
18
- if (file.metadata?.conversation !== convId.toString()) {
19
- throw error(403, "You don't have access to this file.");
20
- }
21
 
22
- mime = file.metadata?.mime;
 
23
 
24
- const fileStream = collections.bucket.openDownloadStream(file._id);
25
 
26
- const fileBuffer = await new Promise<Buffer>((resolve, reject) => {
27
- const chunks: Uint8Array[] = [];
28
- fileStream.on("data", (chunk) => chunks.push(chunk));
29
- fileStream.on("error", reject);
30
- fileStream.on("end", () => resolve(Buffer.concat(chunks)));
31
- });
32
-
33
- return fileBuffer;
34
  });
35
 
36
- return { type: "base64", value: buffer.toString("base64"), mime };
37
  }
 
9
  convId: Conversation["_id"] | SharedConversation["_id"]
10
  ): Promise<MessageFile & { type: "base64" }> {
11
  const fileId = collections.bucket.find({ filename: `${convId.toString()}-${sha256}` });
 
12
 
13
+ const file = await fileId.next();
14
+ if (!file) {
15
+ throw error(404, "File not found");
16
+ }
17
+ if (file.metadata?.conversation !== convId.toString()) {
18
+ throw error(403, "You don't have access to this file.");
19
+ }
20
 
21
+ const mime = file.metadata?.mime;
22
+ const name = file.filename;
23
 
24
+ const fileStream = collections.bucket.openDownloadStream(file._id);
25
 
26
+ const buffer = await new Promise<Buffer>((resolve, reject) => {
27
+ const chunks: Uint8Array[] = [];
28
+ fileStream.on("data", (chunk) => chunks.push(chunk));
29
+ fileStream.on("error", reject);
30
+ fileStream.on("end", () => resolve(Buffer.concat(chunks)));
 
 
 
31
  });
32
 
33
+ return { type: "base64", name, value: buffer.toString("base64"), mime };
34
  }
src/lib/server/files/uploadFile.ts CHANGED
@@ -20,7 +20,9 @@ export async function uploadFile(file: File, conv: Conversation): Promise<Messag
20
 
21
  // only return the filename when upload throws a finish event or a 20s time out occurs
22
  return new Promise((resolve, reject) => {
23
- upload.once("finish", () => resolve({ type: "hash", value: sha, mime: file.type }));
 
 
24
  upload.once("error", reject);
25
  setTimeout(() => reject(new Error("Upload timed out")), 20_000);
26
  });
 
20
 
21
  // only return the filename when upload throws a finish event or a 20s time out occurs
22
  return new Promise((resolve, reject) => {
23
+ upload.once("finish", () =>
24
+ resolve({ type: "hash", value: sha, mime: file.type, name: file.name })
25
+ );
26
  upload.once("error", reject);
27
  setTimeout(() => reject(new Error("Upload timed out")), 20_000);
28
  });
src/lib/server/models.ts CHANGED
@@ -12,6 +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
 
16
  type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
17
 
@@ -61,6 +62,7 @@ const modelConfig = z.object({
61
  .passthrough()
62
  .optional(),
63
  multimodal: z.boolean().default(false),
 
64
  unlisted: z.boolean().default(false),
65
  embeddingModel: validateEmbeddingModelByName(embeddingModels).optional(),
66
  });
@@ -94,7 +96,7 @@ async function getChatPromptRender(
94
  process.exit();
95
  }
96
 
97
- const renderTemplate = ({ messages, preprompt }: ChatTemplateInput) => {
98
  let formattedMessages: { role: string; content: string }[] = messages.map((message) => ({
99
  content: message.content,
100
  role: message.from,
@@ -110,9 +112,64 @@ async function getChatPromptRender(
110
  ];
111
  }
112
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  const output = tokenizer.apply_chat_template(formattedMessages, {
114
  tokenize: false,
115
  add_generation_prompt: true,
 
 
 
 
 
 
 
 
 
116
  });
117
 
118
  if (typeof output !== "string") {
@@ -134,6 +191,10 @@ const processModel = async (m: z.infer<typeof modelConfig>) => ({
134
  parameters: { ...m.parameters, stop_sequences: m.parameters?.stop },
135
  });
136
 
 
 
 
 
137
  const addEndpoint = (m: Awaited<ReturnType<typeof processModel>>) => ({
138
  ...m,
139
  getEndpoint: async (): Promise<Endpoint> => {
@@ -189,7 +250,9 @@ const addEndpoint = (m: Awaited<ReturnType<typeof processModel>>) => ({
189
  },
190
  });
191
 
192
- export const models = await Promise.all(modelsRaw.map((e) => processModel(e).then(addEndpoint)));
 
 
193
 
194
  export const defaultModel = models[0];
195
 
@@ -224,5 +287,5 @@ export const smallModel = env.TASK_MODEL
224
 
225
  export type BackendModel = Optional<
226
  typeof defaultModel,
227
- "preprompt" | "parameters" | "multimodal" | "unlisted"
228
  >;
 
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
 
 
62
  .passthrough()
63
  .optional(),
64
  multimodal: z.boolean().default(false),
65
+ tools: z.boolean().default(false),
66
  unlisted: z.boolean().default(false),
67
  embeddingModel: validateEmbeddingModelByName(embeddingModels).optional(),
68
  });
 
96
  process.exit();
97
  }
98
 
99
+ const renderTemplate = ({ messages, preprompt, tools, toolResults }: ChatTemplateInput) => {
100
  let formattedMessages: { role: string; content: string }[] = messages.map((message) => ({
101
  content: message.content,
102
  role: message.from,
 
112
  ];
113
  }
114
 
115
+ if (toolResults?.length) {
116
+ // todo: should update the command r+ tokenizer to support system messages at any location
117
+ // or use the `rag` mode without the citations
118
+ formattedMessages = [
119
+ {
120
+ role: "system",
121
+ content:
122
+ "\n\n<results>\n" +
123
+ toolResults
124
+ .flatMap((result, idx) => {
125
+ if (result.status === ToolResultStatus.Error) {
126
+ return (
127
+ `Document: ${idx}\n` + `Tool "${result.call.name}" error\n` + result.message
128
+ );
129
+ }
130
+ return (
131
+ `Document: ${idx}\n` +
132
+ result.outputs
133
+ .flatMap((output) =>
134
+ Object.entries(output).map(([title, text]) => `${title}\n${text}`)
135
+ )
136
+ .join("\n")
137
+ );
138
+ })
139
+ .join("\n\n") +
140
+ "\n</results>",
141
+ },
142
+ ...formattedMessages,
143
+ ];
144
+ tools = [];
145
+ }
146
+
147
+ const chatTemplate = tools?.length ? "tool_use" : undefined;
148
+
149
+ const documents = (toolResults ?? []).flatMap((result) => {
150
+ if (result.status === ToolResultStatus.Error) {
151
+ return [{ title: `Tool "${result.call.name}" error`, text: "\n" + result.message }];
152
+ }
153
+ return result.outputs.flatMap((output) =>
154
+ Object.entries(output).map(([title, text]) => ({
155
+ title: `Tool "${result.call.name}" ${title}`,
156
+ text: "\n" + text,
157
+ }))
158
+ );
159
+ });
160
+
161
  const output = tokenizer.apply_chat_template(formattedMessages, {
162
  tokenize: false,
163
  add_generation_prompt: true,
164
+ chat_template: chatTemplate,
165
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
166
+ // @ts-ignore
167
+ tools:
168
+ tools?.map(({ parameterDefinitions, ...tool }) => ({
169
+ parameter_definitions: parameterDefinitions,
170
+ ...tool,
171
+ })) ?? [],
172
+ documents,
173
  });
174
 
175
  if (typeof output !== "string") {
 
191
  parameters: { ...m.parameters, stop_sequences: m.parameters?.stop },
192
  });
193
 
194
+ export type ProcessedModel = Awaited<ReturnType<typeof processModel>> & {
195
+ getEndpoint: () => Promise<Endpoint>;
196
+ };
197
+
198
  const addEndpoint = (m: Awaited<ReturnType<typeof processModel>>) => ({
199
  ...m,
200
  getEndpoint: async (): Promise<Endpoint> => {
 
250
  },
251
  });
252
 
253
+ export const models: ProcessedModel[] = await Promise.all(
254
+ modelsRaw.map((e) => processModel(e).then(addEndpoint))
255
+ );
256
 
257
  export const defaultModel = models[0];
258
 
 
287
 
288
  export type BackendModel = Optional<
289
  typeof defaultModel,
290
+ "preprompt" | "parameters" | "multimodal" | "unlisted" | "tools"
291
  >;
src/lib/server/textGeneration/assistant.ts ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { isURLLocal } from "../isURLLocal";
2
+ import { env } from "$env/dynamic/private";
3
+ import { collections } from "$lib/server/database";
4
+ import type { Assistant } from "$lib/types/Assistant";
5
+ import type { ObjectId } from "mongodb";
6
+
7
+ export async function processPreprompt(preprompt: string) {
8
+ const urlRegex = /{{\s?url=(.*?)\s?}}/g;
9
+
10
+ for (const match of preprompt.matchAll(urlRegex)) {
11
+ try {
12
+ const url = new URL(match[1]);
13
+ if ((await isURLLocal(url)) && env.ENABLE_LOCAL_FETCH !== "true") {
14
+ throw new Error("URL couldn't be fetched, it resolved to a local address.");
15
+ }
16
+
17
+ const res = await fetch(url.href);
18
+
19
+ if (!res.ok) {
20
+ throw new Error("URL couldn't be fetched, error " + res.status);
21
+ }
22
+ const text = await res.text();
23
+ preprompt = preprompt.replaceAll(match[0], text);
24
+ } catch (e) {
25
+ preprompt = preprompt.replaceAll(match[0], (e as Error).message);
26
+ }
27
+ }
28
+
29
+ return preprompt;
30
+ }
31
+
32
+ export async function getAssistantById(id?: ObjectId) {
33
+ return collections.assistants
34
+ .findOne<Pick<Assistant, "rag" | "dynamicPrompt" | "generateSettings">>(
35
+ { _id: id },
36
+ { projection: { rag: 1, dynamicPrompt: 1, generateSettings: 1 } }
37
+ )
38
+ .then((a) => a ?? undefined);
39
+ }
40
+
41
+ export function assistantHasWebSearch(assistant?: Pick<Assistant, "rag"> | null) {
42
+ return (
43
+ env.ENABLE_ASSISTANTS_RAG === "true" &&
44
+ !!assistant?.rag &&
45
+ (assistant.rag.allowedLinks.length > 0 ||
46
+ assistant.rag.allowedDomains.length > 0 ||
47
+ assistant.rag.allowAllDomains)
48
+ );
49
+ }
50
+
51
+ export function assistantHasDynamicPrompt(assistant?: Pick<Assistant, "dynamicPrompt">) {
52
+ return env.ENABLE_ASSISTANTS_RAG === "true" && Boolean(assistant?.dynamicPrompt);
53
+ }
src/lib/server/textGeneration/generate.ts ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { ToolResult } from "$lib/types/Tool";
2
+ import { MessageUpdateType, type MessageUpdate } from "$lib/types/MessageUpdate";
3
+ import { AbortedGenerations } from "../abortedGenerations";
4
+ import type { TextGenerationContext } from "./types";
5
+ import type { EndpointMessage } from "../endpoints/endpoints";
6
+
7
+ type GenerateContext = Omit<TextGenerationContext, "messages"> & { messages: EndpointMessage[] };
8
+
9
+ export async function* generate(
10
+ { model, endpoint, conv, messages, assistant, isContinue, promptedAt }: GenerateContext,
11
+ toolResults: ToolResult[],
12
+ preprompt?: string
13
+ ): AsyncIterable<MessageUpdate> {
14
+ for await (const output of await endpoint({
15
+ messages,
16
+ preprompt,
17
+ continueMessage: isContinue,
18
+ generateSettings: assistant?.generateSettings,
19
+ toolResults,
20
+ })) {
21
+ // text generation completed
22
+ if (output.generated_text) {
23
+ let interrupted =
24
+ !output.token.special && !model.parameters.stop?.includes(output.token.text);
25
+
26
+ let text = output.generated_text.trimEnd();
27
+ for (const stopToken of model.parameters.stop ?? []) {
28
+ if (!text.endsWith(stopToken)) continue;
29
+
30
+ interrupted = false;
31
+ text = text.slice(0, text.length - stopToken.length);
32
+ }
33
+
34
+ yield { type: MessageUpdateType.FinalAnswer, text, interrupted };
35
+ continue;
36
+ }
37
+
38
+ // ignore special tokens
39
+ if (output.token.special) continue;
40
+
41
+ // pass down normal token
42
+ yield { type: MessageUpdateType.Stream, token: output.token.text };
43
+
44
+ // abort check
45
+ const date = AbortedGenerations.getInstance().getList().get(conv._id.toString());
46
+ if (date && date > promptedAt) break;
47
+
48
+ // no output check
49
+ if (!output) break;
50
+ }
51
+ }
src/lib/server/textGeneration/index.ts ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { runWebSearch } from "$lib/server/websearch/runWebSearch";
2
+ import { preprocessMessages } from "../endpoints/preprocessMessages";
3
+
4
+ import { generateTitleForConversation } from "./title";
5
+ import {
6
+ assistantHasDynamicPrompt,
7
+ assistantHasWebSearch,
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,
15
+ MessageUpdateType,
16
+ MessageUpdateStatus,
17
+ } from "$lib/types/MessageUpdate";
18
+ import { generate } from "./generate";
19
+ import { mergeAsyncGenerators } from "$lib/utils/mergeAsyncGenerators";
20
+ import type { TextGenerationContext } from "./types";
21
+
22
+ export async function* textGeneration(ctx: TextGenerationContext) {
23
+ yield* mergeAsyncGenerators([
24
+ textGenerationWithoutTitle(ctx),
25
+ generateTitleForConversation(ctx.conv),
26
+ ]);
27
+ }
28
+
29
+ async function* textGenerationWithoutTitle(
30
+ ctx: TextGenerationContext
31
+ ): AsyncGenerator<MessageUpdate, undefined, undefined> {
32
+ yield {
33
+ type: MessageUpdateType.Status,
34
+ status: MessageUpdateStatus.Started,
35
+ };
36
+
37
+ ctx.assistant ??= await getAssistantById(ctx.conv.assistantId);
38
+ const { model, conv, messages, assistant, isContinue, webSearch, toolsPreference } = ctx;
39
+ const convId = conv._id;
40
+
41
+ // perform websearch if requested
42
+ // it can be because the user toggled the webSearch or because the assistant has webSearch enabled
43
+ // if tools are enabled, we don't perform it here since we will add the websearch as a tool
44
+ let webSearchResult: WebSearch | undefined;
45
+ if (
46
+ !isContinue &&
47
+ !model.tools &&
48
+ ((webSearch && !conv.assistantId) || assistantHasWebSearch(assistant))
49
+ ) {
50
+ webSearchResult = yield* runWebSearch(conv, messages, assistant?.rag);
51
+ }
52
+
53
+ let preprompt = conv.preprompt;
54
+ if (assistantHasDynamicPrompt(assistant) && preprompt) {
55
+ preprompt = await processPreprompt(preprompt);
56
+ if (messages[0].from === "system") messages[0].content = preprompt;
57
+ }
58
+
59
+ const tools = pickTools(toolsPreference, Boolean(assistant));
60
+ const toolResults = yield* runTools(ctx, tools, preprompt);
61
+
62
+ const processedMessages = await preprocessMessages(messages, webSearchResult, convId);
63
+ yield* generate({ ...ctx, messages: processedMessages }, toolResults, preprompt);
64
+ }
src/lib/server/{summarize.ts → textGeneration/title.ts} RENAMED
@@ -1,14 +1,41 @@
1
  import { env } from "$env/dynamic/private";
2
  import { generateFromDefaultEndpoint } from "$lib/server/generateFromDefaultEndpoint";
3
- import type { EndpointMessage } from "./endpoints/endpoints";
4
  import { logger } from "$lib/server/logger";
 
 
5
 
6
- export async function summarize(prompt: string) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  if (!env.LLM_SUMMERIZATION) {
8
  return prompt.split(/\s+/g).slice(0, 5).join(" ");
9
  }
10
 
11
  const messages: Array<EndpointMessage> = [
 
 
 
 
 
12
  { from: "user", content: "Who is the president of Gabon?" },
13
  { from: "assistant", content: "🇬🇦 President of Gabon" },
14
  { from: "user", content: "Who is Julien Chaumond?" },
@@ -23,6 +50,8 @@ export async function summarize(prompt: string) {
23
  { from: "assistant", content: "🎥 Favorite movie" },
24
  { from: "user", content: "Explain the concept of artificial intelligence in one sentence" },
25
  { from: "assistant", content: "🤖 AI definition" },
 
 
26
  { from: "user", content: prompt },
27
  ];
28
 
 
1
  import { env } from "$env/dynamic/private";
2
  import { generateFromDefaultEndpoint } from "$lib/server/generateFromDefaultEndpoint";
3
+ import type { EndpointMessage } from "../endpoints/endpoints";
4
  import { logger } from "$lib/server/logger";
5
+ import { MessageUpdateType, type MessageUpdate } from "$lib/types/MessageUpdate";
6
+ import type { Conversation } from "$lib/types/Conversation";
7
 
8
+ export async function* generateTitleForConversation(
9
+ conv: Conversation
10
+ ): AsyncGenerator<MessageUpdate, undefined, undefined> {
11
+ try {
12
+ const userMessage = conv.messages.find((m) => m.from === "user");
13
+ // HACK: detect if the conversation is new
14
+ if (conv.title !== "New Chat" || !userMessage) return;
15
+
16
+ const prompt = userMessage.content;
17
+ const title = (await generateTitle(prompt)) ?? "New Chat";
18
+
19
+ yield {
20
+ type: MessageUpdateType.Title,
21
+ title,
22
+ };
23
+ } catch (cause) {
24
+ console.error(Error("Failed whilte generating title for conversation", { cause }));
25
+ }
26
+ }
27
+
28
+ export async function generateTitle(prompt: string) {
29
  if (!env.LLM_SUMMERIZATION) {
30
  return prompt.split(/\s+/g).slice(0, 5).join(" ");
31
  }
32
 
33
  const messages: Array<EndpointMessage> = [
34
+ {
35
+ from: "system",
36
+ content:
37
+ "You are a summarization AI. You'll never answer a user's question directly, but instead summarize the user's request into a single short sentence of four words or less. Always start your answer with an emoji relevant to the summary",
38
+ },
39
  { from: "user", content: "Who is the president of Gabon?" },
40
  { from: "assistant", content: "🇬🇦 President of Gabon" },
41
  { from: "user", content: "Who is Julien Chaumond?" },
 
50
  { from: "assistant", content: "🎥 Favorite movie" },
51
  { from: "user", content: "Explain the concept of artificial intelligence in one sentence" },
52
  { from: "assistant", content: "🤖 AI definition" },
53
+ { from: "user", content: "Draw a cute cat" },
54
+ { from: "assistant", content: "🐱 Cute cat drawing" },
55
  { from: "user", content: prompt },
56
  ];
57
 
src/lib/server/textGeneration/tools.ts ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ToolResultStatus, type ToolCall, type ToolResult } from "$lib/types/Tool";
2
+ import { v4 as uuidV4 } from "uuid";
3
+ import JSON5 from "json5";
4
+ import type { BackendTool, BackendToolContext } from "../tools";
5
+ import {
6
+ MessageToolUpdateType,
7
+ MessageUpdateStatus,
8
+ MessageUpdateType,
9
+ type MessageUpdate,
10
+ } from "$lib/types/MessageUpdate";
11
+ import type { TextGenerationContext } from "./types";
12
+
13
+ import { allTools } from "../tools";
14
+ import directlyAnswer from "../tools/directlyAnswer";
15
+ import websearch from "../tools/web/search";
16
+ import { z } from "zod";
17
+ import { logger } from "../logger";
18
+ import { toolHasName } from "../tools/utils";
19
+ import type { MessageFile } from "$lib/types/Message";
20
+ import { mergeAsyncGenerators } from "$lib/utils/mergeAsyncGenerators";
21
+
22
+ function makeFilesPrompt(files: MessageFile[], fileMessageIndex: number): string {
23
+ if (files.length === 0) {
24
+ return "The user has not uploaded any files. Do not attempt to use any tools that require files";
25
+ }
26
+
27
+ const stringifiedFiles = files
28
+ .map(
29
+ (file, fileIndex) =>
30
+ ` - fileMessageIndex ${fileMessageIndex} | fileIndex ${fileIndex} | ${file.name} (${file.mime})`
31
+ )
32
+ .join("\n");
33
+ return `Attached ${files.length} file${files.length === 1 ? "" : "s"}:\n${stringifiedFiles}`;
34
+ }
35
+
36
+ export function pickTools(
37
+ toolsPreference: Record<string, boolean>,
38
+ isAssistant: boolean
39
+ ): BackendTool[] {
40
+ // if it's an assistant, only support websearch for now
41
+ if (isAssistant) return [directlyAnswer, websearch];
42
+
43
+ // filter based on tool preferences, add the tools that are on by default
44
+ return allTools.filter((el) => {
45
+ if (el.isLocked && el.isOnByDefault) return true;
46
+ return toolsPreference?.[el.name] ?? el.isOnByDefault;
47
+ });
48
+ }
49
+
50
+ async function* runTool(
51
+ { conv, messages, preprompt, assistant }: BackendToolContext,
52
+ tools: BackendTool[],
53
+ call: ToolCall
54
+ ): AsyncGenerator<MessageUpdate, ToolResult | undefined, undefined> {
55
+ const uuid = uuidV4();
56
+
57
+ const tool = tools.find((el) => toolHasName(call.name, el));
58
+ if (!tool) {
59
+ return { call, status: ToolResultStatus.Error, message: `Could not find tool "${call.name}"` };
60
+ }
61
+
62
+ // Special case for directly_answer tool where we ignore
63
+ if (toolHasName(directlyAnswer.name, tool)) return;
64
+
65
+ yield {
66
+ type: MessageUpdateType.Tool,
67
+ subtype: MessageToolUpdateType.Call,
68
+ uuid,
69
+ call,
70
+ };
71
+ try {
72
+ const toolResult = yield* tool.call(call.parameters, {
73
+ conv,
74
+ messages,
75
+ preprompt,
76
+ assistant,
77
+ });
78
+ yield {
79
+ type: MessageUpdateType.Tool,
80
+ subtype: MessageToolUpdateType.Result,
81
+ uuid,
82
+ result: { ...toolResult, call } as ToolResult,
83
+ };
84
+ return { ...toolResult, call } as ToolResult;
85
+ } catch (cause) {
86
+ console.error(Error(`Failed while running tool ${call.name}`), { cause });
87
+ return {
88
+ call,
89
+ status: ToolResultStatus.Error,
90
+ message: cause instanceof Error ? cause.message : String(cause),
91
+ };
92
+ }
93
+ }
94
+
95
+ export async function* runTools(
96
+ { endpoint, conv, messages, assistant }: TextGenerationContext,
97
+ tools: BackendTool[],
98
+ preprompt?: string
99
+ ): AsyncGenerator<MessageUpdate, ToolResult[], undefined> {
100
+ const calls: ToolCall[] = [];
101
+
102
+ const messagesWithFilesPrompt = messages.map((message, idx) => {
103
+ if (!message.files?.length) return message;
104
+ return {
105
+ ...message,
106
+ content: `${message.content}\n${makeFilesPrompt(message.files, idx)}`,
107
+ };
108
+ });
109
+
110
+ // do the function calling bits here
111
+ for await (const output of await endpoint({
112
+ messages: messagesWithFilesPrompt,
113
+ preprompt,
114
+ generateSettings: assistant?.generateSettings,
115
+ tools,
116
+ })) {
117
+ // model natively supports tool calls
118
+ if (output.token.toolCalls) {
119
+ calls.push(...output.token.toolCalls);
120
+ continue;
121
+ }
122
+
123
+ // look for a code blocks of ```json and parse them
124
+ // if they're valid json, add them to the calls array
125
+ if (output.generated_text) {
126
+ const codeBlocks = Array.from(output.generated_text.matchAll(/```json\n(.*?)```/gs));
127
+ if (codeBlocks.length === 0) continue;
128
+
129
+ // grab only the capture group from the regex match
130
+ for (const [, block] of codeBlocks) {
131
+ try {
132
+ calls.push(
133
+ ...JSON5.parse(block).filter(isExternalToolCall).map(externalToToolCall).filter(Boolean)
134
+ );
135
+ } catch (cause) {
136
+ // error parsing the calls
137
+ yield {
138
+ type: MessageUpdateType.Status,
139
+ status: MessageUpdateStatus.Error,
140
+ message: cause instanceof Error ? cause.message : String(cause),
141
+ };
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ const toolContext: BackendToolContext = { conv, messages, preprompt, assistant };
148
+ const toolResults: (ToolResult | undefined)[] = yield* mergeAsyncGenerators(
149
+ calls.map((call) => runTool(toolContext, tools, call))
150
+ );
151
+ return toolResults.filter((result): result is ToolResult => result !== undefined);
152
+ }
153
+
154
+ const externalToolCall = z.object({
155
+ tool_name: z.string(),
156
+ parameters: z.record(z.any()),
157
+ });
158
+
159
+ type ExternalToolCall = z.infer<typeof externalToolCall>;
160
+
161
+ function isExternalToolCall(call: unknown): call is ExternalToolCall {
162
+ return externalToolCall.safeParse(call).success;
163
+ }
164
+
165
+ function externalToToolCall(call: ExternalToolCall): ToolCall | undefined {
166
+ // Convert - to _ since some models insist on using _ instead of -
167
+ const tool = allTools.find((tool) => toolHasName(call.tool_name, tool));
168
+ if (!tool) {
169
+ logger.debug(`Model requested tool that does not exist: "${call.tool_name}". Skipping tool...`);
170
+ return;
171
+ }
172
+
173
+ const parametersWithDefaults: Record<string, string> = {};
174
+
175
+ for (const [key, definition] of Object.entries(tool.parameterDefinitions)) {
176
+ const value = call.parameters[key];
177
+
178
+ // Required so ensure it's there, otherwise return undefined
179
+ if (definition.required) {
180
+ if (value === undefined) {
181
+ logger.debug(
182
+ `Model requested tool "${call.tool_name}" but was missing required parameter "${key}". Skipping tool...`
183
+ );
184
+ return;
185
+ }
186
+ parametersWithDefaults[key] = value;
187
+ continue;
188
+ }
189
+
190
+ // Optional so use default if not there
191
+ parametersWithDefaults[key] = value ?? definition.default;
192
+ }
193
+
194
+ return {
195
+ name: call.tool_name,
196
+ parameters: parametersWithDefaults,
197
+ };
198
+ }
src/lib/server/textGeneration/types.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { ProcessedModel } from "../models";
2
+ import type { Endpoint } from "../endpoints/endpoints";
3
+ import type { Conversation } from "$lib/types/Conversation";
4
+ import type { Message } from "$lib/types/Message";
5
+ import type { Assistant } from "$lib/types/Assistant";
6
+
7
+ export interface TextGenerationContext {
8
+ model: ProcessedModel;
9
+ endpoint: Endpoint;
10
+ conv: Conversation;
11
+ messages: Message[];
12
+ assistant?: Pick<Assistant, "rag" | "dynamicPrompt" | "generateSettings">;
13
+ isContinue: boolean;
14
+ webSearch: boolean;
15
+ toolsPreference: Record<string, boolean>;
16
+ promptedAt: Date;
17
+ }
src/lib/server/tools/calculator.ts ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ToolResultStatus } from "$lib/types/Tool";
2
+ import type { BackendTool } from ".";
3
+ import vm from "node:vm";
4
+
5
+ const calculator: BackendTool = {
6
+ name: "query_calculator",
7
+ displayName: "Calculator",
8
+ description:
9
+ "A simple calculator, takes a string containing a mathematical expression and returns the answer. Only supports +, -, *, ** (power) and /, as well as parenthesis ().",
10
+ isOnByDefault: true,
11
+ parameterDefinitions: {
12
+ equation: {
13
+ description:
14
+ "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.",
15
+ type: "formula",
16
+ required: true,
17
+ },
18
+ },
19
+ async *call(params) {
20
+ try {
21
+ const blocks = String(params.equation).split("\n");
22
+ const query = blocks[blocks.length - 1].replace(/[^-()\d/*+.]/g, "");
23
+
24
+ return {
25
+ status: ToolResultStatus.Success,
26
+ outputs: [{ calculator: `${query} = ${vm.runInNewContext(query)}` }],
27
+ };
28
+ } catch (e) {
29
+ return {
30
+ status: ToolResultStatus.Error,
31
+ message: "Invalid expression",
32
+ };
33
+ }
34
+ },
35
+ };
36
+
37
+ export default calculator;
src/lib/server/tools/directlyAnswer.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ToolResultStatus } from "$lib/types/Tool";
2
+ import type { BackendTool } from ".";
3
+
4
+ const directlyAnswer: BackendTool = {
5
+ name: "directly_answer",
6
+ isOnByDefault: true,
7
+ isHidden: true,
8
+ isLocked: true,
9
+ description: "Use this tool to let the user know you wish to answer directly",
10
+ parameterDefinitions: {},
11
+ async *call() {
12
+ return {
13
+ status: ToolResultStatus.Success,
14
+ outputs: [],
15
+ display: false,
16
+ };
17
+ },
18
+ };
19
+
20
+ export default directlyAnswer;
src/lib/server/tools/documentParser.ts ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { BackendTool } from ".";
2
+ import { ToolResultStatus } from "$lib/types/Tool";
3
+ import { callSpace } from "./utils";
4
+ import { downloadFile } from "$lib/server/files/downloadFile";
5
+
6
+ type PdfParserInput = [Blob /* pdf */, string /* filename */];
7
+ type PdfParserOutput = [string /* markdown */, Record<string, unknown> /* metadata */];
8
+
9
+ const documentParser: BackendTool = {
10
+ name: "document_parser",
11
+ displayName: "Document Parser",
12
+ description: "Use this tool to parse any document and get its content in markdown format.",
13
+ isOnByDefault: true,
14
+ parameterDefinitions: {
15
+ fileMessageIndex: {
16
+ description: "Index of the message containing the document file to parse",
17
+ type: "number",
18
+ required: true,
19
+ },
20
+ fileIndex: {
21
+ description: "Index of the document file to parse",
22
+ type: "number",
23
+ required: true,
24
+ },
25
+ },
26
+ async *call({ fileMessageIndex, fileIndex }, { conv, messages }) {
27
+ fileMessageIndex = Number(fileMessageIndex);
28
+ fileIndex = Number(fileIndex);
29
+
30
+ const message = messages[fileMessageIndex];
31
+ const files = message?.files ?? [];
32
+ if (!files || files.length === 0) {
33
+ return {
34
+ status: ToolResultStatus.Error,
35
+ message: "User did not provide a pdf to parse",
36
+ };
37
+ }
38
+ if (fileIndex >= files.length) {
39
+ return {
40
+ status: ToolResultStatus.Error,
41
+ message: "Model provided an invalid file index",
42
+ };
43
+ }
44
+
45
+ const file = files[fileIndex];
46
+ const fileBlob = await downloadFile(files[fileIndex].value, conv._id)
47
+ .then((file) => fetch(`data:${file.mime};base64,${file.value}`))
48
+ .then((res) => res.blob());
49
+
50
+ const outputs = await callSpace<PdfParserInput, PdfParserOutput>(
51
+ "huggingchat/document-parser",
52
+ "predict",
53
+ [fileBlob, file.name]
54
+ );
55
+
56
+ let documentMarkdown = outputs[0];
57
+ // TODO: quick fix for avoiding context limit. eventually should use the tokenizer
58
+ if (documentMarkdown.length > 30_000) {
59
+ documentMarkdown = documentMarkdown.slice(0, 30_000) + "\n\n... (truncated)";
60
+ }
61
+ return {
62
+ status: ToolResultStatus.Success,
63
+ outputs: [{ [file.name]: documentMarkdown }],
64
+ display: false,
65
+ };
66
+ },
67
+ };
68
+
69
+ export default documentParser;
src/lib/server/tools/images/editing.ts ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { BackendTool } from "..";
2
+ import { uploadFile } from "../../files/uploadFile";
3
+ import { ToolResultStatus } from "$lib/types/Tool";
4
+ import { MessageUpdateType } from "$lib/types/MessageUpdate";
5
+ import { callSpace, type GradioImage } from "../utils";
6
+ import { downloadFile } from "$lib/server/files/downloadFile";
7
+
8
+ type ImageEditingInput = [
9
+ Blob /* image */,
10
+ string /* prompt */,
11
+ string /* negative prompt */,
12
+ number /* guidance scale */,
13
+ number /* steps */
14
+ ];
15
+ type ImageEditingOutput = [GradioImage];
16
+
17
+ const imageEditing: BackendTool = {
18
+ name: "image_editing",
19
+ displayName: "Image Editing",
20
+ description: "Use this tool to edit an image from a prompt.",
21
+ isOnByDefault: true,
22
+ parameterDefinitions: {
23
+ prompt: {
24
+ description:
25
+ "A prompt to generate an image from. Describe the image visually in simple terms, separate terms with a comma.",
26
+ type: "string",
27
+ required: true,
28
+ },
29
+ fileMessageIndex: {
30
+ description: "Index of the message containing the file to edit",
31
+ type: "number",
32
+ required: true,
33
+ },
34
+ fileIndex: {
35
+ description: "Index of the file to edit",
36
+ type: "number",
37
+ required: true,
38
+ },
39
+ },
40
+ async *call({ prompt, fileMessageIndex, fileIndex }, { conv, messages }) {
41
+ prompt = String(prompt);
42
+ fileMessageIndex = Number(fileMessageIndex);
43
+ fileIndex = Number(fileIndex);
44
+
45
+ const message = messages[fileMessageIndex];
46
+ const images = message?.files ?? [];
47
+ if (!images || images.length === 0) {
48
+ return {
49
+ status: ToolResultStatus.Error,
50
+ message: "User did not provide an image to edit.",
51
+ };
52
+ }
53
+ if (fileIndex >= images.length) {
54
+ return {
55
+ status: ToolResultStatus.Error,
56
+ message: "Model provided an invalid file index",
57
+ };
58
+ }
59
+ if (!images[fileIndex].mime.startsWith("image/")) {
60
+ return {
61
+ status: ToolResultStatus.Error,
62
+ message: "Model provided a file indx which is not an image",
63
+ };
64
+ }
65
+
66
+ // todo: should handle multiple images
67
+ const image = await downloadFile(images[fileIndex].value, conv._id)
68
+ .then((file) => fetch(`data:${file.mime};base64,${file.value}`))
69
+ .then((res) => res.blob());
70
+
71
+ const outputs = await callSpace<ImageEditingInput, ImageEditingOutput>(
72
+ "multimodalart/cosxl",
73
+ "run_edit",
74
+ [
75
+ image,
76
+ prompt,
77
+ "", // negative prompt
78
+ 7, // guidance scale
79
+ 20, // steps
80
+ ]
81
+ );
82
+
83
+ const outputImage = await fetch(outputs[0].url)
84
+ .then((res) => res.blob())
85
+ .then((blob) => new File([blob], outputs[0].orig_name, { type: blob.type }))
86
+ .then((file) => uploadFile(file, conv));
87
+
88
+ yield {
89
+ type: MessageUpdateType.File,
90
+ name: outputImage.name,
91
+ sha: outputImage.value,
92
+ mime: outputImage.mime,
93
+ };
94
+
95
+ return {
96
+ status: ToolResultStatus.Success,
97
+ outputs: [
98
+ {
99
+ 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.`,
100
+ },
101
+ ],
102
+ display: false,
103
+ };
104
+ },
105
+ };
106
+
107
+ export default imageEditing;
src/lib/server/tools/images/generation.ts ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { BackendTool } from "..";
2
+ import { uploadFile } from "../../files/uploadFile";
3
+ import { ToolResultStatus } from "$lib/types/Tool";
4
+ import { MessageUpdateType } from "$lib/types/MessageUpdate";
5
+ import { callSpace, type GradioImage } from "../utils";
6
+
7
+ type ImageGenerationInput = [
8
+ number /* number (numeric value between 1 and 8) in 'Number of Images' Slider component */,
9
+ number /* number in 'Image Height' Number component */,
10
+ number /* number in 'Image Width' Number component */,
11
+ string /* prompt */,
12
+ number /* seed random */
13
+ ];
14
+ type ImageGenerationOutput = [{ image: GradioImage }[]];
15
+
16
+ const imageGeneration: BackendTool = {
17
+ name: "image_generation",
18
+ displayName: "Image Generation",
19
+ description: "Use this tool to generate an image from a prompt.",
20
+ isOnByDefault: true,
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
+ numberOfImages: {
29
+ description: "Number of images to generate, between 1 and 8.",
30
+ type: "number",
31
+ required: false,
32
+ default: 1,
33
+ },
34
+ width: {
35
+ description: "Width of the generated image.",
36
+ type: "number",
37
+ required: false,
38
+ default: 1024,
39
+ },
40
+ height: {
41
+ description: "Height of the generated image.",
42
+ type: "number",
43
+ required: false,
44
+ default: 1024,
45
+ },
46
+ },
47
+ async *call({ prompt, numberOfImages }, { conv }) {
48
+ const outputs = await callSpace<ImageGenerationInput, ImageGenerationOutput>(
49
+ "ByteDance/Hyper-SDXL-1Step-T2I",
50
+ "/process_image",
51
+ [
52
+ Number(numberOfImages), // number (numeric value between 1 and 8) in 'Number of Images' Slider component
53
+ 512, // number in 'Image Height' Number component
54
+ 512, // number in 'Image Width' Number component
55
+ String(prompt), // prompt
56
+ Math.floor(Math.random() * 1000), // seed random
57
+ ]
58
+ );
59
+ const imageBlobs = await Promise.all(
60
+ outputs[0].map((output) =>
61
+ fetch(output.image.url)
62
+ .then((res) => res.blob())
63
+ .then(
64
+ (blob) =>
65
+ new File([blob], `${prompt}.${blob.type.split("/")[1] ?? "png"}`, { type: blob.type })
66
+ )
67
+ .then((file) => uploadFile(file, conv))
68
+ )
69
+ );
70
+
71
+ for (const image of imageBlobs) {
72
+ yield {
73
+ type: MessageUpdateType.File,
74
+ name: image.name,
75
+ sha: image.value,
76
+ mime: image.mime,
77
+ };
78
+ }
79
+
80
+ return {
81
+ status: ToolResultStatus.Success,
82
+ outputs: [
83
+ {
84
+ 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.`,
85
+ },
86
+ ],
87
+ display: false,
88
+ };
89
+ },
90
+ };
91
+
92
+ export default imageGeneration;
src/lib/server/tools/index.ts ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Assistant } from "$lib/types/Assistant";
2
+ import type { Conversation } from "$lib/types/Conversation";
3
+ import type { Message } from "$lib/types/Message";
4
+ import type { MessageUpdate } from "$lib/types/MessageUpdate";
5
+ import type { Tool, ToolResult } from "$lib/types/Tool";
6
+
7
+ import calculator from "./calculator";
8
+ import directlyAnswer from "./directlyAnswer";
9
+ import imageEditing from "./images/editing";
10
+ import imageGeneration from "./images/generation";
11
+ import documentParser from "./documentParser";
12
+ import fetchUrl from "./web/url";
13
+ import websearch from "./web/search";
14
+
15
+ export interface BackendToolContext {
16
+ conv: Conversation;
17
+ messages: Message[];
18
+ preprompt?: string;
19
+ assistant?: Pick<Assistant, "rag" | "dynamicPrompt" | "generateSettings">;
20
+ }
21
+
22
+ export interface BackendTool extends Tool {
23
+ call(
24
+ params: Record<string, string | number | boolean>,
25
+ context: BackendToolContext
26
+ ): AsyncGenerator<MessageUpdate, Omit<ToolResult, "call">, undefined>;
27
+ }
28
+
29
+ export const allTools: BackendTool[] = [
30
+ directlyAnswer,
31
+ websearch,
32
+ imageGeneration,
33
+ fetchUrl,
34
+ imageEditing,
35
+ documentParser,
36
+ calculator,
37
+ ];
src/lib/server/tools/utils.ts ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { env } from "$env/dynamic/private";
2
+ import { Client } from "@gradio/client";
3
+
4
+ export type GradioImage = {
5
+ path: string;
6
+ url: string;
7
+ orig_name: string;
8
+ is_stream: boolean;
9
+ meta: Record<string, unknown>;
10
+ };
11
+
12
+ type GradioResponse = {
13
+ data: unknown[];
14
+ };
15
+
16
+ export async function callSpace<TInput extends unknown[], TOutput extends unknown[]>(
17
+ name: string,
18
+ func: string,
19
+ parameters: TInput
20
+ ): Promise<TOutput> {
21
+ const client = await Client.connect(name, {
22
+ hf_token: (env.HF_TOKEN ?? env.HF_ACCESS_TOKEN) as unknown as `hf_${string}`,
23
+ });
24
+ return await client
25
+ .predict(func, parameters)
26
+ .then((res) => (res as unknown as GradioResponse).data as TOutput);
27
+ }
28
+
29
+ export { toolHasName } from "$lib/utils/tools";
src/lib/server/tools/web/search.ts ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ToolResultStatus } from "$lib/types/Tool";
2
+ import type { BackendTool } from "..";
3
+ import { runWebSearch } from "../../websearch/runWebSearch";
4
+
5
+ const websearch: BackendTool = {
6
+ name: "websearch",
7
+ displayName: "Web Search",
8
+ isOnByDefault: true,
9
+ description:
10
+ "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.",
11
+ parameterDefinitions: {
12
+ query: {
13
+ required: true,
14
+ type: "string",
15
+ description:
16
+ "A search query which will be used to fetch the most relevant snippets regarding the user's query",
17
+ },
18
+ },
19
+ async *call({ query }, { conv, assistant, messages }) {
20
+ const webSearchToolResults = yield* runWebSearch(conv, messages, assistant?.rag, String(query));
21
+ const chunks = webSearchToolResults?.contextSources
22
+ .map(({ context }) => context)
23
+ .join("\n------------\n");
24
+
25
+ return {
26
+ status: ToolResultStatus.Success,
27
+ outputs: [{ websearch: chunks }],
28
+ display: false,
29
+ };
30
+ },
31
+ };
32
+
33
+ export default websearch;
src/lib/server/tools/web/url.ts ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { stringifyMarkdownElementTree } from "$lib/server/websearch/markdown/utils/stringify";
2
+ import { scrapeUrl } from "$lib/server/websearch/scrape/scrape";
3
+ import { ToolResultStatus } from "$lib/types/Tool";
4
+ import type { BackendTool } from "..";
5
+
6
+ const fetchUrl: BackendTool = {
7
+ name: "fetch_url",
8
+ displayName: "URL Fetcher",
9
+ description: "A tool that can be used to fetch an URL and return the content directly.",
10
+ isOnByDefault: true,
11
+ parameterDefinitions: {
12
+ url: {
13
+ description: "The url that should be fetched.",
14
+ type: "str",
15
+ required: true,
16
+ },
17
+ },
18
+ async *call(params) {
19
+ const blocks = String(params.url).split("\n");
20
+ const url = blocks[blocks.length - 1];
21
+
22
+ const { title, markdownTree } = await scrapeUrl(url, Infinity);
23
+
24
+ return {
25
+ status: ToolResultStatus.Success,
26
+ outputs: [{ title, text: stringifyMarkdownElementTree(markdownTree) }],
27
+ display: false,
28
+ };
29
+ },
30
+ };
31
+
32
+ export default fetchUrl;
src/lib/server/websearch/markdown/utils/stringify.ts CHANGED
@@ -26,6 +26,13 @@ export function stringifyMarkdownElement(elem: MarkdownElement): string {
26
  return `${content}\n\n`;
27
  }
28
 
 
 
 
 
 
 
 
29
  // ----- HTML Elements -----
30
 
31
  /** Ignores all non-inline tag types and grabs their text. Converts inline tags to markdown */
 
26
  return `${content}\n\n`;
27
  }
28
 
29
+ /** Converts a tree of markdown elements to a string with formatting */
30
+ export function stringifyMarkdownElementTree(elem: MarkdownElement): string {
31
+ const stringified = stringifyMarkdownElement(elem);
32
+ if (!("children" in elem)) return stringified;
33
+ return stringified + elem.children.map(stringifyMarkdownElementTree).join("");
34
+ }
35
+
36
  // ----- HTML Elements -----
37
 
38
  /** Ignores all non-inline tag types and grabs their text. Converts inline tags to markdown */
src/lib/server/websearch/runWebSearch.ts CHANGED
@@ -1,35 +1,35 @@
1
  import { defaultEmbeddingModel, embeddingModels } from "$lib/server/embeddingModels";
2
 
3
  import type { Conversation } from "$lib/types/Conversation";
4
- import type { MessageUpdate } from "$lib/types/MessageUpdate";
5
  import type { Message } from "$lib/types/Message";
6
  import type { WebSearch, WebSearchScrapedSource } from "$lib/types/WebSearch";
7
  import type { Assistant } from "$lib/types/Assistant";
 
8
 
9
  import { search } from "./search/search";
10
  import { scrape } from "./scrape/scrape";
11
  import { findContextSources } from "./embed/embed";
12
  import { removeParents } from "./markdown/tree";
 
 
 
 
 
 
 
13
 
14
  const MAX_N_PAGES_TO_SCRAPE = 8 as const;
15
  const MAX_N_PAGES_TO_EMBED = 5 as const;
16
 
17
- export type AppendUpdate = (message: string, args?: string[], type?: "error" | "update") => void;
18
- const makeAppendUpdate =
19
- (updatePad: (upd: MessageUpdate) => void): AppendUpdate =>
20
- (message, args, type) =>
21
- updatePad({ type: "webSearch", messageType: type ?? "update", message, args });
22
-
23
- export async function runWebSearch(
24
  conv: Conversation,
25
  messages: Message[],
26
- updatePad: (upd: MessageUpdate) => void,
27
- ragSettings?: Assistant["rag"]
28
- ): Promise<WebSearch> {
29
  const prompt = messages[messages.length - 1].content;
30
  const createdAt = new Date();
31
  const updatedAt = new Date();
32
- const appendUpdate = makeAppendUpdate(updatePad);
33
 
34
  try {
35
  const embeddingModel =
@@ -39,29 +39,26 @@ export async function runWebSearch(
39
  }
40
 
41
  // Search the web
42
- const { searchQuery, pages } = await search(messages, ragSettings, appendUpdate);
43
  if (pages.length === 0) throw Error("No results found for this search query");
44
 
45
  // Scrape pages
46
- appendUpdate("Browsing search results");
47
 
48
- const scrapedPages = await Promise.all(
49
- pages
50
- .slice(0, MAX_N_PAGES_TO_SCRAPE)
51
- .map(scrape(appendUpdate, embeddingModel.chunkCharLength))
52
- ).then((allScrapedPages) =>
53
- allScrapedPages
54
- .filter((p): p is WebSearchScrapedSource => Boolean(p))
55
- .filter((p) => p.page.markdownTree.children.length > 0)
56
- .slice(0, MAX_N_PAGES_TO_EMBED)
57
  );
 
 
 
 
58
 
59
  if (!scrapedPages.length) {
60
  throw Error(`No text found in the first ${MAX_N_PAGES_TO_SCRAPE} results`);
61
  }
62
 
63
  // Chunk the text of each of the elements and find the most similar chunks to the prompt
64
- appendUpdate("Extracting relevant information");
65
  const contextSources = await findContextSources(scrapedPages, prompt, embeddingModel).then(
66
  (ctxSources) =>
67
  ctxSources.map((source) => ({
@@ -69,14 +66,9 @@ export async function runWebSearch(
69
  page: { ...source.page, markdownTree: removeParents(source.page.markdownTree) },
70
  }))
71
  );
72
- updatePad({
73
- type: "webSearch",
74
- messageType: "sources",
75
- message: "sources",
76
- sources: contextSources,
77
- });
78
 
79
- return {
80
  prompt,
81
  searchQuery,
82
  results: scrapedPages.map(({ page, ...source }) => ({
@@ -87,11 +79,14 @@ export async function runWebSearch(
87
  createdAt,
88
  updatedAt,
89
  };
 
 
90
  } catch (searchError) {
91
  const message = searchError instanceof Error ? searchError.message : String(searchError);
92
  console.error(message);
93
- appendUpdate("An error occurred", [JSON.stringify(message)], "error");
94
- return {
 
95
  prompt,
96
  searchQuery: "",
97
  results: [],
@@ -99,5 +94,7 @@ export async function runWebSearch(
99
  createdAt,
100
  updatedAt,
101
  };
 
 
102
  }
103
  }
 
1
  import { defaultEmbeddingModel, embeddingModels } from "$lib/server/embeddingModels";
2
 
3
  import type { Conversation } from "$lib/types/Conversation";
 
4
  import type { Message } from "$lib/types/Message";
5
  import type { WebSearch, WebSearchScrapedSource } from "$lib/types/WebSearch";
6
  import type { Assistant } from "$lib/types/Assistant";
7
+ import type { MessageWebSearchUpdate } from "$lib/types/MessageUpdate";
8
 
9
  import { search } from "./search/search";
10
  import { scrape } from "./scrape/scrape";
11
  import { findContextSources } from "./embed/embed";
12
  import { removeParents } from "./markdown/tree";
13
+ import {
14
+ makeErrorUpdate,
15
+ makeFinalAnswerUpdate,
16
+ makeGeneralUpdate,
17
+ makeSourcesUpdate,
18
+ } from "./update";
19
+ import { mergeAsyncGenerators } from "$lib/utils/mergeAsyncGenerators";
20
 
21
  const MAX_N_PAGES_TO_SCRAPE = 8 as const;
22
  const MAX_N_PAGES_TO_EMBED = 5 as const;
23
 
24
+ export async function* runWebSearch(
 
 
 
 
 
 
25
  conv: Conversation,
26
  messages: Message[],
27
+ ragSettings?: Assistant["rag"],
28
+ query?: string
29
+ ): AsyncGenerator<MessageWebSearchUpdate, WebSearch, undefined> {
30
  const prompt = messages[messages.length - 1].content;
31
  const createdAt = new Date();
32
  const updatedAt = new Date();
 
33
 
34
  try {
35
  const embeddingModel =
 
39
  }
40
 
41
  // Search the web
42
+ const { searchQuery, pages } = yield* search(messages, ragSettings, query);
43
  if (pages.length === 0) throw Error("No results found for this search query");
44
 
45
  // Scrape pages
46
+ yield makeGeneralUpdate({ message: "Browsing search results" });
47
 
48
+ const allScrapedPages = yield* mergeAsyncGenerators(
49
+ pages.slice(0, MAX_N_PAGES_TO_SCRAPE).map(scrape(embeddingModel.chunkCharLength))
 
 
 
 
 
 
 
50
  );
51
+ const scrapedPages = allScrapedPages
52
+ .filter((p): p is WebSearchScrapedSource => Boolean(p))
53
+ .filter((p) => p.page.markdownTree.children.length > 0)
54
+ .slice(0, MAX_N_PAGES_TO_EMBED);
55
 
56
  if (!scrapedPages.length) {
57
  throw Error(`No text found in the first ${MAX_N_PAGES_TO_SCRAPE} results`);
58
  }
59
 
60
  // Chunk the text of each of the elements and find the most similar chunks to the prompt
61
+ yield makeGeneralUpdate({ message: "Extracting relevant information" });
62
  const contextSources = await findContextSources(scrapedPages, prompt, embeddingModel).then(
63
  (ctxSources) =>
64
  ctxSources.map((source) => ({
 
66
  page: { ...source.page, markdownTree: removeParents(source.page.markdownTree) },
67
  }))
68
  );
69
+ yield makeSourcesUpdate(contextSources);
 
 
 
 
 
70
 
71
+ const webSearch: WebSearch = {
72
  prompt,
73
  searchQuery,
74
  results: scrapedPages.map(({ page, ...source }) => ({
 
79
  createdAt,
80
  updatedAt,
81
  };
82
+ yield makeFinalAnswerUpdate(webSearch);
83
+ return webSearch;
84
  } catch (searchError) {
85
  const message = searchError instanceof Error ? searchError.message : String(searchError);
86
  console.error(message);
87
+ yield makeErrorUpdate({ message: "An error occurred", args: [message] });
88
+
89
+ const webSearch: WebSearch = {
90
  prompt,
91
  searchQuery: "",
92
  results: [],
 
94
  createdAt,
95
  updatedAt,
96
  };
97
+ yield makeFinalAnswerUpdate(webSearch);
98
+ return webSearch;
99
  }
100
  }
src/lib/server/websearch/scrape/playwright.ts CHANGED
@@ -4,6 +4,7 @@ import {
4
  devices,
5
  type Page,
6
  type BrowserContextOptions,
 
7
  } from "playwright";
8
  import { PlaywrightBlocker } from "@cliqz/adblocker-playwright";
9
  import { env } from "$env/dynamic/private";
@@ -44,16 +45,16 @@ async function initPlaywrightService() {
44
  return Object.freeze({ ctx, blocker });
45
  }
46
 
47
- export async function loadPage(url: string): Promise<Page> {
48
  if (!playwrightService) playwrightService = initPlaywrightService();
49
  const { ctx, blocker } = await playwrightService;
50
 
51
  const page = await ctx.newPage();
52
  await blocker.enableBlockingInPage(page);
53
 
54
- await page.goto(url, { waitUntil: "load", timeout: 2000 }).catch(() => {
55
  console.warn(`Failed to load page within 2s: ${url}`);
56
  });
57
 
58
- return page;
59
  }
 
4
  devices,
5
  type Page,
6
  type BrowserContextOptions,
7
+ type Response,
8
  } from "playwright";
9
  import { PlaywrightBlocker } from "@cliqz/adblocker-playwright";
10
  import { env } from "$env/dynamic/private";
 
45
  return Object.freeze({ ctx, blocker });
46
  }
47
 
48
+ export async function loadPage(url: string): Promise<{ res?: Response; page: Page }> {
49
  if (!playwrightService) playwrightService = initPlaywrightService();
50
  const { ctx, blocker } = await playwrightService;
51
 
52
  const page = await ctx.newPage();
53
  await blocker.enableBlockingInPage(page);
54
 
55
+ const res = await page.goto(url, { waitUntil: "load", timeout: 3500 }).catch(() => {
56
  console.warn(`Failed to load page within 2s: ${url}`);
57
  });
58
 
59
+ return { res: res ?? undefined, page };
60
  }
src/lib/server/websearch/scrape/scrape.ts CHANGED
@@ -1,26 +1,52 @@
1
- import type { AppendUpdate } from "../runWebSearch";
2
  import type { WebSearchScrapedSource, WebSearchSource } from "$lib/types/WebSearch";
 
3
  import { loadPage } from "./playwright";
4
 
5
  import { spatialParser } from "./parser";
6
  import { htmlToMarkdownTree } from "../markdown/tree";
7
  import { timeout } from "$lib/utils/timeout";
 
8
 
9
- export const scrape =
10
- (appendUpdate: AppendUpdate, maxCharsPerElem: number) =>
11
- async (source: WebSearchSource): Promise<WebSearchScrapedSource | undefined> => {
 
12
  try {
13
  const page = await scrapeUrl(source.link, maxCharsPerElem);
14
- appendUpdate("Browsing webpage", [source.link]);
15
  return { ...source, page };
16
  } catch (e) {
17
  const message = e instanceof Error ? e.message : String(e);
18
- appendUpdate("Failed to parse webpage", [message, source.link], "error");
19
  }
20
  };
21
 
22
  export async function scrapeUrl(url: string, maxCharsPerElem: number) {
23
- const page = await loadPage(url);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
  return timeout(page.evaluate(spatialParser), 2000)
26
  .then(({ elements, ...parsed }) => ({
 
 
1
  import type { WebSearchScrapedSource, WebSearchSource } from "$lib/types/WebSearch";
2
+ import type { MessageWebSearchUpdate } from "$lib/types/MessageUpdate";
3
  import { loadPage } from "./playwright";
4
 
5
  import { spatialParser } from "./parser";
6
  import { htmlToMarkdownTree } from "../markdown/tree";
7
  import { timeout } from "$lib/utils/timeout";
8
+ import { makeErrorUpdate, makeGeneralUpdate } from "../update";
9
 
10
+ export const scrape = (maxCharsPerElem: number) =>
11
+ async function* (
12
+ source: WebSearchSource
13
+ ): AsyncGenerator<MessageWebSearchUpdate, WebSearchScrapedSource | undefined, undefined> {
14
  try {
15
  const page = await scrapeUrl(source.link, maxCharsPerElem);
16
+ yield makeGeneralUpdate({ message: "Browsing webpage", args: [source.link] });
17
  return { ...source, page };
18
  } catch (e) {
19
  const message = e instanceof Error ? e.message : String(e);
20
+ yield makeErrorUpdate({ message: "Failed to parse webpage", args: [message, source.link] });
21
  }
22
  };
23
 
24
  export async function scrapeUrl(url: string, maxCharsPerElem: number) {
25
+ const { res, page } = await loadPage(url);
26
+
27
+ if (!res) throw Error("Failed to load page");
28
+
29
+ // Check if it's a non-html content type that we can handle directly
30
+ // TODO: direct mappings to markdown can be added for markdown, csv and others
31
+ const contentType = res.headers()["content-type"] ?? "";
32
+ if (
33
+ contentType.includes("text/plain") ||
34
+ contentType.includes("text/markdown") ||
35
+ contentType.includes("application/json") ||
36
+ contentType.includes("application/xml") ||
37
+ contentType.includes("text/csv")
38
+ ) {
39
+ const title = await page.title();
40
+ const content = await page.content();
41
+ return {
42
+ title,
43
+ markdownTree: htmlToMarkdownTree(
44
+ title,
45
+ [{ tagName: "p", attributes: {}, content: [content] }],
46
+ maxCharsPerElem
47
+ ),
48
+ };
49
+ }
50
 
51
  return timeout(page.evaluate(spatialParser), 2000)
52
  .then(({ elements, ...parsed }) => ({
src/lib/server/websearch/search/search.ts CHANGED
@@ -1,7 +1,6 @@
1
  import type { WebSearchSource } from "$lib/types/WebSearch";
2
  import type { Message } from "$lib/types/Message";
3
  import type { Assistant } from "$lib/types/Assistant";
4
- import type { AppendUpdate } from "../runWebSearch";
5
  import { getWebSearchProvider, searchWeb } from "./endpoints";
6
  import { generateQuery } from "./generateQuery";
7
  import { isURLStringLocal } from "$lib/server/isURLLocal";
@@ -10,30 +9,36 @@ import { isURL } from "$lib/utils/isUrl";
10
  import z from "zod";
11
  import JSON5 from "json5";
12
  import { env } from "$env/dynamic/private";
 
 
13
 
14
  const listSchema = z.array(z.string()).default([]);
15
  const allowList = listSchema.parse(JSON5.parse(env.WEBSEARCH_ALLOWLIST));
16
  const blockList = listSchema.parse(JSON5.parse(env.WEBSEARCH_BLOCKLIST));
17
 
18
- export async function search(
19
  messages: Message[],
20
- ragSettings: Assistant["rag"] | undefined,
21
- appendUpdate: AppendUpdate
22
- ): Promise<{ searchQuery: string; pages: WebSearchSource[] }> {
 
 
 
 
23
  if (ragSettings && ragSettings?.allowedLinks.length > 0) {
24
- appendUpdate("Using links specified in Assistant");
25
  return {
26
  searchQuery: "",
27
  pages: await directLinksToSource(ragSettings.allowedLinks).then(filterByBlockList),
28
  };
29
  }
30
 
31
- const searchQuery = await generateQuery(messages);
32
- appendUpdate(`Searching ${getWebSearchProvider()}`, [searchQuery]);
33
 
34
  // handle the global and (optional) rag lists
35
  if (ragSettings && ragSettings?.allowedDomains.length > 0) {
36
- appendUpdate("Filtering on specified domains");
37
  }
38
  const filters = buildQueryFromSiteFilters(
39
  [...(ragSettings?.allowedDomains ?? []), ...allowList],
 
1
  import type { WebSearchSource } from "$lib/types/WebSearch";
2
  import type { Message } from "$lib/types/Message";
3
  import type { Assistant } from "$lib/types/Assistant";
 
4
  import { getWebSearchProvider, searchWeb } from "./endpoints";
5
  import { generateQuery } from "./generateQuery";
6
  import { isURLStringLocal } from "$lib/server/isURLLocal";
 
9
  import z from "zod";
10
  import JSON5 from "json5";
11
  import { env } from "$env/dynamic/private";
12
+ import { makeGeneralUpdate } from "../update";
13
+ import type { MessageWebSearchUpdate } from "$lib/types/MessageUpdate";
14
 
15
  const listSchema = z.array(z.string()).default([]);
16
  const allowList = listSchema.parse(JSON5.parse(env.WEBSEARCH_ALLOWLIST));
17
  const blockList = listSchema.parse(JSON5.parse(env.WEBSEARCH_BLOCKLIST));
18
 
19
+ export async function* search(
20
  messages: Message[],
21
+ ragSettings?: Assistant["rag"],
22
+ query?: string
23
+ ): AsyncGenerator<
24
+ MessageWebSearchUpdate,
25
+ { searchQuery: string; pages: WebSearchSource[] },
26
+ undefined
27
+ > {
28
  if (ragSettings && ragSettings?.allowedLinks.length > 0) {
29
+ yield makeGeneralUpdate({ message: "Using links specified in Assistant" });
30
  return {
31
  searchQuery: "",
32
  pages: await directLinksToSource(ragSettings.allowedLinks).then(filterByBlockList),
33
  };
34
  }
35
 
36
+ const searchQuery = query ?? (await generateQuery(messages));
37
+ yield makeGeneralUpdate({ message: `Searching ${getWebSearchProvider()}`, args: [searchQuery] });
38
 
39
  // handle the global and (optional) rag lists
40
  if (ragSettings && ragSettings?.allowedDomains.length > 0) {
41
+ yield makeGeneralUpdate({ message: "Filtering on specified domains" });
42
  }
43
  const filters = buildQueryFromSiteFilters(
44
  [...(ragSettings?.allowedDomains ?? []), ...allowList],
src/lib/server/websearch/update.ts ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { WebSearch, WebSearchSource } from "$lib/types/WebSearch";
2
+ import {
3
+ MessageUpdateType,
4
+ MessageWebSearchUpdateType,
5
+ type MessageWebSearchErrorUpdate,
6
+ type MessageWebSearchFinishedUpdate,
7
+ type MessageWebSearchGeneralUpdate,
8
+ type MessageWebSearchSourcesUpdate,
9
+ } from "$lib/types/MessageUpdate";
10
+
11
+ export function makeGeneralUpdate(
12
+ update: Pick<MessageWebSearchGeneralUpdate, "message" | "args">
13
+ ): MessageWebSearchGeneralUpdate {
14
+ return {
15
+ type: MessageUpdateType.WebSearch,
16
+ subtype: MessageWebSearchUpdateType.Update,
17
+ ...update,
18
+ };
19
+ }
20
+
21
+ export function makeErrorUpdate(
22
+ update: Pick<MessageWebSearchErrorUpdate, "message" | "args">
23
+ ): MessageWebSearchErrorUpdate {
24
+ return {
25
+ type: MessageUpdateType.WebSearch,
26
+ subtype: MessageWebSearchUpdateType.Error,
27
+ ...update,
28
+ };
29
+ }
30
+
31
+ export function makeSourcesUpdate(sources: WebSearchSource[]): MessageWebSearchSourcesUpdate {
32
+ return {
33
+ type: MessageUpdateType.WebSearch,
34
+ subtype: MessageWebSearchUpdateType.Sources,
35
+ message: "sources",
36
+ sources,
37
+ };
38
+ }
39
+
40
+ export function makeFinalAnswerUpdate(webSearch: WebSearch): MessageWebSearchFinishedUpdate {
41
+ return {
42
+ type: MessageUpdateType.WebSearch,
43
+ subtype: MessageWebSearchUpdateType.Finished,
44
+ webSearch,
45
+ };
46
+ }
src/lib/stores/settings.ts CHANGED
@@ -15,6 +15,7 @@ type SettingsStore = {
15
  customPrompts: Record<string, string>;
16
  recentlySaved: boolean;
17
  assistants: Array<ObjectId | string>;
 
18
  };
19
 
20
  type SettingsStoreWritable = Writable<SettingsStore> & {
 
15
  customPrompts: Record<string, string>;
16
  recentlySaved: boolean;
17
  assistants: Array<ObjectId | string>;
18
+ tools?: Record<string, boolean>;
19
  };
20
 
21
  type SettingsStoreWritable = Writable<SettingsStore> & {
src/lib/types/Message.ts CHANGED
@@ -27,6 +27,7 @@ export type Message = Partial<Timestamps> & {
27
 
28
  export type MessageFile = {
29
  type: "hash" | "base64";
 
30
  value: string;
31
  mime: string;
32
  };
 
27
 
28
  export type MessageFile = {
29
  type: "hash" | "base64";
30
+ name: string;
31
  value: string;
32
  mime: string;
33
  };
src/lib/types/MessageUpdate.ts CHANGED
@@ -1,46 +1,111 @@
1
- import type { WebSearchSource } from "./WebSearch";
 
2
 
3
- export type FinalAnswer = {
4
- type: "finalAnswer";
5
- text: string;
6
- };
 
 
 
 
7
 
8
- export type TextStreamUpdate = {
9
- type: "stream";
10
- token: string;
11
- };
 
 
 
 
 
12
 
13
- export type AgentUpdate = {
14
- type: "agent";
15
- agent: string;
16
- content: string;
17
- binary?: Blob;
18
- };
 
 
 
 
 
19
 
20
- export type WebSearchUpdate = {
21
- type: "webSearch";
22
- messageType: "update" | "error" | "sources";
 
 
 
 
 
 
 
 
 
 
23
  message: string;
24
  args?: string[];
25
- sources?: WebSearchSource[];
26
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
- export type StatusUpdate = {
29
- type: "status";
30
- status: "started" | "pending" | "finished" | "error" | "title";
31
- message?: string;
32
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
- export type ErrorUpdate = {
35
- type: "error";
36
- message: string;
 
 
 
 
 
 
 
 
37
  name: string;
38
- };
39
-
40
- export type MessageUpdate =
41
- | FinalAnswer
42
- | TextStreamUpdate
43
- | AgentUpdate
44
- | WebSearchUpdate
45
- | StatusUpdate
46
- | ErrorUpdate;
 
1
+ import type { WebSearch, WebSearchSource } from "$lib/types/WebSearch";
2
+ import type { ToolCall, ToolResult } from "$lib/types/Tool";
3
 
4
+ export type MessageUpdate =
5
+ | MessageStatusUpdate
6
+ | MessageTitleUpdate
7
+ | MessageToolUpdate
8
+ | MessageWebSearchUpdate
9
+ | MessageStreamUpdate
10
+ | MessageFileUpdate
11
+ | MessageFinalAnswerUpdate;
12
 
13
+ export enum MessageUpdateType {
14
+ Status = "status",
15
+ Title = "title",
16
+ Tool = "tool",
17
+ WebSearch = "webSearch",
18
+ Stream = "stream",
19
+ File = "file",
20
+ FinalAnswer = "finalAnswer",
21
+ }
22
 
23
+ // Status
24
+ export enum MessageUpdateStatus {
25
+ Started = "started",
26
+ Error = "error",
27
+ Finished = "finished",
28
+ }
29
+ export interface MessageStatusUpdate {
30
+ type: MessageUpdateType.Status;
31
+ status: MessageUpdateStatus;
32
+ message?: string;
33
+ }
34
 
35
+ // Web search
36
+ export enum MessageWebSearchUpdateType {
37
+ Update = "update",
38
+ Error = "error",
39
+ Sources = "sources",
40
+ Finished = "finished",
41
+ }
42
+ interface BaseMessageWebSearchUpdate<TSubType extends MessageWebSearchUpdateType> {
43
+ type: MessageUpdateType.WebSearch;
44
+ subtype: TSubType;
45
+ }
46
+ export interface MessageWebSearchErrorUpdate
47
+ extends BaseMessageWebSearchUpdate<MessageWebSearchUpdateType.Error> {
48
  message: string;
49
  args?: string[];
50
+ }
51
+ export interface MessageWebSearchGeneralUpdate
52
+ extends BaseMessageWebSearchUpdate<MessageWebSearchUpdateType.Update> {
53
+ message: string;
54
+ args?: string[];
55
+ }
56
+ export interface MessageWebSearchSourcesUpdate
57
+ extends BaseMessageWebSearchUpdate<MessageWebSearchUpdateType.Sources> {
58
+ message: string;
59
+ sources: WebSearchSource[];
60
+ }
61
+ export interface MessageWebSearchFinishedUpdate
62
+ extends BaseMessageWebSearchUpdate<MessageWebSearchUpdateType.Finished> {
63
+ webSearch: WebSearch;
64
+ }
65
+ export type MessageWebSearchUpdate =
66
+ | MessageWebSearchErrorUpdate
67
+ | MessageWebSearchGeneralUpdate
68
+ | MessageWebSearchSourcesUpdate
69
+ | MessageWebSearchFinishedUpdate;
70
 
71
+ // Tool
72
+ export enum MessageToolUpdateType {
73
+ /** A request to call a tool alongside it's parameters */
74
+ Call = "call",
75
+ /** The result of a tool call */
76
+ Result = "result",
77
+ }
78
+ interface MessageToolBaseUpdate<TSubType extends MessageToolUpdateType> {
79
+ type: MessageUpdateType.Tool;
80
+ subtype: TSubType;
81
+ uuid: string;
82
+ }
83
+ export interface MessageToolCallUpdate extends MessageToolBaseUpdate<MessageToolUpdateType.Call> {
84
+ call: ToolCall;
85
+ }
86
+ export interface MessageToolResultUpdate
87
+ extends MessageToolBaseUpdate<MessageToolUpdateType.Result> {
88
+ result: ToolResult;
89
+ }
90
+ export type MessageToolUpdate = MessageToolCallUpdate | MessageToolResultUpdate;
91
 
92
+ // Everything else
93
+ export interface MessageTitleUpdate {
94
+ type: MessageUpdateType.Title;
95
+ title: string;
96
+ }
97
+ export interface MessageStreamUpdate {
98
+ type: MessageUpdateType.Stream;
99
+ token: string;
100
+ }
101
+ export interface MessageFileUpdate {
102
+ type: MessageUpdateType.File;
103
  name: string;
104
+ sha: string;
105
+ mime: string;
106
+ }
107
+ export interface MessageFinalAnswerUpdate {
108
+ type: MessageUpdateType.FinalAnswer;
109
+ text: string;
110
+ interrupted: boolean;
111
+ }
 
src/lib/types/Model.ts CHANGED
@@ -17,4 +17,5 @@ export type Model = Pick<
17
  | "preprompt"
18
  | "multimodal"
19
  | "unlisted"
 
20
  >;
 
17
  | "preprompt"
18
  | "multimodal"
19
  | "unlisted"
20
+ | "tools"
21
  >;
src/lib/types/Settings.ts CHANGED
@@ -21,6 +21,7 @@ export interface Settings extends Timestamps {
21
  customPrompts?: Record<string, string>;
22
 
23
  assistants?: Assistant["_id"][];
 
24
  }
25
 
26
  // TODO: move this to a constant file along with other constants
@@ -30,4 +31,5 @@ export const DEFAULT_SETTINGS = {
30
  hideEmojiOnSidebar: false,
31
  customPrompts: {},
32
  assistants: [],
 
33
  };
 
21
  customPrompts?: Record<string, string>;
22
 
23
  assistants?: Assistant["_id"][];
24
+ tools?: Record<string, boolean>;
25
  }
26
 
27
  // TODO: move this to a constant file along with other constants
 
31
  hideEmojiOnSidebar: false,
32
  customPrompts: {},
33
  assistants: [],
34
+ tools: {},
35
  };