diff --git a/.env b/.env index 203f353412a18af8116d62ab4ae692ad43b02af9..cb79087b5a461b0083103b0946b271f02c46ca3c 100644 --- a/.env +++ b/.env @@ -154,6 +154,7 @@ WEBHOOK_URL_REPORT_ASSISTANT=#provide webhook url to get notified when an assist ALLOWED_USER_EMAILS=`[]` # if it's defined, only these emails will be allowed to use the app USAGE_LIMITS=`{}` + ALLOW_INSECURE_COOKIES=false # recommended to keep this to false but set to true if you need to run over http without tls METRICS_PORT= LOG_LEVEL=info diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 1590a8c06d47d8ebc8c457d94c309e7e39ba3489..b1aeb0ef85fb179c5b2cdc5c4aa684c4d6700c70 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -24,7 +24,7 @@ module.exports = { extraFileExtensions: [".svelte"], }, rules: { - "no-shadow": ["error"], + "require-yield": "off", "@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-non-null-assertion": "error", "@typescript-eslint/no-unused-vars": [ diff --git a/chart/env/prod.yaml b/chart/env/prod.yaml index c1e47387611ac30d2ab4a21904b4608285667412..0652fbf4f05e01643e233b66cc522dea33a59b93 100644 --- a/chart/env/prod.yaml +++ b/chart/env/prod.yaml @@ -39,6 +39,7 @@ envVars: "modelUrl": "https://huggingface.co/CohereForAI/c4ai-command-r-plus", "websiteUrl": "https://docs.cohere.com/docs/command-r-plus", "logoUrl": "https://huggingface.co/datasets/huggingchat/models-logo/resolve/main/cohere-logo.png", + "tools": true, "parameters": { "stop": ["<|END_OF_TURN_TOKEN|>"], "truncate" : 28672, @@ -47,20 +48,20 @@ envVars: }, "promptExamples" : [ { - "title": "Write an email from bullet list", - "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)" + "title": "Generate a mouse portrait", + "prompt": "Generate the portrait of a scientific mouse in its laboratory." + }, { + "title": "Review a pull request", + "prompt": "Review this pull request: https://github.com/huggingface/chat-ui/pull/1131/files" }, { "title": "Code a snake game", "prompt": "Code a basic snake game in python, give explanations for each step." - }, { - "title": "Assist in a task", - "prompt": "How do I make a delicious lemon cheesecake?" } ] }, { "name" : "meta-llama/Meta-Llama-3-70B-Instruct", - "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.", + "description": "Meta Llama 3 delivers top performance on various benchmarks and introduces new features like better reasoning.", "logoUrl": "https://huggingface.co/datasets/huggingchat/models-logo/resolve/main/meta-logo.png", "modelUrl": "https://huggingface.co/meta-llama/Meta-Llama-3-70B-Instruct", "websiteUrl": "https://llama.meta.com/llama3/", diff --git a/package-lock.json b/package-lock.json index b82e2f623ad5bc352abd1deb682d27dd885114cc..5df45d978d283451b2e00db8cf804b4fb9440f20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,9 @@ "version": "0.8.4", "dependencies": { "@cliqz/adblocker-playwright": "^1.27.2", + "@gradio/client": "^0.19.4", "@huggingface/hub": "^0.5.1", - "@huggingface/inference": "^2.6.3", + "@huggingface/inference": "^2.7.0", "@iconify-json/bi": "^1.1.21", "@playwright/browser-chromium": "^1.43.1", "@resvg/resvg-js": "^2.6.2", @@ -42,7 +43,7 @@ "satori-html": "^0.3.2", "sbd": "^1.0.19", "serpapi": "^1.1.1", - "sharp": "^0.33.3", + "sharp": "^0.33.4", "tailwind-scrollbar": "^3.0.0", "tailwindcss": "^3.4.0", "uuid": "^9.0.1", @@ -174,6 +175,22 @@ "google-auth-library": "^9.4.2" } }, + "node_modules/@bundled-es-modules/cookie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.0.tgz", + "integrity": "sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==", + "dependencies": { + "cookie": "^0.5.0" + } + }, + "node_modules/@bundled-es-modules/statuses": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz", + "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==", + "dependencies": { + "statuses": "^2.0.1" + } + }, "node_modules/@cliqz/adblocker": { "version": "1.27.2", "resolved": "https://registry.npmjs.org/@cliqz/adblocker/-/adblocker-1.27.2.tgz", @@ -706,6 +723,23 @@ "node": ">=18.0.0" } }, + "node_modules/@gradio/client": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@gradio/client/-/client-0.19.4.tgz", + "integrity": "sha512-O7bSkgoGL7Fe180UTB9IF9ZlIMdiIhWkInE22NDCsH3Lyt7QdOxDSRKZUEYTPELUIBrejWiuQ1ADNJQzDTBvBg==", + "dependencies": { + "@types/eventsource": "^1.1.15", + "bufferutil": "^4.0.7", + "eventsource": "^2.0.2", + "msw": "^2.2.1", + "semiver": "^1.1.0", + "typescript": "^5.0.0", + "ws": "^8.13.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@huggingface/hub": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/@huggingface/hub/-/hub-0.5.1.tgz", @@ -718,9 +752,12 @@ } }, "node_modules/@huggingface/inference": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/@huggingface/inference/-/inference-2.6.3.tgz", - "integrity": "sha512-KK6xNrEldjjopiGqwaBCkA+4tEyuIz0qHsD5SVYaQ65HSlmBbntJieSw4NRWT+S5bK/Bf/GFCixW0NshAOcBqA==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@huggingface/inference/-/inference-2.7.0.tgz", + "integrity": "sha512-u7Fn637Q3f7nUB1tajM4CgzhvoFQkOQr5W5Fm+2wT9ETgGoLBh25BLlYPTJRjAd2WY01s71v0lqAwNvHHCc3mg==", + "dependencies": { + "@huggingface/tasks": "^0.10.0" + }, "engines": { "node": ">=18" } @@ -733,6 +770,11 @@ "node": ">=18" } }, + "node_modules/@huggingface/tasks": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/@huggingface/tasks/-/tasks-0.10.8.tgz", + "integrity": "sha512-oIp9912FwByyyyxkB/CIiW203QxPeYzBG7dydLmqZdcW0ma0if1uklGumqkJ6y/rJlv7AR/jJ9wbee0Wl5YZTw==" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.8", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", @@ -812,9 +854,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.3.tgz", - "integrity": "sha512-FaNiGX1MrOuJ3hxuNzWgsT/mg5OHG/Izh59WW2mk1UwYHUwtfbhk5QNKYZgxf0pLOhx9ctGiGa2OykD71vOnSw==", + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.4.tgz", + "integrity": "sha512-p0suNqXufJs9t3RqLBO6vvrgr5OhgbWp76s5gTRvdmxmuv9E1rcaqGUsl3l4mKVmXPkTkTErXediAui4x+8PSA==", "cpu": [ "arm64" ], @@ -837,9 +879,9 @@ } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.3.tgz", - "integrity": "sha512-2QeSl7QDK9ru//YBT4sQkoq7L0EAJZA3rtV+v9p8xTKl4U1bUqTIaCnoC7Ctx2kCjQgwFXDasOtPTCT8eCTXvw==", + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.4.tgz", + "integrity": "sha512-0l7yRObwtTi82Z6ebVI2PnHT8EB2NxBgpK2MiKJZJ7cz32R4lxd001ecMhzzsZig3Yv9oclvqqdV93jo9hy+Dw==", "cpu": [ "x64" ], @@ -1030,9 +1072,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.3.tgz", - "integrity": "sha512-Q7Ee3fFSC9P7vUSqVEF0zccJsZ8GiiCJYGWDdhEjdlOeS9/jdkyJ6sUSPj+bL8VuOYFSbofrW0t/86ceVhx32w==", + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.4.tgz", + "integrity": "sha512-RUgBD1c0+gCYZGCCe6mMdTiOFS0Zc/XrN0fYd6hISIKcDUbAW5NtSQW9g/powkrXYm6Vzwd6y+fqmExDuCdHNQ==", "cpu": [ "arm" ], @@ -1055,9 +1097,9 @@ } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.3.tgz", - "integrity": "sha512-Zf+sF1jHZJKA6Gor9hoYG2ljr4wo9cY4twaxgFDvlG0Xz9V7sinsPp8pFd1XtlhTzYo0IhDbl3rK7P6MzHpnYA==", + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.4.tgz", + "integrity": "sha512-2800clwVg1ZQtxwSoTlHvtm9ObgAax7V6MTAB/hDT945Tfyy3hVkmiHpeLPCKYqYR1Gcmv1uDZ3a4OFwkdBL7Q==", "cpu": [ "arm64" ], @@ -1080,9 +1122,9 @@ } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.3.tgz", - "integrity": "sha512-vFk441DKRFepjhTEH20oBlFrHcLjPfI8B0pMIxGm3+yilKyYeHEVvrZhYFdqIseSclIqbQ3SnZMwEMWonY5XFA==", + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.4.tgz", + "integrity": "sha512-h3RAL3siQoyzSoH36tUeS0PDmb5wINKGYzcLB5C6DIiAn2F3udeFAum+gj8IbA/82+8RGCTn7XW8WTFnqag4tQ==", "cpu": [ "s390x" ], @@ -1091,7 +1133,7 @@ "linux" ], "engines": { - "glibc": ">=2.28", + "glibc": ">=2.31", "node": "^18.17.0 || ^20.3.0 || >=21.0.0", "npm": ">=9.6.5", "pnpm": ">=7.1.0", @@ -1105,9 +1147,9 @@ } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.3.tgz", - "integrity": "sha512-Q4I++herIJxJi+qmbySd072oDPRkCg/SClLEIDh5IL9h1zjhqjv82H0Seupd+q2m0yOfD+/fJnjSoDFtKiHu2g==", + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.4.tgz", + "integrity": "sha512-GoR++s0XW9DGVi8SUGQ/U4AeIzLdNjHka6jidVwapQ/JebGVQIpi52OdyxCNVRE++n1FCLzjDovJNozif7w/Aw==", "cpu": [ "x64" ], @@ -1130,9 +1172,9 @@ } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.3.tgz", - "integrity": "sha512-qnDccehRDXadhM9PM5hLvcPRYqyFCBN31kq+ErBSZtZlsAc1U4Z85xf/RXv1qolkdu+ibw64fUDaRdktxTNP9A==", + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.4.tgz", + "integrity": "sha512-nhr1yC3BlVrKDTl6cO12gTpXMl4ITBUZieehFvMntlCXFzH2bvKG76tBL2Y/OqhupZt81pR7R+Q5YhJxW0rGgQ==", "cpu": [ "arm64" ], @@ -1155,9 +1197,9 @@ } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.3.tgz", - "integrity": "sha512-Jhchim8kHWIU/GZ+9poHMWRcefeaxFIs9EBqf9KtcC14Ojk6qua7ghKiPs0sbeLbLj/2IGBtDcxHyjCdYWkk2w==", + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.4.tgz", + "integrity": "sha512-uCPTku0zwqDmZEOi4ILyGdmW76tH7dm8kKlOIV1XC5cLyJ71ENAAqarOHQh0RLfpIpbV5KOpXzdU6XkJtS0daw==", "cpu": [ "x64" ], @@ -1180,15 +1222,15 @@ } }, "node_modules/@img/sharp-wasm32": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.3.tgz", - "integrity": "sha512-68zivsdJ0koE96stdUfM+gmyaK/NcoSZK5dV5CAjES0FUXS9lchYt8LAB5rTbM7nlWtxaU/2GON0HVN6/ZYJAQ==", + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.4.tgz", + "integrity": "sha512-Bmmauh4sXUsUqkleQahpdNXKvo+wa1V9KhT2pDA4VJGKwnKMJXiSTGphn0gnJrlooda0QxCtXc6RX1XAU6hMnQ==", "cpu": [ "wasm32" ], "optional": true, "dependencies": { - "@emnapi/runtime": "^1.1.0" + "@emnapi/runtime": "^1.1.1" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0", @@ -1201,9 +1243,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.3.tgz", - "integrity": "sha512-CyimAduT2whQD8ER4Ux7exKrtfoaUiVr7HG0zZvO0XTFn2idUWljjxv58GxNTkFb8/J9Ub9AqITGkJD6ZginxQ==", + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.4.tgz", + "integrity": "sha512-99SJ91XzUhYHbx7uhK3+9Lf7+LjwMGQZMDlO/E/YVJ7Nc3lyDFZPGhjwiYdctoH2BOzW9+TnfqcaMKt0jHLdqw==", "cpu": [ "ia32" ], @@ -1222,9 +1264,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.3.tgz", - "integrity": "sha512-viT4fUIDKnli3IfOephGnolMzhz5VaTvDRkYqtZxOMIoMQ4MrAziO7pT1nVnOt2FAm7qW5aa+CCc13aEY6Le0g==", + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.4.tgz", + "integrity": "sha512-3QLocdTRVIrFNye5YocZl+KKpYKP+fksi1QhmOArgx7GyhIbQp/WrJRu176jm8IxromS7RIkzMiMINVdBtC8Aw==", "cpu": [ "x64" ], @@ -1242,6 +1284,76 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@inquirer/confirm": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-3.1.7.tgz", + "integrity": "sha512-BZjjj19W8gnh5UGFTdP5ZxpgMNRjy03Dzq3k28sB2MDlEUFrcyTkMEoGgvBmGpUw0vNBoCJkTcbHZ3e9tb+d+w==", + "dependencies": { + "@inquirer/core": "^8.2.0", + "@inquirer/type": "^1.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-8.2.0.tgz", + "integrity": "sha512-pexNF9j2orvMMTgoQ/uKOw8V6/R7x/sIDwRwXRhl4i0pPSh6paRzFehpFKpfMbqix1/+gzCekhYTmVbQpWkVjQ==", + "dependencies": { + "@inquirer/figures": "^1.0.1", + "@inquirer/type": "^1.3.1", + "@types/mute-stream": "^0.0.4", + "@types/node": "^20.12.11", + "@types/wrap-ansi": "^3.0.0", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "cli-spinners": "^2.9.2", + "cli-width": "^4.1.0", + "mute-stream": "^1.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core/node_modules/@types/node": { + "version": "20.12.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", + "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@inquirer/core/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.1.tgz", + "integrity": "sha512-mtup3wVKia3ZwULPHcbs4Mor8Voi+iIXEWD7wCNbIO6lYR62oPCTQyrddi5OMYVXHzeCSoneZwJuS8sBvlEwDw==", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.3.1.tgz", + "integrity": "sha512-Pe3PFccjPVJV1vtlfVvm9OnlbxqdnP5QcscFEFEnK5quChf1ufZtM0r8mR5ToWHMxZOh0s8o/qp9ANGRTo/DAw==", + "engines": { + "node": ">=18" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", @@ -1294,6 +1406,30 @@ "sparse-bitfield": "^3.0.3" } }, + "node_modules/@mswjs/cookies": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@mswjs/cookies/-/cookies-1.1.0.tgz", + "integrity": "sha512-0ZcCVQxifZmhwNBoQIrystCb+2sWBY2Zw8lpfJBPCHGCA/HWqehITeCRVIv4VMy8MPlaHo2w2pTHFV2pFfqKPw==", + "engines": { + "node": ">=18" + } + }, + "node_modules/@mswjs/interceptors": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.29.1.tgz", + "integrity": "sha512-3rDakgJZ77+RiQUuSK69t1F0m8BQKA8Vh5DCS5V0DWvNY67zob2JhhQrhCO0AKLGINTRSFd1tBaHcJTkhefoSw==", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.2.1", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1326,6 +1462,25 @@ "node": ">= 8" } }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==" + }, "node_modules/@opentelemetry/api": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.8.0.tgz", @@ -2168,6 +2323,11 @@ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, + "node_modules/@types/eventsource": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@types/eventsource/-/eventsource-1.1.15.tgz", + "integrity": "sha512-XQmGcbnxUNa06HR3VBVkc9+A2Vpi9ZyLJcdS5dwaQQ/4ZMWFO+5c90FnMUpbtMZwB/FChoYHwuVg8TvkECacTA==" + }, "node_modules/@types/express": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", @@ -2266,6 +2426,14 @@ "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", "dev": true }, + "node_modules/@types/mute-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", + "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "18.13.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.13.0.tgz", @@ -2356,6 +2524,11 @@ "@types/send": "*" } }, + "node_modules/@types/statuses": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.5.tgz", + "integrity": "sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==" + }, "node_modules/@types/tough-cookie": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", @@ -2382,6 +2555,11 @@ "@types/webidl-conversions": "*" } }, + "node_modules/@types/wrap-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", + "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.7.4", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.4.tgz", @@ -2799,11 +2977,35 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -2812,7 +3014,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -3195,8 +3396,6 @@ "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.7.tgz", "integrity": "sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==", "hasInstallScript": true, - "optional": true, - "peer": true, "dependencies": { "node-gyp-build": "^4.3.0" }, @@ -3317,7 +3516,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -3389,6 +3587,54 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/code-red": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", @@ -3538,7 +3784,6 @@ "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -4351,6 +4596,14 @@ "node": ">=0.8.x" } }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -4803,6 +5056,14 @@ "node": ">=14" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -4971,6 +5232,14 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/graphql": { + "version": "16.8.1", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz", + "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, "node_modules/gtoken": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", @@ -5013,7 +5282,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -5067,6 +5335,11 @@ "node": ">= 0.4" } }, + "node_modules/headers-polyfill": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", + "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==" + }, "node_modules/help-me": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", @@ -5356,6 +5629,14 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -5373,6 +5654,11 @@ "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", "dev": true }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==" + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -6101,6 +6387,77 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/msw": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.3.0.tgz", + "integrity": "sha512-cDr1q/QTMzaWhY8n9lpGhceY209k29UZtdTgJ3P8Bzne3TSMchX2EM/ldvn4ATLOktpCefCU2gcEgzHc31GTPw==", + "hasInstallScript": true, + "dependencies": { + "@bundled-es-modules/cookie": "^2.0.0", + "@bundled-es-modules/statuses": "^1.0.1", + "@inquirer/confirm": "^3.0.0", + "@mswjs/cookies": "^1.1.0", + "@mswjs/interceptors": "^0.29.0", + "@open-draft/until": "^2.1.0", + "@types/cookie": "^0.6.0", + "@types/statuses": "^2.0.4", + "chalk": "^4.1.2", + "graphql": "^16.8.1", + "headers-polyfill": "^4.0.2", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.2", + "path-to-regexp": "^6.2.0", + "strict-event-emitter": "^0.5.1", + "type-fest": "^4.9.0", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.7.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/msw/node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" + }, + "node_modules/msw/node_modules/path-to-regexp": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", + "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==" + }, + "node_modules/msw/node_modules/type-fest": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.18.2.tgz", + "integrity": "sha512-+suCYpfJLAe4OXS6+PPXjW3urOS4IoP9waSiLuXfLgqZODKw/aWwASvzqE886wA0kQgGy0mIWyhd87VpqIy6Xg==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -6233,8 +6590,6 @@ "version": "4.6.1", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.1.tgz", "integrity": "sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==", - "optional": true, - "peer": true, "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", @@ -6473,6 +6828,11 @@ "node": ">= 0.8.0" } }, + "node_modules/outvariant": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.2.tgz", + "integrity": "sha512-Ou3dJ6bA/UJ5GVHxah4LnqDwZRwAmWxrG3wtrHrbGnP4RnLCtA64A4F+ae7Y8ww660JaddSoArUR5HjipWSHAQ==" + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -6866,11 +7226,11 @@ "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==" }, "node_modules/playwright": { - "version": "1.43.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.43.1.tgz", - "integrity": "sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==", + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.40.0.tgz", + "integrity": "sha512-gyHAgQjiDf1m34Xpwzaqb76KgfzYrhK7iih+2IzcOCoZWr/8ZqmdBw+t0RU85ZmfJMgtgAiNtBQ/KS2325INXw==", "dependencies": { - "playwright-core": "1.43.1" + "playwright-core": "1.40.0" }, "bin": { "playwright": "cli.js" @@ -6906,6 +7266,17 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/playwright/node_modules/playwright-core": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.40.0.tgz", + "integrity": "sha512-fvKewVJpGeca8t0ipM56jkVSU6Eo0RmFvQ/MaCQNDYm+sdvKkMBBWTE1FdeMqIdumRaXXjZChWHvIzCGM/tA/Q==", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/postcss": { "version": "8.4.35", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", @@ -7542,6 +7913,14 @@ "node": ">= 12.13.0" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -7783,6 +8162,14 @@ "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" }, + "node_modules/semiver": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/semiver/-/semiver-1.1.0.tgz", + "integrity": "sha512-QNI2ChmuioGC1/xjyYwyZYADILWyW6AmS1UH6gDj/SFUUUS4MBAWs/7mxnkRPc/F4iHezDP+O8t0dO8WHiEOdg==", + "engines": { + "node": ">=6" + } + }, "node_modules/semver": { "version": "7.6.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", @@ -7885,9 +8272,9 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, "node_modules/sharp": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.3.tgz", - "integrity": "sha512-vHUeXJU1UvlO/BNwTpT0x/r53WkLUVxrmb5JTgW92fdFCFk0ispLMAeu/jPO2vjkXM1fYUi3K7/qcLF47pwM1A==", + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.4.tgz", + "integrity": "sha512-7i/dt5kGl7qR4gwPRD2biwD2/SvBn3O04J77XKFgL2OnZtQw+AG9wnuS/csmu80nPRHLYE9E41fyEiG8nhH6/Q==", "hasInstallScript": true, "dependencies": { "color": "^4.2.3", @@ -7902,8 +8289,8 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.3", - "@img/sharp-darwin-x64": "0.33.3", + "@img/sharp-darwin-arm64": "0.33.4", + "@img/sharp-darwin-x64": "0.33.4", "@img/sharp-libvips-darwin-arm64": "1.0.2", "@img/sharp-libvips-darwin-x64": "1.0.2", "@img/sharp-libvips-linux-arm": "1.0.2", @@ -7912,15 +8299,15 @@ "@img/sharp-libvips-linux-x64": "1.0.2", "@img/sharp-libvips-linuxmusl-arm64": "1.0.2", "@img/sharp-libvips-linuxmusl-x64": "1.0.2", - "@img/sharp-linux-arm": "0.33.3", - "@img/sharp-linux-arm64": "0.33.3", - "@img/sharp-linux-s390x": "0.33.3", - "@img/sharp-linux-x64": "0.33.3", - "@img/sharp-linuxmusl-arm64": "0.33.3", - "@img/sharp-linuxmusl-x64": "0.33.3", - "@img/sharp-wasm32": "0.33.3", - "@img/sharp-win32-ia32": "0.33.3", - "@img/sharp-win32-x64": "0.33.3" + "@img/sharp-linux-arm": "0.33.4", + "@img/sharp-linux-arm64": "0.33.4", + "@img/sharp-linux-s390x": "0.33.4", + "@img/sharp-linux-x64": "0.33.4", + "@img/sharp-linuxmusl-arm64": "0.33.4", + "@img/sharp-linuxmusl-x64": "0.33.4", + "@img/sharp-wasm32": "0.33.4", + "@img/sharp-win32-ia32": "0.33.4", + "@img/sharp-win32-x64": "0.33.4" } }, "node_modules/shebang-command": { @@ -8163,6 +8550,11 @@ "queue-tick": "^1.0.1" } }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -8171,6 +8563,24 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, "node_modules/string.prototype.codepointat": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", @@ -8180,7 +8590,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -8292,7 +8701,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -8952,7 +9360,6 @@ "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", - "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8995,6 +9402,11 @@ "node": ">=14.0" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "node_modules/unicode-trie": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", @@ -9960,6 +10372,19 @@ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -9998,6 +10423,14 @@ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -10012,6 +10445,31 @@ "node": ">= 6" } }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index 5952e57c9b81216a8c9dd44745b6117e72382fca..90dc4d4b44644e416d6810fe7ba6727fd9b3b5b0 100644 --- a/package.json +++ b/package.json @@ -54,11 +54,12 @@ "type": "module", "dependencies": { "@cliqz/adblocker-playwright": "^1.27.2", + "@gradio/client": "^0.19.4", "@huggingface/hub": "^0.5.1", - "@huggingface/inference": "^2.6.3", + "@huggingface/inference": "^2.7.0", "@iconify-json/bi": "^1.1.21", - "@resvg/resvg-js": "^2.6.2", "@playwright/browser-chromium": "^1.43.1", + "@resvg/resvg-js": "^2.6.2", "@xenova/transformers": "^2.16.1", "autoprefixer": "^10.4.14", "browser-image-resizer": "^2.4.1", @@ -87,7 +88,7 @@ "satori-html": "^0.3.2", "sbd": "^1.0.19", "serpapi": "^1.1.1", - "sharp": "^0.33.3", + "sharp": "^0.33.4", "tailwind-scrollbar": "^3.0.0", "tailwindcss": "^3.4.0", "uuid": "^9.0.1", diff --git a/src/lib/actions/clickOutside.ts b/src/lib/actions/clickOutside.ts index 52e310e22d738d2f221ce1bdb4202ab45e811aba..6aa146932fea03a06fa3c654c58460d33c389850 100644 --- a/src/lib/actions/clickOutside.ts +++ b/src/lib/actions/clickOutside.ts @@ -1,4 +1,4 @@ -export function clickOutside(element: HTMLDialogElement, callbackFunction: () => void) { +export function clickOutside(element: HTMLElement, callbackFunction: () => void) { function onClick(event: MouseEvent) { if (!element.contains(event.target as Node)) { callbackFunction(); diff --git a/src/lib/buildPrompt.ts b/src/lib/buildPrompt.ts index 00598c101e9006e55c01cfb51d78418da7e78042..2fb288f8f6454bc178b19709bf8b86b2cc23dce8 100644 --- a/src/lib/buildPrompt.ts +++ b/src/lib/buildPrompt.ts @@ -1,8 +1,11 @@ import type { EndpointParameters } from "./server/endpoints/endpoints"; import type { BackendModel } from "./server/models"; +import type { Tool, ToolResult } from "./types/Tool"; type buildPromptOptions = Pick & { model: BackendModel; + tools?: Tool[]; + toolResults?: ToolResult[]; }; export async function buildPrompt({ @@ -10,11 +13,18 @@ export async function buildPrompt({ model, preprompt, continueMessage, + tools, + toolResults, }: buildPromptOptions): Promise { const filteredMessages = messages.filter((m) => m.from !== "system"); let prompt = model - .chatPromptRender({ messages: filteredMessages, preprompt }) + .chatPromptRender({ + messages: filteredMessages, + preprompt, + tools, + toolResults, + }) // Not super precise, but it's truncated in the model's backend anyway .split(" ") .slice(-(model.parameters?.truncate ?? 0)) diff --git a/src/lib/components/OpenWebSearchResults.svelte b/src/lib/components/OpenWebSearchResults.svelte index bd219ec10462f4a287eb1ed7c0c10b84b4eda7e2..c4f01555131fc639bb243c75800590cb29fe594a 100644 --- a/src/lib/components/OpenWebSearchResults.svelte +++ b/src/lib/components/OpenWebSearchResults.svelte @@ -1,16 +1,22 @@
@@ -61,7 +67,7 @@ {:else}
    {#each webSearchMessages as message} - {#if message.messageType === "update"} + {#if message.subtype === MessageWebSearchUpdateType.Update}
  1. {/if}
  2. - {:else if message.messageType === "error"} + {:else if message.subtype === MessageWebSearchUpdateType.Error}
  3. + import { page } from "$app/stores"; + import { clickOutside } from "$lib/actions/clickOutside"; + import { useSettingsStore } from "$lib/stores/settings"; + import type { ToolFront } from "$lib/types/Tool"; + import { isHuggingChat } from "$lib/utils/isHuggingChat"; + import IconTool from "./icons/IconTool.svelte"; + import CarbonInformation from "~icons/carbon/information"; + + export let loading = false; + const settings = useSettingsStore(); + + let detailsEl: HTMLDetailsElement; + + // active tools are all the checked tools, either from settings or on by default + $: activeToolCount = $page.data.tools.filter( + (tool: ToolFront) => $settings?.tools?.[tool.name] ?? tool.isOnByDefault + ).length; + + +
    { + if (detailsEl.hasAttribute("open")) { + detailsEl.removeAttribute("open"); + } + }} +> + + + Tools + ({activeToolCount}) + +
    +
    +
    + Available tools + {#if isHuggingChat} + + {/if} +
    + {#each $page.data.tools as tool} + {@const isChecked = $settings?.tools?.[tool.name] ?? tool.isOnByDefault} +
    + { + await settings.instantSet({ + tools: { + ...$settings.tools, + [tool.name]: !isChecked, + }, + }); + }} + /> + +
    + {/each} +
    +
    +
    diff --git a/src/lib/components/UploadBtn.svelte b/src/lib/components/UploadBtn.svelte index cb869443e9b04baf31d9b2ca5b1b324e38f85361..fdb1af4f5132e5f622d9de3e2f22c24cd218dd58 100644 --- a/src/lib/components/UploadBtn.svelte +++ b/src/lib/components/UploadBtn.svelte @@ -3,21 +3,26 @@ export let classNames = ""; export let files: File[]; - let filelist: FileList; - $: if (filelist) { - files = Array.from(filelist); - } + /** + * Due to a bug with Svelte, we cannot use bind:files with multiple + * So we use this workaround + **/ + const onFileChange = (e: Event) => { + if (!e.target) return; + const target = e.target as HTMLInputElement; + files = [...files, ...(target.files ?? [])]; + }; diff --git a/src/lib/components/chat/ChatMessage.svelte b/src/lib/components/chat/ChatMessage.svelte index f76cd12a358c18caa1e6bf11131f3a01d1b07e2d..97f65b0b9d7536e4a5d0ae8c957cbcc9ad7ca96a 100644 --- a/src/lib/components/chat/ChatMessage.svelte +++ b/src/lib/components/chat/ChatMessage.svelte @@ -1,7 +1,7 @@ +{#if modalImageToShow} + + (modalImageToShow = null)}> + {#if modalImageToShow.type === "hash"} + input from user + {:else} + + input from user + {/if} + +{/if} + {#if message.from === "assistant"}
    + {#if message.files?.length} +
    + {#each message.files as file} + + + {/each} +
    + {/if} {#if searchUpdates && searchUpdates.length > 0} {/if} + {#if toolUpdates} + {#each Object.values(toolUpdates) as tool} + {#if tool.length} + {@const toolName = tool.find(isMessageToolCallUpdate)?.call.name} + {@const toolDone = tool.some(isMessageToolResultUpdate)} + {#if toolName && toolName !== "websearch"} +
    + +
    + + + + +
    + + + {toolDone ? "Called" : "Calling"} tool + {availableTools.find((el) => toolHasName(toolName, el))?.displayName} + +
    + {#each tool as toolUpdate} + {#if toolUpdate.subtype === MessageToolUpdateType.Call} +
    +

    Parameters

    +
    +
    +
      + {#each Object.entries(toolUpdate.call.parameters ?? {}) as [k, v]} +
    • + {k}: + {v} +
    • + {/each} +
    + {/if} + {/each} +
    + {/if} + {/if} + {/each} + {/if} +
    {/if}
    - {#if !loading && message.content} + {#if !loading && (message.content || toolUpdates)}
    - {#if message.files && message.files.length > 0} -
    +
    + {#if message.files?.length} +
    {#each message.files as file} - - {#if file.type === "hash"} - input from user + {#if file.mime.startsWith("image/")} + {:else} - - input from user + {/if} {/each}
    @@ -458,3 +590,20 @@ {/if} + + diff --git a/src/lib/components/chat/ChatWindow.svelte b/src/lib/components/chat/ChatWindow.svelte index a00133d94e11ca5ef5ce60036d705651150207ee..af3b0bafd386b314bc02a12a9b7158312142db72 100644 --- a/src/lib/components/chat/ChatWindow.svelte +++ b/src/lib/components/chat/ChatWindow.svelte @@ -1,11 +1,10 @@ + +
    + {#if file.mime.startsWith("image/")} +
    + {file.name} +
    + {:else} +
    +
    + +
    +
    +
    + {file.name} +
    +
    {file.mime.split("/")[1].toUpperCase()}
    +
    +
    + {/if} + + {#if canClose} + + {/if} +
    diff --git a/src/lib/components/icons/IconTool.svelte b/src/lib/components/icons/IconTool.svelte new file mode 100644 index 0000000000000000000000000000000000000000..28caf04ef163be07f70d3b78b04fe646d664ac21 --- /dev/null +++ b/src/lib/components/icons/IconTool.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/lib/migrations/routines/03-add-tools-in-settings.ts b/src/lib/migrations/routines/03-add-tools-in-settings.ts new file mode 100644 index 0000000000000000000000000000000000000000..ec87fbc8c5949f9d32d4e326d6eb2f8235bd7461 --- /dev/null +++ b/src/lib/migrations/routines/03-add-tools-in-settings.ts @@ -0,0 +1,29 @@ +import type { Migration } from "."; +import { collections } from "$lib/server/database"; +import { ObjectId } from "mongodb"; +import { logger } from "$lib/server/logger"; + +const addToolsToSettings: Migration = { + _id: new ObjectId("5c9c4c4c4c4c4c4c4c4c4c4c"), + name: "Add empty 'tools' record in settings", + up: async () => { + const { settings } = collections; + + // Find all assistants whose modelId is not in modelIds, and update it to use defaultModelId + await settings.updateMany( + { + tools: { $exists: false }, + }, + { $set: { tools: {} } } + ); + + settings + .createIndex({ tools: 1 }) + .catch((e) => logger.error("Error creating index during tools migration", e)); + + return true; + }, + runEveryTime: false, +}; + +export default addToolsToSettings; diff --git a/src/lib/migrations/routines/04-update-message-updates.ts b/src/lib/migrations/routines/04-update-message-updates.ts new file mode 100644 index 0000000000000000000000000000000000000000..540e3baf28c0972a3d3ed279d234ee4f377472d0 --- /dev/null +++ b/src/lib/migrations/routines/04-update-message-updates.ts @@ -0,0 +1,190 @@ +import type { Migration } from "."; +import { collections } from "$lib/server/database"; +import { ObjectId, type WithId } from "mongodb"; +import type { Conversation } from "$lib/types/Conversation"; +import type { WebSearchSource } from "$lib/types/WebSearch"; +import { + MessageUpdateStatus, + MessageUpdateType, + MessageWebSearchUpdateType, + type MessageUpdate, + type MessageWebSearchFinishedUpdate, +} from "$lib/types/MessageUpdate"; +import type { Message } from "$lib/types/Message"; +import { isMessageWebSearchSourcesUpdate } from "$lib/utils/messageUpdates"; + +// ----------- +// Copy of the previous message update types +export type FinalAnswer = { + type: "finalAnswer"; + text: string; +}; + +export type TextStreamUpdate = { + type: "stream"; + token: string; +}; + +type WebSearchUpdate = { + type: "webSearch"; + messageType: "update" | "error" | "sources"; + message: string; + args?: string[]; + sources?: WebSearchSource[]; +}; + +type StatusUpdate = { + type: "status"; + status: "started" | "pending" | "finished" | "error" | "title"; + message?: string; +}; + +type ErrorUpdate = { + type: "error"; + message: string; + name: string; +}; + +type FileUpdate = { + type: "file"; + sha: string; +}; + +type OldMessageUpdate = + | FinalAnswer + | TextStreamUpdate + | WebSearchUpdate + | StatusUpdate + | ErrorUpdate + | FileUpdate; + +/** Converts the old message update to the new schema */ +function convertMessageUpdate(message: Message, update: OldMessageUpdate): MessageUpdate | null { + try { + // Text and files + if (update.type === "finalAnswer") { + return { + type: MessageUpdateType.FinalAnswer, + text: update.text, + interrupted: message.interrupted ?? false, + }; + } else if (update.type === "stream") { + return { + type: MessageUpdateType.Stream, + token: update.token, + }; + } else if (update.type === "file") { + return { + type: MessageUpdateType.File, + name: "Unknown", + sha: update.sha, + // assume jpeg but could be any image. should be harmless + mime: "image/jpeg", + }; + } + + // Status + else if (update.type === "status") { + if (update.status === "title") { + return { + type: MessageUpdateType.Title, + title: update.message ?? "New Chat", + }; + } + if (update.status === "pending") return null; + + const status = + update.status === "started" + ? MessageUpdateStatus.Started + : update.status === "finished" + ? MessageUpdateStatus.Finished + : MessageUpdateStatus.Error; + return { + type: MessageUpdateType.Status, + status, + message: update.message, + }; + } else if (update.type === "error") { + // Treat it as an error status update + return { + type: MessageUpdateType.Status, + status: MessageUpdateStatus.Error, + message: update.message, + }; + } + + // Web Search + else if (update.type === "webSearch") { + if (update.messageType === "update") { + return { + type: MessageUpdateType.WebSearch, + subtype: MessageWebSearchUpdateType.Update, + message: update.message, + args: update.args, + }; + } else if (update.messageType === "error") { + return { + type: MessageUpdateType.WebSearch, + subtype: MessageWebSearchUpdateType.Error, + message: update.message, + args: update.args, + }; + } else if (update.messageType === "sources") { + return { + type: MessageUpdateType.WebSearch, + subtype: MessageWebSearchUpdateType.Sources, + message: update.message, + sources: update.sources ?? [], + }; + } + } + console.warn("Unknown message update during migration:", update); + return null; + } catch (error) { + console.error("Error converting message update during migration. Skipping it... Error:", error); + return null; + } +} + +const updateMessageUpdates: Migration = { + _id: new ObjectId("5f9f4f4f4f4f4f4f4f4f4f4f"), + name: "Convert message updates to the new schema", + up: async () => { + const allConversations = collections.conversations.find({}, { projection: { messages: 1 } }); + + let conversation: WithId> | null = null; + while ((conversation = await allConversations.tryNext())) { + const messages = conversation.messages.map((message) => { + // Convert all of the existing updates to the new schema + const updates = message.updates + ?.map((update) => convertMessageUpdate(message, update as OldMessageUpdate)) + .filter((update): update is MessageUpdate => Boolean(update)); + + // Add the new web search finished update if the sources update exists and webSearch is defined + const webSearchSourcesUpdateIndex = updates?.findIndex(isMessageWebSearchSourcesUpdate); + if ( + message.webSearch && + updates && + webSearchSourcesUpdateIndex && + webSearchSourcesUpdateIndex !== -1 + ) { + const webSearchFinishedUpdate: MessageWebSearchFinishedUpdate = { + type: MessageUpdateType.WebSearch, + subtype: MessageWebSearchUpdateType.Finished, + webSearch: message.webSearch, + }; + updates.splice(webSearchSourcesUpdateIndex + 1, 0, webSearchFinishedUpdate); + } + return { ...message, updates }; + }); + + // Set the new messages array + await collections.conversations.updateOne({ _id: conversation._id }, { $set: { messages } }); + } + + return true; + }, + runEveryTime: false, +}; + +export default updateMessageUpdates; diff --git a/src/lib/migrations/routines/05-update-message-files.ts b/src/lib/migrations/routines/05-update-message-files.ts new file mode 100644 index 0000000000000000000000000000000000000000..0a91cb86aa399ce9b55732fbf598b1ec5cc077a4 --- /dev/null +++ b/src/lib/migrations/routines/05-update-message-files.ts @@ -0,0 +1,56 @@ +import { ObjectId, type WithId } from "mongodb"; +import { collections } from "$lib/server/database"; + +import type { Migration } from "."; +import type { Conversation } from "$lib/types/Conversation"; +import type { MessageFile } from "$lib/types/Message"; + +const updateMessageFiles: Migration = { + _id: new ObjectId("5f9f5f5f5f5f5f5f5f5f5f5f"), + name: "Convert message files to the new schema", + up: async () => { + const allConversations = collections.conversations.find({}, { projection: { messages: 1 } }); + + let conversation: WithId> | null = null; + while ((conversation = await allConversations.tryNext())) { + const messages = conversation.messages.map((message) => { + const files = (message.files as string[] | undefined)?.map((file) => { + // File is already in the new format + if (typeof file !== "string") return file; + + // File was a hash pointing to a file in the bucket + if (file.length === 64) { + return { + type: "hash", + name: "unknown.jpg", + value: file, + mime: "image/jpeg", + }; + } + // File was a base64 string + else { + return { + type: "base64", + name: "unknown.jpg", + value: file, + mime: "image/jpeg", + }; + } + }); + + return { + ...message, + files, + }; + }); + + // Set the new messages array + await collections.conversations.updateOne({ _id: conversation._id }, { $set: { messages } }); + } + + return true; + }, + runEveryTime: false, +}; + +export default updateMessageFiles; diff --git a/src/lib/migrations/routines/index.ts b/src/lib/migrations/routines/index.ts index 0d6eafa8f04e86e7bba3ba9462774d2c25589600..78bb34d1ad9dde69d4b7e9ab4efc5cad0efc763c 100644 --- a/src/lib/migrations/routines/index.ts +++ b/src/lib/migrations/routines/index.ts @@ -3,6 +3,9 @@ import type { ObjectId } from "mongodb"; import updateSearchAssistant from "./01-update-search-assistants"; import updateAssistantsModels from "./02-update-assistants-models"; import type { Database } from "$lib/server/database"; +import addToolsToSettings from "./03-add-tools-in-settings"; +import updateMessageUpdates from "./04-update-message-updates"; +import updateMessageFiles from "./05-update-message-files"; export interface Migration { _id: ObjectId; @@ -14,4 +17,10 @@ export interface Migration { runEveryTime?: boolean; } -export const migrations: Migration[] = [updateSearchAssistant, updateAssistantsModels]; +export const migrations: Migration[] = [ + updateSearchAssistant, + updateAssistantsModels, + addToolsToSettings, + updateMessageUpdates, + updateMessageFiles, +]; diff --git a/src/lib/server/endpoints/cohere/endpointCohere.ts b/src/lib/server/endpoints/cohere/endpointCohere.ts index f1c5562fa022d3e5e40f7d8f240a32185f61e9c3..1b20565d1c6c561af4e3062ebe06214dffa75bfb 100644 --- a/src/lib/server/endpoints/cohere/endpointCohere.ts +++ b/src/lib/server/endpoints/cohere/endpointCohere.ts @@ -4,6 +4,9 @@ import type { Endpoint } from "../endpoints"; import type { TextGenerationStreamOutput } from "@huggingface/inference"; import type { Cohere, CohereClient } from "cohere-ai"; import { buildPrompt } from "$lib/buildPrompt"; +import { ToolResultStatus, type ToolCall } from "$lib/types/Tool"; +import { pipeline, Writable, Readable } from "node:stream"; +import { toolHasName } from "$lib/utils/tools"; export const endpointCohereParametersSchema = z.object({ weight: z.number().int().positive().default(1), @@ -28,12 +31,18 @@ export async function endpointCohere( throw new Error("Failed to import cohere-ai", { cause: e }); } - return async ({ messages, preprompt, generateSettings, continueMessage }) => { + return async ({ messages, preprompt, generateSettings, continueMessage, tools, toolResults }) => { let system = preprompt; if (messages?.[0]?.from === "system") { system = messages[0].content; } + // Tools must use [A-z_] for their names and directly_answer is banned + // It's safe to convert the tool names because we treat - and _ the same + tools = tools + ?.filter((tool) => !toolHasName("directly_answer", tool)) + .map((tool) => ({ ...tool, name: tool.name.replaceAll("-", "_") })); + const parameters = { ...model.parameters, ...generateSettings }; return (async function* () { @@ -42,10 +51,12 @@ export async function endpointCohere( if (raw) { const prompt = await buildPrompt({ - messages: messages.filter((message) => message.from !== "system"), + messages, model, preprompt: system, continueMessage, + tools, + toolResults, }); stream = await cohere.chatStream({ @@ -67,18 +78,35 @@ export async function endpointCohere( message: message.content, })) satisfies Cohere.ChatMessage[]; - stream = await cohere.chatStream({ - model: model.id ?? model.name, - chatHistory: formattedMessages.slice(0, -1), - message: formattedMessages[formattedMessages.length - 1].message, - preamble: system, - p: parameters?.top_p, - k: parameters?.top_k, - maxTokens: parameters?.max_new_tokens, - temperature: parameters?.temperature, - stopSequences: parameters?.stop, - frequencyPenalty: parameters?.frequency_penalty, - }); + stream = await cohere + .chatStream({ + model: model.id ?? model.name, + chatHistory: formattedMessages.slice(0, -1), + message: formattedMessages[formattedMessages.length - 1].message, + preamble: system, + p: parameters?.top_p, + k: parameters?.top_k, + maxTokens: parameters?.max_new_tokens, + temperature: parameters?.temperature, + stopSequences: parameters?.stop, + frequencyPenalty: parameters?.frequency_penalty, + tools, + toolResults: toolResults?.map((toolResult) => { + if (toolResult.status === ToolResultStatus.Error) { + return { call: toolResult.call, outputs: [{ error: toolResult.message }] }; + } + return { call: toolResult.call, outputs: toolResult.outputs }; + }), + }) + .catch(async (err) => { + if (!err.body) throw err; + + // Decode the error message and throw + const message = await convertStreamToBuffer(err.body).catch(() => { + throw err; + }); + throw Error(message, { cause: err }); + }); } for await (const output of stream) { @@ -93,6 +121,18 @@ export async function endpointCohere( generated_text: null, details: null, } satisfies TextGenerationStreamOutput; + } else if (output.eventType === "tool-calls-generation") { + yield { + token: { + id: tokenId++, + text: "", + logprob: 0, + special: true, + toolCalls: output.toolCalls as ToolCall[], + }, + generated_text: null, + details: null, + }; } else if (output.eventType === "stream-end") { if (["ERROR", "ERROR_TOXIC", "ERROR_LIMIT"].includes(output.finishReason)) { throw new Error(output.finishReason); @@ -112,3 +152,26 @@ export async function endpointCohere( })(); }; } + +async function convertStreamToBuffer(webReadableStream: Readable) { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + + pipeline( + webReadableStream, + new Writable({ + write(chunk, _, callback) { + chunks.push(chunk); + callback(); + }, + }), + (err) => { + if (err) { + reject(err); + } else { + resolve(Buffer.concat(chunks).toString("utf-8")); + } + } + ); + }); +} diff --git a/src/lib/server/endpoints/endpoints.ts b/src/lib/server/endpoints/endpoints.ts index 8e8f66a7969250714257b3effaf6c8c38ff06100..e2970d57df19a95031407c5fe1919bec21d8fb63 100644 --- a/src/lib/server/endpoints/endpoints.ts +++ b/src/lib/server/endpoints/endpoints.ts @@ -1,6 +1,6 @@ import type { Conversation } from "$lib/types/Conversation"; import type { Message } from "$lib/types/Message"; -import type { TextGenerationStreamOutput } from "@huggingface/inference"; +import type { TextGenerationStreamOutput, TextGenerationStreamToken } from "@huggingface/inference"; import { endpointTgi, endpointTgiParametersSchema } from "./tgi/endpointTgi"; import { z } from "zod"; import endpointAws, { endpointAwsParametersSchema } from "./aws/endpointAws"; @@ -26,23 +26,31 @@ import endpointLangserve, { endpointLangserveParametersSchema, } from "./langserve/endpointLangserve"; +import type { Tool, ToolCall, ToolResult } from "$lib/types/Tool"; + export type EndpointMessage = Omit; + // parameters passed when generating text export interface EndpointParameters { messages: EndpointMessage[]; preprompt?: Conversation["preprompt"]; continueMessage?: boolean; // used to signal that the last message will be extended generateSettings?: Partial; + tools?: Tool[]; + toolResults?: ToolResult[]; isMultimodal?: boolean; } interface CommonEndpoint { weight: number; } +type TextGenerationStreamOutputWithTools = TextGenerationStreamOutput & { + token: TextGenerationStreamToken & { toolCalls?: ToolCall[] }; +}; // type signature for the endpoint export type Endpoint = ( params: EndpointParameters -) => Promise>; +) => Promise>; // generator function that takes in parameters for defining the endpoint and return the endpoint export type EndpointGenerator = (parameters: T) => Endpoint; diff --git a/src/lib/server/endpoints/tgi/endpointTgi.ts b/src/lib/server/endpoints/tgi/endpointTgi.ts index 94914c2189db9b2479d9db2e7373b345edf188e7..305a9af00729f7f9174a76fa97c3b01ac6e5682d 100644 --- a/src/lib/server/endpoints/tgi/endpointTgi.ts +++ b/src/lib/server/endpoints/tgi/endpointTgi.ts @@ -35,7 +35,15 @@ export function endpointTgi(input: z.input): endpointTgiParametersSchema.parse(input); const imageProcessor = makeImageProcessor(multimodal.image); - return async ({ messages, preprompt, continueMessage, generateSettings, isMultimodal }) => { + return async ({ + messages, + preprompt, + continueMessage, + generateSettings, + tools, + toolResults, + isMultimodal, + }) => { const messagesWithResizedFiles = await Promise.all( messages.map((message) => prepareMessage(Boolean(isMultimodal), message, imageProcessor)) ); @@ -45,6 +53,8 @@ export function endpointTgi(input: z.input): preprompt, model, continueMessage, + tools, + toolResults, }); return textGenerationStream( diff --git a/src/lib/server/files/downloadFile.ts b/src/lib/server/files/downloadFile.ts index dac5bbda8486686228316488c8b9c0fa99bbba6f..37776452b7409ffdb1abe88b4629fec81b21d6f3 100644 --- a/src/lib/server/files/downloadFile.ts +++ b/src/lib/server/files/downloadFile.ts @@ -9,29 +9,26 @@ export async function downloadFile( convId: Conversation["_id"] | SharedConversation["_id"] ): Promise { const fileId = collections.bucket.find({ filename: `${convId.toString()}-${sha256}` }); - let mime = ""; - const buffer = await fileId.next().then(async (file) => { - if (!file) { - throw error(404, "File not found"); - } - if (file.metadata?.conversation !== convId.toString()) { - throw error(403, "You don't have access to this file."); - } + const file = await fileId.next(); + if (!file) { + throw error(404, "File not found"); + } + if (file.metadata?.conversation !== convId.toString()) { + throw error(403, "You don't have access to this file."); + } - mime = file.metadata?.mime; + const mime = file.metadata?.mime; + const name = file.filename; - const fileStream = collections.bucket.openDownloadStream(file._id); + const fileStream = collections.bucket.openDownloadStream(file._id); - const fileBuffer = await new Promise((resolve, reject) => { - const chunks: Uint8Array[] = []; - fileStream.on("data", (chunk) => chunks.push(chunk)); - fileStream.on("error", reject); - fileStream.on("end", () => resolve(Buffer.concat(chunks))); - }); - - return fileBuffer; + const buffer = await new Promise((resolve, reject) => { + const chunks: Uint8Array[] = []; + fileStream.on("data", (chunk) => chunks.push(chunk)); + fileStream.on("error", reject); + fileStream.on("end", () => resolve(Buffer.concat(chunks))); }); - return { type: "base64", value: buffer.toString("base64"), mime }; + return { type: "base64", name, value: buffer.toString("base64"), mime }; } diff --git a/src/lib/server/files/uploadFile.ts b/src/lib/server/files/uploadFile.ts index 339a4b4ea85bbced04d498f6c179324f57d3cea9..97b335beaf00cbad96ce1ef3bbd531cd8ce129ba 100644 --- a/src/lib/server/files/uploadFile.ts +++ b/src/lib/server/files/uploadFile.ts @@ -20,7 +20,9 @@ export async function uploadFile(file: File, conv: Conversation): Promise { - upload.once("finish", () => resolve({ type: "hash", value: sha, mime: file.type })); + upload.once("finish", () => + resolve({ type: "hash", value: sha, mime: file.type, name: file.name }) + ); upload.once("error", reject); setTimeout(() => reject(new Error("Upload timed out")), 20_000); }); diff --git a/src/lib/server/models.ts b/src/lib/server/models.ts index 78f0383331ae982d25d093d6d369997183f0e379..0ca5acf6173f1ccc964ffbb3d3469c5b08dcf738 100644 --- a/src/lib/server/models.ts +++ b/src/lib/server/models.ts @@ -12,6 +12,7 @@ import type { PreTrainedTokenizer } from "@xenova/transformers"; import JSON5 from "json5"; import { getTokenizer } from "$lib/utils/getTokenizer"; import { logger } from "$lib/server/logger"; +import { ToolResultStatus } from "$lib/types/Tool"; type Optional = Pick, K> & Omit; @@ -61,6 +62,7 @@ const modelConfig = z.object({ .passthrough() .optional(), multimodal: z.boolean().default(false), + tools: z.boolean().default(false), unlisted: z.boolean().default(false), embeddingModel: validateEmbeddingModelByName(embeddingModels).optional(), }); @@ -94,7 +96,7 @@ async function getChatPromptRender( process.exit(); } - const renderTemplate = ({ messages, preprompt }: ChatTemplateInput) => { + const renderTemplate = ({ messages, preprompt, tools, toolResults }: ChatTemplateInput) => { let formattedMessages: { role: string; content: string }[] = messages.map((message) => ({ content: message.content, role: message.from, @@ -110,9 +112,64 @@ async function getChatPromptRender( ]; } + if (toolResults?.length) { + // todo: should update the command r+ tokenizer to support system messages at any location + // or use the `rag` mode without the citations + formattedMessages = [ + { + role: "system", + content: + "\n\n\n" + + toolResults + .flatMap((result, idx) => { + if (result.status === ToolResultStatus.Error) { + return ( + `Document: ${idx}\n` + `Tool "${result.call.name}" error\n` + result.message + ); + } + return ( + `Document: ${idx}\n` + + result.outputs + .flatMap((output) => + Object.entries(output).map(([title, text]) => `${title}\n${text}`) + ) + .join("\n") + ); + }) + .join("\n\n") + + "\n", + }, + ...formattedMessages, + ]; + tools = []; + } + + const chatTemplate = tools?.length ? "tool_use" : undefined; + + const documents = (toolResults ?? []).flatMap((result) => { + if (result.status === ToolResultStatus.Error) { + return [{ title: `Tool "${result.call.name}" error`, text: "\n" + result.message }]; + } + return result.outputs.flatMap((output) => + Object.entries(output).map(([title, text]) => ({ + title: `Tool "${result.call.name}" ${title}`, + text: "\n" + text, + })) + ); + }); + const output = tokenizer.apply_chat_template(formattedMessages, { tokenize: false, add_generation_prompt: true, + chat_template: chatTemplate, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + tools: + tools?.map(({ parameterDefinitions, ...tool }) => ({ + parameter_definitions: parameterDefinitions, + ...tool, + })) ?? [], + documents, }); if (typeof output !== "string") { @@ -134,6 +191,10 @@ const processModel = async (m: z.infer) => ({ parameters: { ...m.parameters, stop_sequences: m.parameters?.stop }, }); +export type ProcessedModel = Awaited> & { + getEndpoint: () => Promise; +}; + const addEndpoint = (m: Awaited>) => ({ ...m, getEndpoint: async (): Promise => { @@ -189,7 +250,9 @@ const addEndpoint = (m: Awaited>) => ({ }, }); -export const models = await Promise.all(modelsRaw.map((e) => processModel(e).then(addEndpoint))); +export const models: ProcessedModel[] = await Promise.all( + modelsRaw.map((e) => processModel(e).then(addEndpoint)) +); export const defaultModel = models[0]; @@ -224,5 +287,5 @@ export const smallModel = env.TASK_MODEL export type BackendModel = Optional< typeof defaultModel, - "preprompt" | "parameters" | "multimodal" | "unlisted" + "preprompt" | "parameters" | "multimodal" | "unlisted" | "tools" >; diff --git a/src/lib/server/textGeneration/assistant.ts b/src/lib/server/textGeneration/assistant.ts new file mode 100644 index 0000000000000000000000000000000000000000..4423db5db037f0f8da2538427219e80c2e1059a7 --- /dev/null +++ b/src/lib/server/textGeneration/assistant.ts @@ -0,0 +1,53 @@ +import { isURLLocal } from "../isURLLocal"; +import { env } from "$env/dynamic/private"; +import { collections } from "$lib/server/database"; +import type { Assistant } from "$lib/types/Assistant"; +import type { ObjectId } from "mongodb"; + +export async function processPreprompt(preprompt: string) { + const urlRegex = /{{\s?url=(.*?)\s?}}/g; + + for (const match of preprompt.matchAll(urlRegex)) { + try { + const url = new URL(match[1]); + if ((await isURLLocal(url)) && env.ENABLE_LOCAL_FETCH !== "true") { + throw new Error("URL couldn't be fetched, it resolved to a local address."); + } + + const res = await fetch(url.href); + + if (!res.ok) { + throw new Error("URL couldn't be fetched, error " + res.status); + } + const text = await res.text(); + preprompt = preprompt.replaceAll(match[0], text); + } catch (e) { + preprompt = preprompt.replaceAll(match[0], (e as Error).message); + } + } + + return preprompt; +} + +export async function getAssistantById(id?: ObjectId) { + return collections.assistants + .findOne>( + { _id: id }, + { projection: { rag: 1, dynamicPrompt: 1, generateSettings: 1 } } + ) + .then((a) => a ?? undefined); +} + +export function assistantHasWebSearch(assistant?: Pick | null) { + return ( + env.ENABLE_ASSISTANTS_RAG === "true" && + !!assistant?.rag && + (assistant.rag.allowedLinks.length > 0 || + assistant.rag.allowedDomains.length > 0 || + assistant.rag.allowAllDomains) + ); +} + +export function assistantHasDynamicPrompt(assistant?: Pick) { + return env.ENABLE_ASSISTANTS_RAG === "true" && Boolean(assistant?.dynamicPrompt); +} diff --git a/src/lib/server/textGeneration/generate.ts b/src/lib/server/textGeneration/generate.ts new file mode 100644 index 0000000000000000000000000000000000000000..94f41e61b7d4edff4807e131bf0919f9f98fb9ab --- /dev/null +++ b/src/lib/server/textGeneration/generate.ts @@ -0,0 +1,51 @@ +import type { ToolResult } from "$lib/types/Tool"; +import { MessageUpdateType, type MessageUpdate } from "$lib/types/MessageUpdate"; +import { AbortedGenerations } from "../abortedGenerations"; +import type { TextGenerationContext } from "./types"; +import type { EndpointMessage } from "../endpoints/endpoints"; + +type GenerateContext = Omit & { messages: EndpointMessage[] }; + +export async function* generate( + { model, endpoint, conv, messages, assistant, isContinue, promptedAt }: GenerateContext, + toolResults: ToolResult[], + preprompt?: string +): AsyncIterable { + for await (const output of await endpoint({ + messages, + preprompt, + continueMessage: isContinue, + generateSettings: assistant?.generateSettings, + toolResults, + })) { + // text generation completed + if (output.generated_text) { + let interrupted = + !output.token.special && !model.parameters.stop?.includes(output.token.text); + + let text = output.generated_text.trimEnd(); + for (const stopToken of model.parameters.stop ?? []) { + if (!text.endsWith(stopToken)) continue; + + interrupted = false; + text = text.slice(0, text.length - stopToken.length); + } + + yield { type: MessageUpdateType.FinalAnswer, text, interrupted }; + continue; + } + + // ignore special tokens + if (output.token.special) continue; + + // pass down normal token + yield { type: MessageUpdateType.Stream, token: output.token.text }; + + // abort check + const date = AbortedGenerations.getInstance().getList().get(conv._id.toString()); + if (date && date > promptedAt) break; + + // no output check + if (!output) break; + } +} diff --git a/src/lib/server/textGeneration/index.ts b/src/lib/server/textGeneration/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..942eff84034bb0bc09b91f639212243c19e9f7da --- /dev/null +++ b/src/lib/server/textGeneration/index.ts @@ -0,0 +1,64 @@ +import { runWebSearch } from "$lib/server/websearch/runWebSearch"; +import { preprocessMessages } from "../endpoints/preprocessMessages"; + +import { generateTitleForConversation } from "./title"; +import { + assistantHasDynamicPrompt, + assistantHasWebSearch, + getAssistantById, + processPreprompt, +} from "./assistant"; +import { pickTools, runTools } from "./tools"; +import type { WebSearch } from "$lib/types/WebSearch"; +import { + type MessageUpdate, + MessageUpdateType, + MessageUpdateStatus, +} from "$lib/types/MessageUpdate"; +import { generate } from "./generate"; +import { mergeAsyncGenerators } from "$lib/utils/mergeAsyncGenerators"; +import type { TextGenerationContext } from "./types"; + +export async function* textGeneration(ctx: TextGenerationContext) { + yield* mergeAsyncGenerators([ + textGenerationWithoutTitle(ctx), + generateTitleForConversation(ctx.conv), + ]); +} + +async function* textGenerationWithoutTitle( + ctx: TextGenerationContext +): AsyncGenerator { + yield { + type: MessageUpdateType.Status, + status: MessageUpdateStatus.Started, + }; + + ctx.assistant ??= await getAssistantById(ctx.conv.assistantId); + const { model, conv, messages, assistant, isContinue, webSearch, toolsPreference } = ctx; + const convId = conv._id; + + // perform websearch if requested + // it can be because the user toggled the webSearch or because the assistant has webSearch enabled + // if tools are enabled, we don't perform it here since we will add the websearch as a tool + let webSearchResult: WebSearch | undefined; + if ( + !isContinue && + !model.tools && + ((webSearch && !conv.assistantId) || assistantHasWebSearch(assistant)) + ) { + webSearchResult = yield* runWebSearch(conv, messages, assistant?.rag); + } + + let preprompt = conv.preprompt; + if (assistantHasDynamicPrompt(assistant) && preprompt) { + preprompt = await processPreprompt(preprompt); + if (messages[0].from === "system") messages[0].content = preprompt; + } + + const tools = pickTools(toolsPreference, Boolean(assistant)); + const toolResults = yield* runTools(ctx, tools, preprompt); + + const processedMessages = await preprocessMessages(messages, webSearchResult, convId); + yield* generate({ ...ctx, messages: processedMessages }, toolResults, preprompt); +} diff --git a/src/lib/server/summarize.ts b/src/lib/server/textGeneration/title.ts similarity index 60% rename from src/lib/server/summarize.ts rename to src/lib/server/textGeneration/title.ts index 033e1ae703356aebb01cab85b3b8fba2018faa3f..3d3359d0dc4441b9a7ce6f513f3e27e16c79af39 100644 --- a/src/lib/server/summarize.ts +++ b/src/lib/server/textGeneration/title.ts @@ -1,14 +1,41 @@ import { env } from "$env/dynamic/private"; import { generateFromDefaultEndpoint } from "$lib/server/generateFromDefaultEndpoint"; -import type { EndpointMessage } from "./endpoints/endpoints"; +import type { EndpointMessage } from "../endpoints/endpoints"; import { logger } from "$lib/server/logger"; +import { MessageUpdateType, type MessageUpdate } from "$lib/types/MessageUpdate"; +import type { Conversation } from "$lib/types/Conversation"; -export async function summarize(prompt: string) { +export async function* generateTitleForConversation( + conv: Conversation +): AsyncGenerator { + try { + const userMessage = conv.messages.find((m) => m.from === "user"); + // HACK: detect if the conversation is new + if (conv.title !== "New Chat" || !userMessage) return; + + const prompt = userMessage.content; + const title = (await generateTitle(prompt)) ?? "New Chat"; + + yield { + type: MessageUpdateType.Title, + title, + }; + } catch (cause) { + console.error(Error("Failed whilte generating title for conversation", { cause })); + } +} + +export async function generateTitle(prompt: string) { if (!env.LLM_SUMMERIZATION) { return prompt.split(/\s+/g).slice(0, 5).join(" "); } const messages: Array = [ + { + from: "system", + content: + "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", + }, { from: "user", content: "Who is the president of Gabon?" }, { from: "assistant", content: "🇬🇦 President of Gabon" }, { from: "user", content: "Who is Julien Chaumond?" }, @@ -23,6 +50,8 @@ export async function summarize(prompt: string) { { from: "assistant", content: "🎥 Favorite movie" }, { from: "user", content: "Explain the concept of artificial intelligence in one sentence" }, { from: "assistant", content: "🤖 AI definition" }, + { from: "user", content: "Draw a cute cat" }, + { from: "assistant", content: "🐱 Cute cat drawing" }, { from: "user", content: prompt }, ]; diff --git a/src/lib/server/textGeneration/tools.ts b/src/lib/server/textGeneration/tools.ts new file mode 100644 index 0000000000000000000000000000000000000000..6b4e053890fa21132479e08b51a98aac8a064438 --- /dev/null +++ b/src/lib/server/textGeneration/tools.ts @@ -0,0 +1,198 @@ +import { ToolResultStatus, type ToolCall, type ToolResult } from "$lib/types/Tool"; +import { v4 as uuidV4 } from "uuid"; +import JSON5 from "json5"; +import type { BackendTool, BackendToolContext } from "../tools"; +import { + MessageToolUpdateType, + MessageUpdateStatus, + MessageUpdateType, + type MessageUpdate, +} from "$lib/types/MessageUpdate"; +import type { TextGenerationContext } from "./types"; + +import { allTools } from "../tools"; +import directlyAnswer from "../tools/directlyAnswer"; +import websearch from "../tools/web/search"; +import { z } from "zod"; +import { logger } from "../logger"; +import { toolHasName } from "../tools/utils"; +import type { MessageFile } from "$lib/types/Message"; +import { mergeAsyncGenerators } from "$lib/utils/mergeAsyncGenerators"; + +function makeFilesPrompt(files: MessageFile[], fileMessageIndex: number): string { + if (files.length === 0) { + return "The user has not uploaded any files. Do not attempt to use any tools that require files"; + } + + const stringifiedFiles = files + .map( + (file, fileIndex) => + ` - fileMessageIndex ${fileMessageIndex} | fileIndex ${fileIndex} | ${file.name} (${file.mime})` + ) + .join("\n"); + return `Attached ${files.length} file${files.length === 1 ? "" : "s"}:\n${stringifiedFiles}`; +} + +export function pickTools( + toolsPreference: Record, + isAssistant: boolean +): BackendTool[] { + // if it's an assistant, only support websearch for now + if (isAssistant) return [directlyAnswer, websearch]; + + // filter based on tool preferences, add the tools that are on by default + return allTools.filter((el) => { + if (el.isLocked && el.isOnByDefault) return true; + return toolsPreference?.[el.name] ?? el.isOnByDefault; + }); +} + +async function* runTool( + { conv, messages, preprompt, assistant }: BackendToolContext, + tools: BackendTool[], + call: ToolCall +): AsyncGenerator { + const uuid = uuidV4(); + + const tool = tools.find((el) => toolHasName(call.name, el)); + if (!tool) { + return { call, status: ToolResultStatus.Error, message: `Could not find tool "${call.name}"` }; + } + + // Special case for directly_answer tool where we ignore + if (toolHasName(directlyAnswer.name, tool)) return; + + yield { + type: MessageUpdateType.Tool, + subtype: MessageToolUpdateType.Call, + uuid, + call, + }; + try { + const toolResult = yield* tool.call(call.parameters, { + conv, + messages, + preprompt, + assistant, + }); + yield { + type: MessageUpdateType.Tool, + subtype: MessageToolUpdateType.Result, + uuid, + result: { ...toolResult, call } as ToolResult, + }; + return { ...toolResult, call } as ToolResult; + } catch (cause) { + console.error(Error(`Failed while running tool ${call.name}`), { cause }); + return { + call, + status: ToolResultStatus.Error, + message: cause instanceof Error ? cause.message : String(cause), + }; + } +} + +export async function* runTools( + { endpoint, conv, messages, assistant }: TextGenerationContext, + tools: BackendTool[], + preprompt?: string +): AsyncGenerator { + const calls: ToolCall[] = []; + + const messagesWithFilesPrompt = messages.map((message, idx) => { + if (!message.files?.length) return message; + return { + ...message, + content: `${message.content}\n${makeFilesPrompt(message.files, idx)}`, + }; + }); + + // do the function calling bits here + for await (const output of await endpoint({ + messages: messagesWithFilesPrompt, + preprompt, + generateSettings: assistant?.generateSettings, + tools, + })) { + // model natively supports tool calls + if (output.token.toolCalls) { + calls.push(...output.token.toolCalls); + continue; + } + + // look for a code blocks of ```json and parse them + // if they're valid json, add them to the calls array + if (output.generated_text) { + const codeBlocks = Array.from(output.generated_text.matchAll(/```json\n(.*?)```/gs)); + if (codeBlocks.length === 0) continue; + + // grab only the capture group from the regex match + for (const [, block] of codeBlocks) { + try { + calls.push( + ...JSON5.parse(block).filter(isExternalToolCall).map(externalToToolCall).filter(Boolean) + ); + } catch (cause) { + // error parsing the calls + yield { + type: MessageUpdateType.Status, + status: MessageUpdateStatus.Error, + message: cause instanceof Error ? cause.message : String(cause), + }; + } + } + } + } + + const toolContext: BackendToolContext = { conv, messages, preprompt, assistant }; + const toolResults: (ToolResult | undefined)[] = yield* mergeAsyncGenerators( + calls.map((call) => runTool(toolContext, tools, call)) + ); + return toolResults.filter((result): result is ToolResult => result !== undefined); +} + +const externalToolCall = z.object({ + tool_name: z.string(), + parameters: z.record(z.any()), +}); + +type ExternalToolCall = z.infer; + +function isExternalToolCall(call: unknown): call is ExternalToolCall { + return externalToolCall.safeParse(call).success; +} + +function externalToToolCall(call: ExternalToolCall): ToolCall | undefined { + // Convert - to _ since some models insist on using _ instead of - + const tool = allTools.find((tool) => toolHasName(call.tool_name, tool)); + if (!tool) { + logger.debug(`Model requested tool that does not exist: "${call.tool_name}". Skipping tool...`); + return; + } + + const parametersWithDefaults: Record = {}; + + for (const [key, definition] of Object.entries(tool.parameterDefinitions)) { + const value = call.parameters[key]; + + // Required so ensure it's there, otherwise return undefined + if (definition.required) { + if (value === undefined) { + logger.debug( + `Model requested tool "${call.tool_name}" but was missing required parameter "${key}". Skipping tool...` + ); + return; + } + parametersWithDefaults[key] = value; + continue; + } + + // Optional so use default if not there + parametersWithDefaults[key] = value ?? definition.default; + } + + return { + name: call.tool_name, + parameters: parametersWithDefaults, + }; +} diff --git a/src/lib/server/textGeneration/types.ts b/src/lib/server/textGeneration/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..a615861fbec9d70c00eb90777d24676d4ac5c0db --- /dev/null +++ b/src/lib/server/textGeneration/types.ts @@ -0,0 +1,17 @@ +import type { ProcessedModel } from "../models"; +import type { Endpoint } from "../endpoints/endpoints"; +import type { Conversation } from "$lib/types/Conversation"; +import type { Message } from "$lib/types/Message"; +import type { Assistant } from "$lib/types/Assistant"; + +export interface TextGenerationContext { + model: ProcessedModel; + endpoint: Endpoint; + conv: Conversation; + messages: Message[]; + assistant?: Pick; + isContinue: boolean; + webSearch: boolean; + toolsPreference: Record; + promptedAt: Date; +} diff --git a/src/lib/server/tools/calculator.ts b/src/lib/server/tools/calculator.ts new file mode 100644 index 0000000000000000000000000000000000000000..574e40ccbef0a0329e10522e5f97b3a05a6e22b3 --- /dev/null +++ b/src/lib/server/tools/calculator.ts @@ -0,0 +1,37 @@ +import { ToolResultStatus } from "$lib/types/Tool"; +import type { BackendTool } from "."; +import vm from "node:vm"; + +const calculator: BackendTool = { + name: "query_calculator", + displayName: "Calculator", + description: + "A simple calculator, takes a string containing a mathematical expression and returns the answer. Only supports +, -, *, ** (power) and /, as well as parenthesis ().", + isOnByDefault: true, + parameterDefinitions: { + equation: { + description: + "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.", + type: "formula", + required: true, + }, + }, + async *call(params) { + try { + const blocks = String(params.equation).split("\n"); + const query = blocks[blocks.length - 1].replace(/[^-()\d/*+.]/g, ""); + + return { + status: ToolResultStatus.Success, + outputs: [{ calculator: `${query} = ${vm.runInNewContext(query)}` }], + }; + } catch (e) { + return { + status: ToolResultStatus.Error, + message: "Invalid expression", + }; + } + }, +}; + +export default calculator; diff --git a/src/lib/server/tools/directlyAnswer.ts b/src/lib/server/tools/directlyAnswer.ts new file mode 100644 index 0000000000000000000000000000000000000000..a0a74c20fef07a68eec73011a0bf931a98d4c824 --- /dev/null +++ b/src/lib/server/tools/directlyAnswer.ts @@ -0,0 +1,20 @@ +import { ToolResultStatus } from "$lib/types/Tool"; +import type { BackendTool } from "."; + +const directlyAnswer: BackendTool = { + name: "directly_answer", + isOnByDefault: true, + isHidden: true, + isLocked: true, + description: "Use this tool to let the user know you wish to answer directly", + parameterDefinitions: {}, + async *call() { + return { + status: ToolResultStatus.Success, + outputs: [], + display: false, + }; + }, +}; + +export default directlyAnswer; diff --git a/src/lib/server/tools/documentParser.ts b/src/lib/server/tools/documentParser.ts new file mode 100644 index 0000000000000000000000000000000000000000..c0cb6d142a9cdd1170f6dea5d9a749c658bc752a --- /dev/null +++ b/src/lib/server/tools/documentParser.ts @@ -0,0 +1,69 @@ +import type { BackendTool } from "."; +import { ToolResultStatus } from "$lib/types/Tool"; +import { callSpace } from "./utils"; +import { downloadFile } from "$lib/server/files/downloadFile"; + +type PdfParserInput = [Blob /* pdf */, string /* filename */]; +type PdfParserOutput = [string /* markdown */, Record /* metadata */]; + +const documentParser: BackendTool = { + name: "document_parser", + displayName: "Document Parser", + description: "Use this tool to parse any document and get its content in markdown format.", + isOnByDefault: true, + parameterDefinitions: { + fileMessageIndex: { + description: "Index of the message containing the document file to parse", + type: "number", + required: true, + }, + fileIndex: { + description: "Index of the document file to parse", + type: "number", + required: true, + }, + }, + async *call({ fileMessageIndex, fileIndex }, { conv, messages }) { + fileMessageIndex = Number(fileMessageIndex); + fileIndex = Number(fileIndex); + + const message = messages[fileMessageIndex]; + const files = message?.files ?? []; + if (!files || files.length === 0) { + return { + status: ToolResultStatus.Error, + message: "User did not provide a pdf to parse", + }; + } + if (fileIndex >= files.length) { + return { + status: ToolResultStatus.Error, + message: "Model provided an invalid file index", + }; + } + + const file = files[fileIndex]; + const fileBlob = await downloadFile(files[fileIndex].value, conv._id) + .then((file) => fetch(`data:${file.mime};base64,${file.value}`)) + .then((res) => res.blob()); + + const outputs = await callSpace( + "huggingchat/document-parser", + "predict", + [fileBlob, file.name] + ); + + let documentMarkdown = outputs[0]; + // TODO: quick fix for avoiding context limit. eventually should use the tokenizer + if (documentMarkdown.length > 30_000) { + documentMarkdown = documentMarkdown.slice(0, 30_000) + "\n\n... (truncated)"; + } + return { + status: ToolResultStatus.Success, + outputs: [{ [file.name]: documentMarkdown }], + display: false, + }; + }, +}; + +export default documentParser; diff --git a/src/lib/server/tools/images/editing.ts b/src/lib/server/tools/images/editing.ts new file mode 100644 index 0000000000000000000000000000000000000000..6623030e4b445c807542ad3c4ccfb8ddf034b540 --- /dev/null +++ b/src/lib/server/tools/images/editing.ts @@ -0,0 +1,107 @@ +import type { BackendTool } from ".."; +import { uploadFile } from "../../files/uploadFile"; +import { ToolResultStatus } from "$lib/types/Tool"; +import { MessageUpdateType } from "$lib/types/MessageUpdate"; +import { callSpace, type GradioImage } from "../utils"; +import { downloadFile } from "$lib/server/files/downloadFile"; + +type ImageEditingInput = [ + Blob /* image */, + string /* prompt */, + string /* negative prompt */, + number /* guidance scale */, + number /* steps */ +]; +type ImageEditingOutput = [GradioImage]; + +const imageEditing: BackendTool = { + name: "image_editing", + displayName: "Image Editing", + description: "Use this tool to edit an image from a prompt.", + isOnByDefault: true, + parameterDefinitions: { + prompt: { + description: + "A prompt to generate an image from. Describe the image visually in simple terms, separate terms with a comma.", + type: "string", + required: true, + }, + fileMessageIndex: { + description: "Index of the message containing the file to edit", + type: "number", + required: true, + }, + fileIndex: { + description: "Index of the file to edit", + type: "number", + required: true, + }, + }, + async *call({ prompt, fileMessageIndex, fileIndex }, { conv, messages }) { + prompt = String(prompt); + fileMessageIndex = Number(fileMessageIndex); + fileIndex = Number(fileIndex); + + const message = messages[fileMessageIndex]; + const images = message?.files ?? []; + if (!images || images.length === 0) { + return { + status: ToolResultStatus.Error, + message: "User did not provide an image to edit.", + }; + } + if (fileIndex >= images.length) { + return { + status: ToolResultStatus.Error, + message: "Model provided an invalid file index", + }; + } + if (!images[fileIndex].mime.startsWith("image/")) { + return { + status: ToolResultStatus.Error, + message: "Model provided a file indx which is not an image", + }; + } + + // todo: should handle multiple images + const image = await downloadFile(images[fileIndex].value, conv._id) + .then((file) => fetch(`data:${file.mime};base64,${file.value}`)) + .then((res) => res.blob()); + + const outputs = await callSpace( + "multimodalart/cosxl", + "run_edit", + [ + image, + prompt, + "", // negative prompt + 7, // guidance scale + 20, // steps + ] + ); + + const outputImage = await fetch(outputs[0].url) + .then((res) => res.blob()) + .then((blob) => new File([blob], outputs[0].orig_name, { type: blob.type })) + .then((file) => uploadFile(file, conv)); + + yield { + type: MessageUpdateType.File, + name: outputImage.name, + sha: outputImage.value, + mime: outputImage.mime, + }; + + return { + status: ToolResultStatus.Success, + outputs: [ + { + 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.`, + }, + ], + display: false, + }; + }, +}; + +export default imageEditing; diff --git a/src/lib/server/tools/images/generation.ts b/src/lib/server/tools/images/generation.ts new file mode 100644 index 0000000000000000000000000000000000000000..0e8d03dc9a6a4ba9e336b4bc483aabb5aad1ab19 --- /dev/null +++ b/src/lib/server/tools/images/generation.ts @@ -0,0 +1,92 @@ +import type { BackendTool } from ".."; +import { uploadFile } from "../../files/uploadFile"; +import { ToolResultStatus } from "$lib/types/Tool"; +import { MessageUpdateType } from "$lib/types/MessageUpdate"; +import { callSpace, type GradioImage } from "../utils"; + +type ImageGenerationInput = [ + number /* number (numeric value between 1 and 8) in 'Number of Images' Slider component */, + number /* number in 'Image Height' Number component */, + number /* number in 'Image Width' Number component */, + string /* prompt */, + number /* seed random */ +]; +type ImageGenerationOutput = [{ image: GradioImage }[]]; + +const imageGeneration: BackendTool = { + name: "image_generation", + displayName: "Image Generation", + description: "Use this tool to generate an image from a prompt.", + isOnByDefault: true, + parameterDefinitions: { + prompt: { + description: + "A prompt to generate an image from. Describe the image visually in simple terms, separate terms with a comma.", + type: "string", + required: true, + }, + numberOfImages: { + description: "Number of images to generate, between 1 and 8.", + type: "number", + required: false, + default: 1, + }, + width: { + description: "Width of the generated image.", + type: "number", + required: false, + default: 1024, + }, + height: { + description: "Height of the generated image.", + type: "number", + required: false, + default: 1024, + }, + }, + async *call({ prompt, numberOfImages }, { conv }) { + const outputs = await callSpace( + "ByteDance/Hyper-SDXL-1Step-T2I", + "/process_image", + [ + Number(numberOfImages), // number (numeric value between 1 and 8) in 'Number of Images' Slider component + 512, // number in 'Image Height' Number component + 512, // number in 'Image Width' Number component + String(prompt), // prompt + Math.floor(Math.random() * 1000), // seed random + ] + ); + const imageBlobs = await Promise.all( + outputs[0].map((output) => + fetch(output.image.url) + .then((res) => res.blob()) + .then( + (blob) => + new File([blob], `${prompt}.${blob.type.split("/")[1] ?? "png"}`, { type: blob.type }) + ) + .then((file) => uploadFile(file, conv)) + ) + ); + + for (const image of imageBlobs) { + yield { + type: MessageUpdateType.File, + name: image.name, + sha: image.value, + mime: image.mime, + }; + } + + return { + status: ToolResultStatus.Success, + outputs: [ + { + 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.`, + }, + ], + display: false, + }; + }, +}; + +export default imageGeneration; diff --git a/src/lib/server/tools/index.ts b/src/lib/server/tools/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..9751997e89ca9241977c9ad90f356b98d17eca23 --- /dev/null +++ b/src/lib/server/tools/index.ts @@ -0,0 +1,37 @@ +import type { Assistant } from "$lib/types/Assistant"; +import type { Conversation } from "$lib/types/Conversation"; +import type { Message } from "$lib/types/Message"; +import type { MessageUpdate } from "$lib/types/MessageUpdate"; +import type { Tool, ToolResult } from "$lib/types/Tool"; + +import calculator from "./calculator"; +import directlyAnswer from "./directlyAnswer"; +import imageEditing from "./images/editing"; +import imageGeneration from "./images/generation"; +import documentParser from "./documentParser"; +import fetchUrl from "./web/url"; +import websearch from "./web/search"; + +export interface BackendToolContext { + conv: Conversation; + messages: Message[]; + preprompt?: string; + assistant?: Pick; +} + +export interface BackendTool extends Tool { + call( + params: Record, + context: BackendToolContext + ): AsyncGenerator, undefined>; +} + +export const allTools: BackendTool[] = [ + directlyAnswer, + websearch, + imageGeneration, + fetchUrl, + imageEditing, + documentParser, + calculator, +]; diff --git a/src/lib/server/tools/utils.ts b/src/lib/server/tools/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..24f9d07381d94131606bd92ccd2f67702224722b --- /dev/null +++ b/src/lib/server/tools/utils.ts @@ -0,0 +1,29 @@ +import { env } from "$env/dynamic/private"; +import { Client } from "@gradio/client"; + +export type GradioImage = { + path: string; + url: string; + orig_name: string; + is_stream: boolean; + meta: Record; +}; + +type GradioResponse = { + data: unknown[]; +}; + +export async function callSpace( + name: string, + func: string, + parameters: TInput +): Promise { + const client = await Client.connect(name, { + hf_token: (env.HF_TOKEN ?? env.HF_ACCESS_TOKEN) as unknown as `hf_${string}`, + }); + return await client + .predict(func, parameters) + .then((res) => (res as unknown as GradioResponse).data as TOutput); +} + +export { toolHasName } from "$lib/utils/tools"; diff --git a/src/lib/server/tools/web/search.ts b/src/lib/server/tools/web/search.ts new file mode 100644 index 0000000000000000000000000000000000000000..da5397e3d39e2c36d5509aa1b5f2d06ef0a4ad24 --- /dev/null +++ b/src/lib/server/tools/web/search.ts @@ -0,0 +1,33 @@ +import { ToolResultStatus } from "$lib/types/Tool"; +import type { BackendTool } from ".."; +import { runWebSearch } from "../../websearch/runWebSearch"; + +const websearch: BackendTool = { + name: "websearch", + displayName: "Web Search", + isOnByDefault: true, + description: + "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.", + parameterDefinitions: { + query: { + required: true, + type: "string", + description: + "A search query which will be used to fetch the most relevant snippets regarding the user's query", + }, + }, + async *call({ query }, { conv, assistant, messages }) { + const webSearchToolResults = yield* runWebSearch(conv, messages, assistant?.rag, String(query)); + const chunks = webSearchToolResults?.contextSources + .map(({ context }) => context) + .join("\n------------\n"); + + return { + status: ToolResultStatus.Success, + outputs: [{ websearch: chunks }], + display: false, + }; + }, +}; + +export default websearch; diff --git a/src/lib/server/tools/web/url.ts b/src/lib/server/tools/web/url.ts new file mode 100644 index 0000000000000000000000000000000000000000..b5bc087d5933531e4fd64f5acc7e8846e84f1ebe --- /dev/null +++ b/src/lib/server/tools/web/url.ts @@ -0,0 +1,32 @@ +import { stringifyMarkdownElementTree } from "$lib/server/websearch/markdown/utils/stringify"; +import { scrapeUrl } from "$lib/server/websearch/scrape/scrape"; +import { ToolResultStatus } from "$lib/types/Tool"; +import type { BackendTool } from ".."; + +const fetchUrl: BackendTool = { + name: "fetch_url", + displayName: "URL Fetcher", + description: "A tool that can be used to fetch an URL and return the content directly.", + isOnByDefault: true, + parameterDefinitions: { + url: { + description: "The url that should be fetched.", + type: "str", + required: true, + }, + }, + async *call(params) { + const blocks = String(params.url).split("\n"); + const url = blocks[blocks.length - 1]; + + const { title, markdownTree } = await scrapeUrl(url, Infinity); + + return { + status: ToolResultStatus.Success, + outputs: [{ title, text: stringifyMarkdownElementTree(markdownTree) }], + display: false, + }; + }, +}; + +export default fetchUrl; diff --git a/src/lib/server/websearch/markdown/utils/stringify.ts b/src/lib/server/websearch/markdown/utils/stringify.ts index ab9e0ad90cfff38ad5a2bc68a661d50d6ee0c3dc..0e6941f7182a1dd9f04efbf042f7abb1e73c98b7 100644 --- a/src/lib/server/websearch/markdown/utils/stringify.ts +++ b/src/lib/server/websearch/markdown/utils/stringify.ts @@ -26,6 +26,13 @@ export function stringifyMarkdownElement(elem: MarkdownElement): string { return `${content}\n\n`; } +/** Converts a tree of markdown elements to a string with formatting */ +export function stringifyMarkdownElementTree(elem: MarkdownElement): string { + const stringified = stringifyMarkdownElement(elem); + if (!("children" in elem)) return stringified; + return stringified + elem.children.map(stringifyMarkdownElementTree).join(""); +} + // ----- HTML Elements ----- /** Ignores all non-inline tag types and grabs their text. Converts inline tags to markdown */ diff --git a/src/lib/server/websearch/runWebSearch.ts b/src/lib/server/websearch/runWebSearch.ts index 981259a2cf219c632ad865c3941306152e06a522..c57d4a1fc4fa5921f5a85f47ca8bcc7e4a69c4bb 100644 --- a/src/lib/server/websearch/runWebSearch.ts +++ b/src/lib/server/websearch/runWebSearch.ts @@ -1,35 +1,35 @@ import { defaultEmbeddingModel, embeddingModels } from "$lib/server/embeddingModels"; import type { Conversation } from "$lib/types/Conversation"; -import type { MessageUpdate } from "$lib/types/MessageUpdate"; import type { Message } from "$lib/types/Message"; import type { WebSearch, WebSearchScrapedSource } from "$lib/types/WebSearch"; import type { Assistant } from "$lib/types/Assistant"; +import type { MessageWebSearchUpdate } from "$lib/types/MessageUpdate"; import { search } from "./search/search"; import { scrape } from "./scrape/scrape"; import { findContextSources } from "./embed/embed"; import { removeParents } from "./markdown/tree"; +import { + makeErrorUpdate, + makeFinalAnswerUpdate, + makeGeneralUpdate, + makeSourcesUpdate, +} from "./update"; +import { mergeAsyncGenerators } from "$lib/utils/mergeAsyncGenerators"; const MAX_N_PAGES_TO_SCRAPE = 8 as const; const MAX_N_PAGES_TO_EMBED = 5 as const; -export type AppendUpdate = (message: string, args?: string[], type?: "error" | "update") => void; -const makeAppendUpdate = - (updatePad: (upd: MessageUpdate) => void): AppendUpdate => - (message, args, type) => - updatePad({ type: "webSearch", messageType: type ?? "update", message, args }); - -export async function runWebSearch( +export async function* runWebSearch( conv: Conversation, messages: Message[], - updatePad: (upd: MessageUpdate) => void, - ragSettings?: Assistant["rag"] -): Promise { + ragSettings?: Assistant["rag"], + query?: string +): AsyncGenerator { const prompt = messages[messages.length - 1].content; const createdAt = new Date(); const updatedAt = new Date(); - const appendUpdate = makeAppendUpdate(updatePad); try { const embeddingModel = @@ -39,29 +39,26 @@ export async function runWebSearch( } // Search the web - const { searchQuery, pages } = await search(messages, ragSettings, appendUpdate); + const { searchQuery, pages } = yield* search(messages, ragSettings, query); if (pages.length === 0) throw Error("No results found for this search query"); // Scrape pages - appendUpdate("Browsing search results"); + yield makeGeneralUpdate({ message: "Browsing search results" }); - const scrapedPages = await Promise.all( - pages - .slice(0, MAX_N_PAGES_TO_SCRAPE) - .map(scrape(appendUpdate, embeddingModel.chunkCharLength)) - ).then((allScrapedPages) => - allScrapedPages - .filter((p): p is WebSearchScrapedSource => Boolean(p)) - .filter((p) => p.page.markdownTree.children.length > 0) - .slice(0, MAX_N_PAGES_TO_EMBED) + const allScrapedPages = yield* mergeAsyncGenerators( + pages.slice(0, MAX_N_PAGES_TO_SCRAPE).map(scrape(embeddingModel.chunkCharLength)) ); + const scrapedPages = allScrapedPages + .filter((p): p is WebSearchScrapedSource => Boolean(p)) + .filter((p) => p.page.markdownTree.children.length > 0) + .slice(0, MAX_N_PAGES_TO_EMBED); if (!scrapedPages.length) { throw Error(`No text found in the first ${MAX_N_PAGES_TO_SCRAPE} results`); } // Chunk the text of each of the elements and find the most similar chunks to the prompt - appendUpdate("Extracting relevant information"); + yield makeGeneralUpdate({ message: "Extracting relevant information" }); const contextSources = await findContextSources(scrapedPages, prompt, embeddingModel).then( (ctxSources) => ctxSources.map((source) => ({ @@ -69,14 +66,9 @@ export async function runWebSearch( page: { ...source.page, markdownTree: removeParents(source.page.markdownTree) }, })) ); - updatePad({ - type: "webSearch", - messageType: "sources", - message: "sources", - sources: contextSources, - }); + yield makeSourcesUpdate(contextSources); - return { + const webSearch: WebSearch = { prompt, searchQuery, results: scrapedPages.map(({ page, ...source }) => ({ @@ -87,11 +79,14 @@ export async function runWebSearch( createdAt, updatedAt, }; + yield makeFinalAnswerUpdate(webSearch); + return webSearch; } catch (searchError) { const message = searchError instanceof Error ? searchError.message : String(searchError); console.error(message); - appendUpdate("An error occurred", [JSON.stringify(message)], "error"); - return { + yield makeErrorUpdate({ message: "An error occurred", args: [message] }); + + const webSearch: WebSearch = { prompt, searchQuery: "", results: [], @@ -99,5 +94,7 @@ export async function runWebSearch( createdAt, updatedAt, }; + yield makeFinalAnswerUpdate(webSearch); + return webSearch; } } diff --git a/src/lib/server/websearch/scrape/playwright.ts b/src/lib/server/websearch/scrape/playwright.ts index 260bc4443982a61288a2fbc92dbea4392f90856a..2046df0de31eb359bd3f518447db24927d8b7894 100644 --- a/src/lib/server/websearch/scrape/playwright.ts +++ b/src/lib/server/websearch/scrape/playwright.ts @@ -4,6 +4,7 @@ import { devices, type Page, type BrowserContextOptions, + type Response, } from "playwright"; import { PlaywrightBlocker } from "@cliqz/adblocker-playwright"; import { env } from "$env/dynamic/private"; @@ -44,16 +45,16 @@ async function initPlaywrightService() { return Object.freeze({ ctx, blocker }); } -export async function loadPage(url: string): Promise { +export async function loadPage(url: string): Promise<{ res?: Response; page: Page }> { if (!playwrightService) playwrightService = initPlaywrightService(); const { ctx, blocker } = await playwrightService; const page = await ctx.newPage(); await blocker.enableBlockingInPage(page); - await page.goto(url, { waitUntil: "load", timeout: 2000 }).catch(() => { + const res = await page.goto(url, { waitUntil: "load", timeout: 3500 }).catch(() => { console.warn(`Failed to load page within 2s: ${url}`); }); - return page; + return { res: res ?? undefined, page }; } diff --git a/src/lib/server/websearch/scrape/scrape.ts b/src/lib/server/websearch/scrape/scrape.ts index c2341f5d980a999cee1fbd86a2c9e3c3d16244f3..9029e8c96c1a818de22708c7a6a47e65edafaf6b 100644 --- a/src/lib/server/websearch/scrape/scrape.ts +++ b/src/lib/server/websearch/scrape/scrape.ts @@ -1,26 +1,52 @@ -import type { AppendUpdate } from "../runWebSearch"; import type { WebSearchScrapedSource, WebSearchSource } from "$lib/types/WebSearch"; +import type { MessageWebSearchUpdate } from "$lib/types/MessageUpdate"; import { loadPage } from "./playwright"; import { spatialParser } from "./parser"; import { htmlToMarkdownTree } from "../markdown/tree"; import { timeout } from "$lib/utils/timeout"; +import { makeErrorUpdate, makeGeneralUpdate } from "../update"; -export const scrape = - (appendUpdate: AppendUpdate, maxCharsPerElem: number) => - async (source: WebSearchSource): Promise => { +export const scrape = (maxCharsPerElem: number) => + async function* ( + source: WebSearchSource + ): AsyncGenerator { try { const page = await scrapeUrl(source.link, maxCharsPerElem); - appendUpdate("Browsing webpage", [source.link]); + yield makeGeneralUpdate({ message: "Browsing webpage", args: [source.link] }); return { ...source, page }; } catch (e) { const message = e instanceof Error ? e.message : String(e); - appendUpdate("Failed to parse webpage", [message, source.link], "error"); + yield makeErrorUpdate({ message: "Failed to parse webpage", args: [message, source.link] }); } }; export async function scrapeUrl(url: string, maxCharsPerElem: number) { - const page = await loadPage(url); + const { res, page } = await loadPage(url); + + if (!res) throw Error("Failed to load page"); + + // Check if it's a non-html content type that we can handle directly + // TODO: direct mappings to markdown can be added for markdown, csv and others + const contentType = res.headers()["content-type"] ?? ""; + if ( + contentType.includes("text/plain") || + contentType.includes("text/markdown") || + contentType.includes("application/json") || + contentType.includes("application/xml") || + contentType.includes("text/csv") + ) { + const title = await page.title(); + const content = await page.content(); + return { + title, + markdownTree: htmlToMarkdownTree( + title, + [{ tagName: "p", attributes: {}, content: [content] }], + maxCharsPerElem + ), + }; + } return timeout(page.evaluate(spatialParser), 2000) .then(({ elements, ...parsed }) => ({ diff --git a/src/lib/server/websearch/search/search.ts b/src/lib/server/websearch/search/search.ts index b497b8b1e6a5d869a933d709b3d8814b0f0120e0..9f232a0ea9826c9581685d79348661adac826dff 100644 --- a/src/lib/server/websearch/search/search.ts +++ b/src/lib/server/websearch/search/search.ts @@ -1,7 +1,6 @@ import type { WebSearchSource } from "$lib/types/WebSearch"; import type { Message } from "$lib/types/Message"; import type { Assistant } from "$lib/types/Assistant"; -import type { AppendUpdate } from "../runWebSearch"; import { getWebSearchProvider, searchWeb } from "./endpoints"; import { generateQuery } from "./generateQuery"; import { isURLStringLocal } from "$lib/server/isURLLocal"; @@ -10,30 +9,36 @@ import { isURL } from "$lib/utils/isUrl"; import z from "zod"; import JSON5 from "json5"; import { env } from "$env/dynamic/private"; +import { makeGeneralUpdate } from "../update"; +import type { MessageWebSearchUpdate } from "$lib/types/MessageUpdate"; const listSchema = z.array(z.string()).default([]); const allowList = listSchema.parse(JSON5.parse(env.WEBSEARCH_ALLOWLIST)); const blockList = listSchema.parse(JSON5.parse(env.WEBSEARCH_BLOCKLIST)); -export async function search( +export async function* search( messages: Message[], - ragSettings: Assistant["rag"] | undefined, - appendUpdate: AppendUpdate -): Promise<{ searchQuery: string; pages: WebSearchSource[] }> { + ragSettings?: Assistant["rag"], + query?: string +): AsyncGenerator< + MessageWebSearchUpdate, + { searchQuery: string; pages: WebSearchSource[] }, + undefined +> { if (ragSettings && ragSettings?.allowedLinks.length > 0) { - appendUpdate("Using links specified in Assistant"); + yield makeGeneralUpdate({ message: "Using links specified in Assistant" }); return { searchQuery: "", pages: await directLinksToSource(ragSettings.allowedLinks).then(filterByBlockList), }; } - const searchQuery = await generateQuery(messages); - appendUpdate(`Searching ${getWebSearchProvider()}`, [searchQuery]); + const searchQuery = query ?? (await generateQuery(messages)); + yield makeGeneralUpdate({ message: `Searching ${getWebSearchProvider()}`, args: [searchQuery] }); // handle the global and (optional) rag lists if (ragSettings && ragSettings?.allowedDomains.length > 0) { - appendUpdate("Filtering on specified domains"); + yield makeGeneralUpdate({ message: "Filtering on specified domains" }); } const filters = buildQueryFromSiteFilters( [...(ragSettings?.allowedDomains ?? []), ...allowList], diff --git a/src/lib/server/websearch/update.ts b/src/lib/server/websearch/update.ts new file mode 100644 index 0000000000000000000000000000000000000000..909e63139cf640272c3a2973d113beadc0939d3d --- /dev/null +++ b/src/lib/server/websearch/update.ts @@ -0,0 +1,46 @@ +import type { WebSearch, WebSearchSource } from "$lib/types/WebSearch"; +import { + MessageUpdateType, + MessageWebSearchUpdateType, + type MessageWebSearchErrorUpdate, + type MessageWebSearchFinishedUpdate, + type MessageWebSearchGeneralUpdate, + type MessageWebSearchSourcesUpdate, +} from "$lib/types/MessageUpdate"; + +export function makeGeneralUpdate( + update: Pick +): MessageWebSearchGeneralUpdate { + return { + type: MessageUpdateType.WebSearch, + subtype: MessageWebSearchUpdateType.Update, + ...update, + }; +} + +export function makeErrorUpdate( + update: Pick +): MessageWebSearchErrorUpdate { + return { + type: MessageUpdateType.WebSearch, + subtype: MessageWebSearchUpdateType.Error, + ...update, + }; +} + +export function makeSourcesUpdate(sources: WebSearchSource[]): MessageWebSearchSourcesUpdate { + return { + type: MessageUpdateType.WebSearch, + subtype: MessageWebSearchUpdateType.Sources, + message: "sources", + sources, + }; +} + +export function makeFinalAnswerUpdate(webSearch: WebSearch): MessageWebSearchFinishedUpdate { + return { + type: MessageUpdateType.WebSearch, + subtype: MessageWebSearchUpdateType.Finished, + webSearch, + }; +} diff --git a/src/lib/stores/settings.ts b/src/lib/stores/settings.ts index b002c05336cc2d3aad5916a00d831a71b1fd21e0..3cbbdaa9e11851b9ac5e7be71c1d3d7ad6a0cee3 100644 --- a/src/lib/stores/settings.ts +++ b/src/lib/stores/settings.ts @@ -15,6 +15,7 @@ type SettingsStore = { customPrompts: Record; recentlySaved: boolean; assistants: Array; + tools?: Record; }; type SettingsStoreWritable = Writable & { diff --git a/src/lib/types/Message.ts b/src/lib/types/Message.ts index 6791164febfd1f0d901d03dc4db760915743cab1..2f108d7f5851617200c77257c60d3e2b93c115cb 100644 --- a/src/lib/types/Message.ts +++ b/src/lib/types/Message.ts @@ -27,6 +27,7 @@ export type Message = Partial & { export type MessageFile = { type: "hash" | "base64"; + name: string; value: string; mime: string; }; diff --git a/src/lib/types/MessageUpdate.ts b/src/lib/types/MessageUpdate.ts index 9bfb25667b987f2c49437b15b00bac74780f7d2e..5f5da5a1d6559fc15fb3be94c8bb6c2584ac4170 100644 --- a/src/lib/types/MessageUpdate.ts +++ b/src/lib/types/MessageUpdate.ts @@ -1,46 +1,111 @@ -import type { WebSearchSource } from "./WebSearch"; +import type { WebSearch, WebSearchSource } from "$lib/types/WebSearch"; +import type { ToolCall, ToolResult } from "$lib/types/Tool"; -export type FinalAnswer = { - type: "finalAnswer"; - text: string; -}; +export type MessageUpdate = + | MessageStatusUpdate + | MessageTitleUpdate + | MessageToolUpdate + | MessageWebSearchUpdate + | MessageStreamUpdate + | MessageFileUpdate + | MessageFinalAnswerUpdate; -export type TextStreamUpdate = { - type: "stream"; - token: string; -}; +export enum MessageUpdateType { + Status = "status", + Title = "title", + Tool = "tool", + WebSearch = "webSearch", + Stream = "stream", + File = "file", + FinalAnswer = "finalAnswer", +} -export type AgentUpdate = { - type: "agent"; - agent: string; - content: string; - binary?: Blob; -}; +// Status +export enum MessageUpdateStatus { + Started = "started", + Error = "error", + Finished = "finished", +} +export interface MessageStatusUpdate { + type: MessageUpdateType.Status; + status: MessageUpdateStatus; + message?: string; +} -export type WebSearchUpdate = { - type: "webSearch"; - messageType: "update" | "error" | "sources"; +// Web search +export enum MessageWebSearchUpdateType { + Update = "update", + Error = "error", + Sources = "sources", + Finished = "finished", +} +interface BaseMessageWebSearchUpdate { + type: MessageUpdateType.WebSearch; + subtype: TSubType; +} +export interface MessageWebSearchErrorUpdate + extends BaseMessageWebSearchUpdate { message: string; args?: string[]; - sources?: WebSearchSource[]; -}; +} +export interface MessageWebSearchGeneralUpdate + extends BaseMessageWebSearchUpdate { + message: string; + args?: string[]; +} +export interface MessageWebSearchSourcesUpdate + extends BaseMessageWebSearchUpdate { + message: string; + sources: WebSearchSource[]; +} +export interface MessageWebSearchFinishedUpdate + extends BaseMessageWebSearchUpdate { + webSearch: WebSearch; +} +export type MessageWebSearchUpdate = + | MessageWebSearchErrorUpdate + | MessageWebSearchGeneralUpdate + | MessageWebSearchSourcesUpdate + | MessageWebSearchFinishedUpdate; -export type StatusUpdate = { - type: "status"; - status: "started" | "pending" | "finished" | "error" | "title"; - message?: string; -}; +// Tool +export enum MessageToolUpdateType { + /** A request to call a tool alongside it's parameters */ + Call = "call", + /** The result of a tool call */ + Result = "result", +} +interface MessageToolBaseUpdate { + type: MessageUpdateType.Tool; + subtype: TSubType; + uuid: string; +} +export interface MessageToolCallUpdate extends MessageToolBaseUpdate { + call: ToolCall; +} +export interface MessageToolResultUpdate + extends MessageToolBaseUpdate { + result: ToolResult; +} +export type MessageToolUpdate = MessageToolCallUpdate | MessageToolResultUpdate; -export type ErrorUpdate = { - type: "error"; - message: string; +// Everything else +export interface MessageTitleUpdate { + type: MessageUpdateType.Title; + title: string; +} +export interface MessageStreamUpdate { + type: MessageUpdateType.Stream; + token: string; +} +export interface MessageFileUpdate { + type: MessageUpdateType.File; name: string; -}; - -export type MessageUpdate = - | FinalAnswer - | TextStreamUpdate - | AgentUpdate - | WebSearchUpdate - | StatusUpdate - | ErrorUpdate; + sha: string; + mime: string; +} +export interface MessageFinalAnswerUpdate { + type: MessageUpdateType.FinalAnswer; + text: string; + interrupted: boolean; +} diff --git a/src/lib/types/Model.ts b/src/lib/types/Model.ts index c58978b9c46b7f19968cf2ea4d81d474dd06b13d..57520ea56316a751c99c4e0a2fb06e5780495993 100644 --- a/src/lib/types/Model.ts +++ b/src/lib/types/Model.ts @@ -17,4 +17,5 @@ export type Model = Pick< | "preprompt" | "multimodal" | "unlisted" + | "tools" >; diff --git a/src/lib/types/Settings.ts b/src/lib/types/Settings.ts index 5a6804e05a891cf527b3592c498350aa890101f8..a37a842a1db099b942dc2edbfe187a041818daad 100644 --- a/src/lib/types/Settings.ts +++ b/src/lib/types/Settings.ts @@ -21,6 +21,7 @@ export interface Settings extends Timestamps { customPrompts?: Record; assistants?: Assistant["_id"][]; + tools?: Record; } // TODO: move this to a constant file along with other constants @@ -30,4 +31,5 @@ export const DEFAULT_SETTINGS = { hideEmojiOnSidebar: false, customPrompts: {}, assistants: [], + tools: {}, }; diff --git a/src/lib/types/Template.ts b/src/lib/types/Template.ts index 9fb2fd36e334adccd69f0f177afc91d47fe691b6..e855e7b44f4ddc4b739ba3129f9ee74fe56b2a4a 100644 --- a/src/lib/types/Template.ts +++ b/src/lib/types/Template.ts @@ -1,6 +1,9 @@ import type { Message } from "./Message"; +import type { Tool, ToolResult } from "./Tool"; export type ChatTemplateInput = { messages: Pick[]; preprompt?: string; + tools?: Tool[]; + toolResults?: ToolResult[]; }; diff --git a/src/lib/types/Tool.ts b/src/lib/types/Tool.ts new file mode 100644 index 0000000000000000000000000000000000000000..e825da514db8e947a1e51da47b56431570da8506 --- /dev/null +++ b/src/lib/types/Tool.ts @@ -0,0 +1,51 @@ +type ToolInput = + | { + description: string; + type: string; + required: true; + } + | { + description: string; + type: string; + required: false; + default: string | number | boolean; + }; + +export interface Tool { + name: string; + displayName?: string; + description: string; + parameterDefinitions: Record; + spec?: string; + isOnByDefault?: true; // will it be toggled if the user hasn't tweaked it in settings ? + isLocked?: true; // can the user enable/disable it ? + isHidden?: true; // should it be hidden from the user ? +} + +export type ToolFront = Pick< + Tool, + "name" | "displayName" | "description" | "isOnByDefault" | "isLocked" +>; + +export enum ToolResultStatus { + Success = "success", + Error = "error", +} +interface ToolResultSuccess { + status: ToolResultStatus.Success; + call: ToolCall; + outputs: Record[]; + display?: boolean; +} +interface ToolResultError { + status: ToolResultStatus.Error; + call: ToolCall; + message: string; + display?: boolean; +} +export type ToolResult = ToolResultSuccess | ToolResultError; + +export interface ToolCall { + name: string; + parameters: Record; +} diff --git a/src/lib/utils/mergeAsyncGenerators.ts b/src/lib/utils/mergeAsyncGenerators.ts new file mode 100644 index 0000000000000000000000000000000000000000..08544298c69d75f638daaf30c14084d106dd6009 --- /dev/null +++ b/src/lib/utils/mergeAsyncGenerators.ts @@ -0,0 +1,38 @@ +type Gen = AsyncGenerator; + +type GenPromiseMap = Map< + Gen, + Promise<{ gen: Gen } & IteratorResult> +>; + +/** Merges multiple async generators into a single async generator that yields values from all of them in parallel. */ +export async function* mergeAsyncGenerators( + generators: Gen[] +): Gen { + const promises: GenPromiseMap = new Map(); + const results: Map, TReturn> = new Map(); + + for (const gen of generators) { + promises.set( + gen, + gen.next().then((result) => ({ gen, ...result })) + ); + } + + while (promises.size) { + const { gen, value, done } = await Promise.race(promises.values()); + if (done) { + results.set(gen, value as TReturn); + promises.delete(gen); + } else { + promises.set( + gen, + gen.next().then((result) => ({ gen, ...result })) + ); + yield value as T; + } + } + + const orderedResults = generators.map((gen) => results.get(gen) as TReturn); + return orderedResults; +} diff --git a/src/lib/utils/messageUpdates.ts b/src/lib/utils/messageUpdates.ts index 83929255e01917f4244a166e892939fdffef0cfa..869645d40c99af001b495ec91ed19d4abfc5435c 100644 --- a/src/lib/utils/messageUpdates.ts +++ b/src/lib/utils/messageUpdates.ts @@ -1,5 +1,39 @@ import type { MessageFile } from "$lib/types/Message"; -import type { MessageUpdate, TextStreamUpdate } from "$lib/types/MessageUpdate"; +import { + type MessageUpdate, + type MessageStreamUpdate, + type MessageToolCallUpdate, + MessageToolUpdateType, + MessageUpdateType, + type MessageToolUpdate, + type MessageWebSearchUpdate, + type MessageWebSearchGeneralUpdate, + type MessageWebSearchSourcesUpdate, + type MessageWebSearchErrorUpdate, + MessageWebSearchUpdateType, +} from "$lib/types/MessageUpdate"; + +export const isMessageWebSearchUpdate = (update: MessageUpdate): update is MessageWebSearchUpdate => + update.type === MessageUpdateType.WebSearch; +export const isMessageWebSearchGeneralUpdate = ( + update: MessageUpdate +): update is MessageWebSearchGeneralUpdate => + isMessageWebSearchUpdate(update) && update.subtype === MessageWebSearchUpdateType.Update; +export const isMessageWebSearchSourcesUpdate = ( + update: MessageUpdate +): update is MessageWebSearchSourcesUpdate => + isMessageWebSearchUpdate(update) && update.subtype === MessageWebSearchUpdateType.Sources; +export const isMessageWebSearchErrorUpdate = ( + update: MessageUpdate +): update is MessageWebSearchErrorUpdate => + isMessageWebSearchUpdate(update) && update.subtype === MessageWebSearchUpdateType.Error; + +export const isMessageToolUpdate = (update: MessageUpdate): update is MessageToolUpdate => + update.type === MessageUpdateType.Tool; +export const isMessageToolCallUpdate = (update: MessageUpdate): update is MessageToolCallUpdate => + isMessageToolUpdate(update) && update.subtype === MessageToolUpdateType.Call; +export const isMessageToolResultUpdate = (update: MessageUpdate): update is MessageToolCallUpdate => + isMessageToolUpdate(update) && update.subtype === MessageToolUpdateType.Result; type MessageUpdateRequestOptions = { base: string; @@ -8,6 +42,7 @@ type MessageUpdateRequestOptions = { isRetry: boolean; isContinue: boolean; webSearch: boolean; + tools?: Record; files?: MessageFile[]; }; export async function fetchMessageUpdates( @@ -27,6 +62,7 @@ export async function fetchMessageUpdates( is_retry: opts.isRetry, is_continue: opts.isContinue, web_search: opts.webSearch, + tools: opts.tools, files: opts.files, }), signal: abortController.signal, @@ -108,7 +144,7 @@ function parseMessageUpdates(value: string): { async function* streamMessageUpdatesToFullWords( iterator: AsyncGenerator ): AsyncGenerator { - let bufferedStreamUpdates: TextStreamUpdate[] = []; + let bufferedStreamUpdates: MessageStreamUpdate[] = []; const endAlphanumeric = /[a-zA-Z0-9À-ž'`]+$/; const beginnningAlphanumeric = /^[a-zA-Z0-9À-ž'`]+/; @@ -130,7 +166,7 @@ async function* streamMessageUpdatesToFullWords( // Combine tokens together and emit yield { - type: "stream", + type: MessageUpdateType.Stream, token: bufferedStreamUpdates .slice(lastIndexEmitted, i) .map((_) => _.token) diff --git a/src/lib/utils/tools.ts b/src/lib/utils/tools.ts new file mode 100644 index 0000000000000000000000000000000000000000..36462fc74d9176953a861883bf8ff63f99fb781f --- /dev/null +++ b/src/lib/utils/tools.ts @@ -0,0 +1,9 @@ +import type { Tool } from "$lib/types/Tool"; + +/** + * Checks if a tool's name equals a value. Replaces all hyphens with underscores before comparison + * since some models return underscores even when hyphens are used in the request. + **/ +export function toolHasName(name: string, tool: Pick): boolean { + return tool.name.replaceAll("-", "_") === name.replaceAll("-", "_"); +} diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index a79c470f6885acbd324a4ed05b530f413f512b49..71a479714dcf1c8f6f814fbe9098170f89f4b0e0 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -8,6 +8,7 @@ import { DEFAULT_SETTINGS } from "$lib/types/Settings"; import { env } from "$env/dynamic/private"; import { ObjectId } from "mongodb"; import type { ConvSidebar } from "$lib/types/ConvSidebar"; +import { allTools } from "$lib/server/tools"; export const load: LayoutServerLoad = async ({ locals, depends }) => { depends(UrlDependency.ConversationList); @@ -142,6 +143,7 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => { DEFAULT_SETTINGS.shareConversationsWithModelAuthors, customPrompts: settings?.customPrompts ?? {}, assistants: userAssistants, + tools: settings?.tools ?? {}, }, models: models.map((model) => ({ id: model.id, @@ -158,9 +160,19 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => { parameters: model.parameters, preprompt: model.preprompt, multimodal: model.multimodal, + tools: model.tools, unlisted: model.unlisted, })), oldModels, + tools: allTools + .filter((tool) => !tool.isHidden) + .map((tool) => ({ + name: tool.name, + displayName: tool.displayName, + description: tool.description, + isOnByDefault: tool.isOnByDefault, + isLocked: tool.isLocked, + })), assistants: assistants .filter((el) => userAssistantsSet.has(el._id.toString())) .map((el) => ({ diff --git a/src/routes/conversation/[id]/+page.svelte b/src/routes/conversation/[id]/+page.svelte index 806b30bd77268c5511e8eda7a6c6eb4c83ee52e6..7aa75a4cb3a60c3d86f67c96db100d0bb9b01dd4 100644 --- a/src/routes/conversation/[id]/+page.svelte +++ b/src/routes/conversation/[id]/+page.svelte @@ -11,7 +11,11 @@ import { findCurrentModel } from "$lib/utils/models"; import { webSearchParameters } from "$lib/stores/webSearchParameters"; import type { Message } from "$lib/types/Message"; - import type { MessageUpdate } from "$lib/types/MessageUpdate"; + import { + MessageUpdateStatus, + MessageUpdateType, + type MessageUpdate, + } from "$lib/types/MessageUpdate"; import titleUpdate from "$lib/stores/titleUpdate"; import file2base64 from "$lib/utils/file2base64"; import { addChildren } from "$lib/utils/tree/addChildren"; @@ -19,6 +23,7 @@ import { fetchMessageUpdates } from "$lib/utils/messageUpdates"; import { createConvTreeStore } from "$lib/stores/convTree"; import type { v4 } from "uuid"; + import { useSettingsStore } from "$lib/stores/settings.js"; export let data; @@ -77,7 +82,12 @@ const base64Files = await Promise.all( (files ?? []).map((file) => - file2base64(file).then((value) => ({ type: "base64" as const, value, mime: file.type })) + file2base64(file).then((value) => ({ + type: "base64" as const, + value, + mime: file.type, + name: file.name, + })) ) ); @@ -193,6 +203,7 @@ isRetry, isContinue, webSearch: !hasAssistant && $webSearchParameters.useSearch, + tools: $settings.tools, // preference for tools files: isRetry ? userMessage?.files : base64Files, }, messageUpdatesAbortController.signal @@ -215,32 +226,45 @@ break; } + // Remove null characters added due to remote keylogging prevention + // See server code for more details + if (update.type === MessageUpdateType.Stream) { + update.token = update.token.replaceAll("\0", ""); + } + messageUpdates.push(update); - if (update.type === "stream") { + if (update.type === MessageUpdateType.Stream) { pending = false; messageToWriteTo.content += update.token; messages = [...messages]; - } else if (update.type === "webSearch") { + } else if ( + update.type === MessageUpdateType.WebSearch || + update.type === MessageUpdateType.Tool + ) { messageToWriteTo.updates = [...(messageToWriteTo.updates ?? []), update]; messages = [...messages]; - } else if (update.type === "status") { - if (update.status === "title" && update.message) { - const convInData = data.conversations.find(({ id }) => id === $page.params.id); - if (convInData) { - convInData.title = update.message; - - $titleUpdate = { - title: update.message, - convId: $page.params.id, - }; - } - } else if (update.status === "error") { - $error = update.message ?? "An error has occurred"; + } else if ( + update.type === MessageUpdateType.Status && + update.status === MessageUpdateStatus.Error + ) { + $error = update.message ?? "An error has occurred"; + } else if (update.type === MessageUpdateType.Title) { + const convInData = data.conversations.find(({ id }) => id === $page.params.id); + if (convInData) { + convInData.title = update.title; + + $titleUpdate = { + title: update.title, + convId: $page.params.id, + }; } - } else if (update.type === "error") { - error.set(update.message); - messageUpdatesAbortController.abort(); + } else if (update.type === MessageUpdateType.File) { + messageToWriteTo.files = [ + ...(messageToWriteTo.files ?? []), + { type: "hash", value: update.sha, mime: update.mime, name: update.name }, + ]; + messages = [...messages]; } } @@ -358,6 +382,7 @@ $: title = data.conversations.find((conv) => conv.id === $page.params.id)?.title ?? data.title; const convTreeStore = createConvTreeStore(); + const settings = useSettingsStore(); diff --git a/src/routes/conversation/[id]/+server.ts b/src/routes/conversation/[id]/+server.ts index 7ce8c1b1b40eef6f559059599c40f38031a3555c..cd94f681b1e0be15e0db5f640bd9fa75d768f1d0 100644 --- a/src/routes/conversation/[id]/+server.ts +++ b/src/routes/conversation/[id]/+server.ts @@ -8,21 +8,21 @@ import type { Message } from "$lib/types/Message"; import { error } from "@sveltejs/kit"; import { ObjectId } from "mongodb"; import { z } from "zod"; -import type { MessageUpdate } from "$lib/types/MessageUpdate"; -import { runWebSearch } from "$lib/server/websearch/runWebSearch"; -import { AbortedGenerations } from "$lib/server/abortedGenerations"; -import { summarize } from "$lib/server/summarize"; +import { + MessageUpdateStatus, + MessageUpdateType, + MessageWebSearchUpdateType, + type MessageUpdate, +} from "$lib/types/MessageUpdate"; import { uploadFile } from "$lib/server/files/uploadFile"; -import type { Assistant } from "$lib/types/Assistant"; import { convertLegacyConversation } from "$lib/utils/tree/convertLegacyConversation"; import { isMessageId } from "$lib/utils/tree/isMessageId"; import { buildSubtree } from "$lib/utils/tree/buildSubtree.js"; import { addChildren } from "$lib/utils/tree/addChildren.js"; import { addSibling } from "$lib/utils/tree/addSibling.js"; -import { preprocessMessages } from "$lib/server/endpoints/preprocessMessages.js"; import { usageLimits } from "$lib/server/usageLimits"; -import { isURLLocal } from "$lib/server/isURLLocal.js"; -import { logger } from "$lib/server/logger.js"; +import { textGeneration } from "$lib/server/textGeneration"; +import type { TextGenerationContext } from "$lib/server/textGeneration/types"; export async function POST({ request, locals, params, getClientAddress }) { const id = z.string().parse(params.id); @@ -133,6 +133,7 @@ export async function POST({ request, locals, params, getClientAddress }) { is_retry: isRetry, is_continue: isContinue, web_search: webSearch, + tools: toolsPreferences, files: inputFiles, } = z .object({ @@ -146,10 +147,12 @@ export async function POST({ request, locals, params, getClientAddress }) { is_retry: z.optional(z.boolean()), is_continue: z.optional(z.boolean()), web_search: z.optional(z.boolean()), + tools: z.record(z.boolean()).optional(), files: z.optional( z.array( z.object({ type: z.literal("base64").or(z.literal("hash")), + name: z.string(), value: z.string(), mime: z.string(), }) @@ -171,7 +174,7 @@ export async function POST({ request, locals, params, getClientAddress }) { ?.filter((file) => file.type !== "hash") .map((file) => { const blob = Buffer.from(file.value, "base64"); - return new File([blob], "file", { type: file.mime }); + return new File([blob], file.name, { type: file.mime }); }) ?? []; // check sizes @@ -284,16 +287,8 @@ export async function POST({ request, locals, params, getClientAddress }) { // update the conversation with the new messages await collections.conversations.updateOne( - { - _id: convId, - }, - { - $set: { - messages: conv.messages, - title: conv.title, - updatedAt: new Date(), - }, - } + { _id: convId }, + { $set: { messages: conv.messages, title: conv.title, updatedAt: new Date() } } ); let doneStreaming = false; @@ -302,246 +297,126 @@ export async function POST({ request, locals, params, getClientAddress }) { const stream = new ReadableStream({ async start(controller) { messageToWriteTo.updates ??= []; - function update(newUpdate: MessageUpdate) { - if (newUpdate.type !== "stream") { - messageToWriteTo?.updates?.push(newUpdate); + async function update(event: MessageUpdate) { + if (!messageToWriteTo || !conv) { + throw Error("No message or conversation to write events to"); } - if (newUpdate.type === "stream" && newUpdate.token === "") { - return; + // Add token to content or skip if empty + if (event.type === MessageUpdateType.Stream) { + if (event.token === "") return; + messageToWriteTo.content += event.token; } - controller.enqueue(JSON.stringify(newUpdate) + "\n"); - if (newUpdate.type === "finalAnswer") { - // 4096 of spaces to make sure the browser doesn't blocking buffer that holding the response - controller.enqueue(" ".repeat(4096)); + // Set the title + else if (event.type === MessageUpdateType.Title) { + conv.title = event.title; + await collections.conversations.updateOne( + { _id: convId }, + { $set: { title: conv?.title, updatedAt: new Date() } } + ); } - } - update({ type: "status", status: "started" }); - - const summarizeIfNeeded = (async () => { - if (conv.title === "New Chat" && conv.messages.length === 3) { - try { - conv.title = (await summarize(conv.messages[1].content)) ?? conv.title; - update({ type: "status", status: "title", message: conv.title }); - await collections.conversations.updateOne( - { - _id: convId, - }, - { - $set: { - title: conv?.title, - updatedAt: new Date(), - }, - } - ); - } catch (e) { - logger.error(e); - } + // Set the final text and the interrupted flag + else if (event.type === MessageUpdateType.FinalAnswer) { + messageToWriteTo.interrupted = event.interrupted; + messageToWriteTo.content = initialMessageContent + event.text; } - })(); - await collections.conversations.updateOne( - { - _id: convId, - }, - { - $set: { - title: conv.title, - updatedAt: new Date(), - }, + // Add file + else if (event.type === MessageUpdateType.File) { + messageToWriteTo.files = [ + ...(messageToWriteTo.files ?? []), + { type: "hash", name: event.name, value: event.sha, mime: event.mime }, + ]; } - ); - - // check if assistant has a rag - const assistant = await collections.assistants.findOne< - Pick - >( - { _id: conv.assistantId }, - { projection: { rag: 1, dynamicPrompt: 1, generateSettings: 1 } } - ); - const assistantHasDynamicPrompt = - env.ENABLE_ASSISTANTS_RAG === "true" && !!assistant && !!assistant?.dynamicPrompt; - - const assistantHasWebSearch = - env.ENABLE_ASSISTANTS_RAG === "true" && - !!assistant && - !!assistant.rag && - (assistant.rag.allowedLinks.length > 0 || - assistant.rag.allowedDomains.length > 0 || - assistant.rag.allowAllDomains); + // Set web search + else if ( + event.type === MessageUpdateType.WebSearch && + event.subtype === MessageWebSearchUpdateType.Finished + ) { + messageToWriteTo.webSearch = event.webSearch; + } - // perform websearch if needed - if (!isContinue && (webSearch || assistantHasWebSearch)) { - messageToWriteTo.webSearch = await runWebSearch( - conv, - messagesForPrompt, - update, - assistant?.rag - ); - } + // Append to the persistent message updates if it's not a stream update + if (event.type !== "stream") { + messageToWriteTo?.updates?.push(event); + } - let preprompt = conv.preprompt; - - if (assistantHasDynamicPrompt && preprompt) { - // process the preprompt - const urlRegex = /{{\s?url=(.*?)\s?}}/g; - let match; - while ((match = urlRegex.exec(preprompt)) !== null) { - try { - const url = new URL(match[1]); - if (await isURLLocal(url)) { - throw new Error("URL couldn't be fetched, it resolved to a local address."); - } - - const res = await fetch(url.href); - - if (!res.ok) { - throw new Error("URL couldn't be fetched, error " + res.status); - } - const text = await res.text(); - preprompt = preprompt.replaceAll(match[0], text); - } catch (e) { - preprompt = preprompt.replaceAll(match[0], (e as Error).message); - } + // Avoid remote keylogging attack executed by watching packet lengths + // by padding the text with null chars to a fixed length + // https://cdn.arstechnica.net/wp-content/uploads/2024/03/LLM-Side-Channel.pdf + if (event.type === MessageUpdateType.Stream) { + event = { ...event, token: event.token.padEnd(16, "\0") }; } - if (messagesForPrompt[0].from === "system") { - messagesForPrompt[0].content = preprompt; + // Send the update to the client + controller.enqueue(JSON.stringify(event) + "\n"); + + // Send 4096 of spaces to make sure the browser doesn't blocking buffer that holding the response + if (event.type === "finalAnswer") { + controller.enqueue(" ".repeat(4096)); } } - // inject websearch result & optionally images into the messages - const processedMessages = preprocessMessages( - messagesForPrompt, - messageToWriteTo.webSearch, - convId + await collections.conversations.updateOne( + { _id: convId }, + { $set: { title: conv.title, updatedAt: new Date() } } ); - - const previousText = messageToWriteTo.content; - - let hasError = false; - - let buffer = ""; - messageToWriteTo.updatedAt = new Date(); + let hasError = false; + const initialMessageContent = messageToWriteTo.content; try { - const endpoint = await model.getEndpoint(); - for await (const output of await endpoint({ - messages: await processedMessages, - preprompt, - continueMessage: isContinue, - generateSettings: assistant?.generateSettings, - isMultimodal: model.multimodal, - })) { - // if not generated_text is here it means the generation is not done - if (!output.generated_text) { - if (!output.token.special) { - buffer += output.token.text; - - // send the first 5 chars - // and leave the rest in the buffer - if (buffer.length >= 5) { - update({ - type: "stream", - token: buffer.slice(0, 5), - }); - buffer = buffer.slice(5); - } - - // abort check - const date = AbortedGenerations.getInstance().getList().get(convId.toString()); - if (date && date > promptedAt) { - break; - } - // no output check - if (!output) { - break; - } - - // otherwise we just concatenate tokens - messageToWriteTo.content += output.token.text; - } - } else { - messageToWriteTo.interrupted = - !output.token.special && !model.parameters.stop?.includes(output.token.text); - // add output.generated text to the last message - // strip end tokens from the output.generated_text - const text = (model.parameters.stop ?? []).reduce((acc: string, curr: string) => { - if (acc.endsWith(curr)) { - messageToWriteTo.interrupted = false; - return acc.slice(0, acc.length - curr.length); - } - return acc; - }, output.generated_text.trimEnd()); - - messageToWriteTo.content = previousText + text; - } - } + const ctx: TextGenerationContext = { + model, + endpoint: await model.getEndpoint(), + conv, + messages: messagesForPrompt, + assistant: undefined, + isContinue: isContinue ?? false, + webSearch: webSearch ?? false, + toolsPreference: toolsPreferences ?? {}, + promptedAt, + }; + // run the text generation and send updates to the client + for await (const event of textGeneration(ctx)) await update(event); } catch (e) { hasError = true; - update({ type: "status", status: "error", message: (e as Error).message }); + await update({ + type: MessageUpdateType.Status, + status: MessageUpdateStatus.Error, + message: (e as Error).message, + }); + console.error(e); } finally { // check if no output was generated - if (!hasError && messageToWriteTo.content === previousText) { - update({ - type: "status", - status: "error", + if (!hasError && messageToWriteTo.content === initialMessageContent) { + await update({ + type: MessageUpdateType.Status, + status: MessageUpdateStatus.Error, message: "No output was generated. Something went wrong.", }); } - - if (buffer) { - update({ - type: "stream", - token: buffer, - }); - } } await collections.conversations.updateOne( - { - _id: convId, - }, - { - $set: { - messages: conv.messages, - title: conv?.title, - updatedAt: new Date(), - }, - } + { _id: convId }, + { $set: { messages: conv.messages, title: conv?.title, updatedAt: new Date() } } ); // used to detect if cancel() is called bc of interrupt or just because the connection closes doneStreaming = true; - update({ - type: "finalAnswer", - text: messageToWriteTo.content, - }); - - await summarizeIfNeeded; controller.close(); - return; }, async cancel() { - if (!doneStreaming) { - await collections.conversations.updateOne( - { - _id: convId, - }, - { - $set: { - messages: conv.messages, - title: conv.title, - updatedAt: new Date(), - }, - } - ); - } + if (doneStreaming) return; + await collections.conversations.updateOne( + { _id: convId }, + { $set: { messages: conv.messages, title: conv.title, updatedAt: new Date() } } + ); }, }); diff --git a/src/routes/conversation/[id]/message/[messageId]/prompt/+server.ts b/src/routes/conversation/[id]/message/[messageId]/prompt/+server.ts index 273df6c97b881fd2abc99a03f0ff509792c37f92..84cd152884f698c7e5404db85d752dafe47cae82 100644 --- a/src/routes/conversation/[id]/message/[messageId]/prompt/+server.ts +++ b/src/routes/conversation/[id]/message/[messageId]/prompt/+server.ts @@ -44,6 +44,9 @@ export async function GET({ params, locals }) { model, }); + const userMessage = conv.messages[messageIndex]; + const assistantMessage = conv.messages[messageIndex + 1]; + return new Response( JSON.stringify( { @@ -54,6 +57,8 @@ export async function GET({ params, locals }) { ...model.parameters, return_full_text: false, }, + userMessage, + ...(assistantMessage ? { assistantMessage } : {}), }, null, 2 diff --git a/src/routes/conversation/[id]/output/[sha256]/+server.ts b/src/routes/conversation/[id]/output/[sha256]/+server.ts index 5b3ae84dcf84e4c2ebd01023a4c25e54b4917bf8..a349842632ea59b39b7396d182bf26566d8db567 100644 --- a/src/routes/conversation/[id]/output/[sha256]/+server.ts +++ b/src/routes/conversation/[id]/output/[sha256]/+server.ts @@ -29,7 +29,7 @@ export const GET: RequestHandler = async ({ locals, params }) => { throw error(404, "Conversation not found"); } } else { - // check if the user has access to the conversation + // look for the conversation in shared conversations const conv = await collections.sharedConversations.findOne({ _id: params.id, }); diff --git a/src/routes/models/+page.svelte b/src/routes/models/+page.svelte index 76fe1eea78922405c52dc54639b9ddd846b20c64..2df388829f589906df4ccef14982cd6fc837dda9 100644 --- a/src/routes/models/+page.svelte +++ b/src/routes/models/+page.svelte @@ -8,6 +8,7 @@ import { page } from "$app/stores"; import CarbonHelpFilled from "~icons/carbon/help-filled"; + import CarbonTools from "~icons/carbon/tools"; export let data: PageData; @@ -43,7 +44,7 @@ href="{base}/models/{model.id}" class="relative flex flex-col gap-2 overflow-hidden rounded-xl border bg-gray-50/50 px-6 py-5 shadow hover:bg-gray-50 hover:shadow-inner dark:border-gray-800/70 dark:bg-gray-950/20 dark:hover:bg-gray-950/40" > -
    +
    {#if model.logoUrl} {/if} + {#if model.tools} +
    + +
    + {/if} {#if index === 0}