From b3a4fa6f430b1f67cbb0498d1d0cf5a366804b0f Mon Sep 17 00:00:00 2001 From: julianam-w <77252335+julianam-w@users.noreply.github.com> Date: Sun, 29 Mar 2026 16:38:06 +1100 Subject: [PATCH 01/46] feat(askAi): no-issue: add Ask AI chatbot feature Adds a RAG-powered AI chatbot to Tamanu that allows clinical staff to ask questions about how Tamanu works. Uses Voyage AI for embeddings, a PostgreSQL RAG database for context retrieval, and Claude via BAML for answer generation. - Database migrations and models for AskAiConversation and AskAiMessage - AskAiService with hybrid vector + full-text RAG search (top 10 results) - API routes on both facility and central servers under /ask-ai/conversations - BAML schema and generated client for structured LLM responses - Floating chat panel in the web sidebar footer (toggle on/off) - Markdown rendering for AI responses via react-markdown - Sources omitted for tamanu namespace (codebase paths not useful to end users) - Prompt instructs AI to note facility vs central server availability --- package-lock.json | 2173 ++++++++++++++--- packages/central-server/app/askAi.js | 122 + packages/central-server/app/buildRoutes.js | 2 + packages/central-server/config/default.json5 | 9 +- .../1772000000000-createAskAiSchemas.ts | 21 + .../1772000000001-createAskAiTables.ts | 87 + .../database/src/models/AskAiConversation.ts | 50 + packages/database/src/models/AskAiMessage.ts | 52 + packages/database/src/models/index.ts | 2 + .../facility-server/app/routes/apiv1/askAi.js | 122 + .../facility-server/app/routes/apiv1/index.js | 2 + packages/facility-server/config/default.json5 | 7 + packages/shared/package.json | 8 +- packages/shared/src/baml_src/ask-ai.baml | 55 + .../src/baml_src/baml_client/async_client.ts | 247 ++ .../src/baml_src/baml_client/async_request.ts | 117 + .../shared/src/baml_src/baml_client/config.ts | 22 + .../src/baml_src/baml_client/globals.ts | 45 + .../shared/src/baml_src/baml_client/index.ts | 51 + .../src/baml_src/baml_client/inlinedbaml.ts | 27 + .../shared/src/baml_src/baml_client/parser.ts | 85 + .../src/baml_src/baml_client/partial_types.ts | 48 + .../src/baml_src/baml_client/sync_client.ts | 152 ++ .../src/baml_src/baml_client/sync_request.ts | 113 + .../src/baml_src/baml_client/tracing.ts | 36 + .../src/baml_src/baml_client/type_builder.ts | 129 + .../shared/src/baml_src/baml_client/types.ts | 61 + .../src/baml_src/baml_client/watchers.ts | 100 + packages/shared/src/services/AskAiService.ts | 188 ++ .../web/app/components/AskAi/AskAiPanel.jsx | 305 +++ packages/web/app/components/AskAi/index.js | 1 + .../web/app/components/Sidebar/Sidebar.jsx | 38 + packages/web/package.json | 1 + packages/web/vite.config.js | 10 +- 34 files changed, 4203 insertions(+), 285 deletions(-) create mode 100644 packages/central-server/app/askAi.js create mode 100755 packages/database/src/migrations/1772000000000-createAskAiSchemas.ts create mode 100755 packages/database/src/migrations/1772000000001-createAskAiTables.ts create mode 100755 packages/database/src/models/AskAiConversation.ts create mode 100644 packages/database/src/models/AskAiMessage.ts create mode 100644 packages/facility-server/app/routes/apiv1/askAi.js create mode 100644 packages/shared/src/baml_src/ask-ai.baml create mode 100644 packages/shared/src/baml_src/baml_client/async_client.ts create mode 100644 packages/shared/src/baml_src/baml_client/async_request.ts create mode 100644 packages/shared/src/baml_src/baml_client/config.ts create mode 100644 packages/shared/src/baml_src/baml_client/globals.ts create mode 100644 packages/shared/src/baml_src/baml_client/index.ts create mode 100644 packages/shared/src/baml_src/baml_client/inlinedbaml.ts create mode 100644 packages/shared/src/baml_src/baml_client/parser.ts create mode 100644 packages/shared/src/baml_src/baml_client/partial_types.ts create mode 100644 packages/shared/src/baml_src/baml_client/sync_client.ts create mode 100644 packages/shared/src/baml_src/baml_client/sync_request.ts create mode 100644 packages/shared/src/baml_src/baml_client/tracing.ts create mode 100644 packages/shared/src/baml_src/baml_client/type_builder.ts create mode 100644 packages/shared/src/baml_src/baml_client/types.ts create mode 100644 packages/shared/src/baml_src/baml_client/watchers.ts create mode 100644 packages/shared/src/services/AskAiService.ts create mode 100644 packages/web/app/components/AskAi/AskAiPanel.jsx create mode 100644 packages/web/app/components/AskAi/index.js diff --git a/package-lock.json b/package-lock.json index dcf2b5776ad..866ad3d1e80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2987,7 +2987,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -3012,7 +3012,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" @@ -3086,7 +3086,7 @@ "version": "7.25.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.6.tgz", "integrity": "sha512-sXaDXaJN9SNLymBdlWFA+bjzBhFD617ZaFiY13dGt7TVslVvVgA6fkZOP7Ki3IGElC45lwHdOTrCtKZGVAWeLQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.24.8" @@ -3102,7 +3102,7 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" @@ -3115,7 +3115,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -3143,7 +3143,7 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" @@ -3168,7 +3168,7 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" @@ -3217,7 +3217,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" @@ -3233,7 +3233,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" @@ -4015,7 +4015,7 @@ "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@beyondessential/eslint-config-beyondessential": { @@ -4335,6 +4335,160 @@ "integrity": "sha512-7xLM6wg76hnMdLi+mBZe1KcAwav2Q4U4XGSbS9fAHHmRvK0ntQTqm4ryYXrWjSncYIfz2u1mLMvMQQ1+EcgKLA==", "license": "MIT" }, + "node_modules/@boundaryml/baml": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@boundaryml/baml/-/baml-0.220.0.tgz", + "integrity": "sha512-37l5RltIdFRf3RT1qZiDYEcStZnrU+IzHfg0sPx4T3viWeVy+b6RbGgSw4eiAGL624jjmxl4aB90Uv0EOtr7eQ==", + "license": "MIT", + "dependencies": { + "@scarf/scarf": "^1.3.0" + }, + "bin": { + "baml": "cli.js", + "baml-cli": "cli.js" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@boundaryml/baml-darwin-arm64": "0.220.0", + "@boundaryml/baml-darwin-x64": "0.220.0", + "@boundaryml/baml-linux-arm64-gnu": "0.220.0", + "@boundaryml/baml-linux-arm64-musl": "0.220.0", + "@boundaryml/baml-linux-x64-gnu": "0.220.0", + "@boundaryml/baml-linux-x64-musl": "0.220.0", + "@boundaryml/baml-win32-arm64-msvc": "0.220.0", + "@boundaryml/baml-win32-x64-msvc": "0.220.0" + } + }, + "node_modules/@boundaryml/baml-darwin-arm64": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@boundaryml/baml-darwin-arm64/-/baml-darwin-arm64-0.220.0.tgz", + "integrity": "sha512-HhiSDwVKcqfA+RU7HeJJMdQSZsMw/sSml90ekSwu9KHvrgu8PXrtuLxlsDQbIHcwTXKHdOGUhHSnpT8wj76RzQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@boundaryml/baml-darwin-x64": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@boundaryml/baml-darwin-x64/-/baml-darwin-x64-0.220.0.tgz", + "integrity": "sha512-X4Ui/tquXZUB69iowIHconvlXgQnjX7fOJjYmUFfUGANpYU1V3YGd8tRUxtQqEj6N28c2TZKs0VR/6Er3R2GhA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@boundaryml/baml-linux-arm64-gnu": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@boundaryml/baml-linux-arm64-gnu/-/baml-linux-arm64-gnu-0.220.0.tgz", + "integrity": "sha512-aZZWj/XY0GWvVV5q8f+Kw4U7R4MGEUGTHWruevIsIawKJucvsPaz2EAoHFKEAU2F/9zD1F7+wI8bS2GP5tzd1w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@boundaryml/baml-linux-arm64-musl": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@boundaryml/baml-linux-arm64-musl/-/baml-linux-arm64-musl-0.220.0.tgz", + "integrity": "sha512-uodxLrVQgXt2IQB8xIcyFwO3Pnrc+mc065bxUqYsm9/+LNXcCzr863GjsDfU5aPrSlzpw2ei5UhfoA3oO0tZzw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@boundaryml/baml-linux-x64-gnu": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@boundaryml/baml-linux-x64-gnu/-/baml-linux-x64-gnu-0.220.0.tgz", + "integrity": "sha512-kCHHWD54TiuQi3wZG9+UCMcESquuYeQ8tAEm+94Q6daqkPPNdMal2rLw9Dk7OGG1pM8CK+bn2qtJMND8l5HDsA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@boundaryml/baml-linux-x64-musl": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@boundaryml/baml-linux-x64-musl/-/baml-linux-x64-musl-0.220.0.tgz", + "integrity": "sha512-BhBWFseDAbDhi/+acK0iaOCgNSkmvu63ZTvf2pIcfoiqxT8rPTH/mj7dFdmGMULnBKeBlS6SzCGFUzsOyqqz2w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@boundaryml/baml-win32-arm64-msvc": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@boundaryml/baml-win32-arm64-msvc/-/baml-win32-arm64-msvc-0.220.0.tgz", + "integrity": "sha512-5+WxZJu026YNfeKApk5j3sDbbQg35owmh9EOpD+KGYHXxpcDkX+hYjeL4KMHtIPlQTMbzEm7UOX60+k6n74OJg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@boundaryml/baml-win32-x64-msvc": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@boundaryml/baml-win32-x64-msvc/-/baml-win32-x64-msvc-0.220.0.tgz", + "integrity": "sha512-QnBhcN4A5cskS+2FHrAlzPZnLSatRRmC6lVp0d4U+n0kNy/9A3iKHSbF5ceunzuO4an7YnPrP3zl9nLweLoazA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@bugsnag/browser": { "version": "7.25.0", "resolved": "https://registry.npmjs.org/@bugsnag/browser/-/browser-7.25.0.tgz", @@ -5591,7 +5745,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "camelcase": "^5.3.1", @@ -5608,7 +5762,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "locate-path": "^5.0.0", @@ -5622,7 +5776,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "p-locate": "^4.1.0" @@ -5635,7 +5789,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "p-try": "^2.0.0" @@ -5651,7 +5805,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "p-limit": "^2.2.0" @@ -5664,7 +5818,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5674,7 +5828,7 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5684,7 +5838,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -5702,7 +5856,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/console": "^29.7.0", @@ -5750,7 +5904,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -5792,7 +5946,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "expect": "^29.7.0", @@ -5806,7 +5960,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "jest-get-type": "^29.6.3" @@ -5836,7 +5990,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", @@ -5852,7 +6006,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", @@ -5896,7 +6050,7 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -5908,7 +6062,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -5929,7 +6083,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.23.9", @@ -5946,7 +6100,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -5959,7 +6113,7 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "devOptional": true, + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -5984,7 +6138,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.18", @@ -5999,7 +6153,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/console": "^29.7.0", @@ -6015,7 +6169,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/test-result": "^29.7.0", @@ -6031,7 +6185,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", @@ -10597,6 +10751,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.2.0.tgz", "integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==", + "dev": true, "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", @@ -10618,6 +10773,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, "engines": { "node": ">=12" }, @@ -10967,6 +11123,13 @@ "url": "https://github.com/sponsors/sayem314" } }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@sideway/address": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", @@ -12837,7 +13000,7 @@ "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.20.7", @@ -12851,7 +13014,7 @@ "version": "7.6.8", "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.0.0" @@ -12861,7 +13024,7 @@ "version": "7.4.4", "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.1.0", @@ -12872,7 +13035,7 @@ "version": "7.20.6", "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.20.7" @@ -13072,6 +13235,15 @@ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, "node_modules/@types/express": { "version": "4.17.13", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", @@ -13102,7 +13274,7 @@ "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -13114,6 +13286,15 @@ "integrity": "sha512-qkcUlZmX6c4J8q45taBKTL3p+LbITgyx7qhlPYOdOHZB7B31K0mXbP5YA7i7SgDeEGuI9MnumiKPEMrxg8j3KQ==", "license": "MIT" }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/hoist-non-react-statics": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", @@ -13631,7 +13812,6 @@ "version": "2.0.11", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "dev": true, "license": "MIT" }, "node_modules/@types/use-sync-external-store": { @@ -14079,7 +14259,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true, "license": "ISC" }, "node_modules/@upstash/redis": { @@ -14554,7 +14733,7 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "type-fest": "^0.21.3" @@ -14570,7 +14749,7 @@ "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==", - "devOptional": true, + "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" @@ -15785,7 +15964,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/transform": "^29.7.0", @@ -15827,7 +16006,7 @@ "version": "6.1.1", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", @@ -15844,7 +16023,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.3.3", @@ -16034,7 +16213,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", @@ -16099,7 +16278,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", @@ -16112,6 +16291,16 @@ "@babel/core": "^7.0.0" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -16379,7 +16568,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "file-uri-to-path": "1.0.0" @@ -16389,7 +16578,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/bintrees": { @@ -17037,6 +17226,16 @@ "node": ">=12.19" } }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chai": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", @@ -17119,7 +17318,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -17136,6 +17335,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/character-entities-legacy": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", @@ -17295,7 +17504,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/classnames": { @@ -17479,7 +17688,7 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "iojs": ">= 1.0.0", @@ -17490,7 +17699,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/color": { @@ -17589,6 +17798,16 @@ "integrity": "sha512-+1dlx0aY5Jo1vHy/tSsIGpSkN4tS9rZSW8FIhG0JH/crs9wwweswIo/POr451r7bZww3hFbPAKnTpimzL/mm4Q==", "license": "Apache-2.0" }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/command-exists": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", @@ -18026,7 +18245,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -18522,6 +18741,29 @@ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "license": "MIT" }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/decode-named-character-reference/node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/decode-uri-component": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", @@ -18562,7 +18804,7 @@ "version": "1.7.1", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" @@ -18610,7 +18852,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=4.0.0" @@ -18844,7 +19086,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -18873,7 +19114,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -19176,6 +19417,19 @@ "typescript": "^5.4.4" } }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/dezalgo": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", @@ -19535,7 +19789,7 @@ "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -19579,6 +19833,7 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -19616,6 +19871,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -20683,10 +20939,21 @@ "node": ">=4.0" } }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, "license": "MIT" }, "node_modules/esutils": { @@ -20822,7 +21089,7 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "devOptional": true, + "dev": true, "engines": { "node": ">= 0.8.0" } @@ -20831,7 +21098,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "devOptional": true, + "dev": true, "license": "(MIT OR WTFPL)", "engines": { "node": ">=6" @@ -20841,7 +21108,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/expect-utils": "^29.7.0", @@ -21091,7 +21358,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { @@ -21914,7 +22181,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/fs-extra": { @@ -22125,7 +22392,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8.0.0" @@ -22250,7 +22517,7 @@ "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/glob-parent": { @@ -22515,6 +22782,52 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/helmet": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", @@ -22621,9 +22934,19 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "devOptional": true, + "dev": true, "license": "MIT" }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/htmlparser2": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", @@ -22866,7 +23189,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "pkg-dir": "^4.2.0", @@ -22886,7 +23209,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "locate-path": "^5.0.0", @@ -22900,7 +23223,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "p-locate": "^4.1.0" @@ -22913,7 +23236,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "p-try": "^2.0.0" @@ -22929,7 +23252,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "p-limit": "^2.2.0" @@ -22942,7 +23265,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -22952,7 +23275,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "find-up": "^4.0.0" @@ -23017,9 +23340,15 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "devOptional": true, + "dev": true, "license": "ISC" }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, "node_modules/input-otp": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz", @@ -23342,7 +23671,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -23773,7 +24102,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=8" @@ -23783,7 +24112,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.12.3", @@ -23800,7 +24129,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "istanbul-lib-coverage": "^3.0.0", @@ -23815,7 +24144,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "semver": "^7.5.3" @@ -23831,7 +24160,7 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "devOptional": true, + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -23844,7 +24173,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -23857,7 +24186,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "debug": "^4.1.1", @@ -23872,7 +24201,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -23882,7 +24211,7 @@ "version": "3.1.7", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "html-escaper": "^2.0.0", @@ -23982,7 +24311,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/core": "^29.7.0", @@ -24009,7 +24338,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "execa": "^5.0.0", @@ -24024,7 +24353,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", @@ -24048,7 +24377,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -24061,7 +24390,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -24074,7 +24403,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.0.0" @@ -24087,7 +24416,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -24097,7 +24426,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", @@ -24129,7 +24458,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -24144,7 +24473,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/core": "^29.7.0", @@ -24178,7 +24507,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", @@ -24224,7 +24553,7 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -24235,7 +24564,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -24246,7 +24575,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -24267,7 +24596,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -24280,7 +24609,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -24321,7 +24650,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "detect-newline": "^3.0.0" @@ -24334,7 +24663,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -24351,7 +24680,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -24426,7 +24755,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -24452,7 +24781,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "jest-get-type": "^29.6.3", @@ -24466,7 +24795,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -24582,7 +24911,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -24600,7 +24929,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -24610,7 +24939,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.0.0", @@ -24631,7 +24960,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "jest-regex-util": "^29.6.3", @@ -24645,7 +24974,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/console": "^29.7.0", @@ -24678,7 +25007,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", @@ -24712,7 +25041,7 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -24724,7 +25053,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -24745,7 +25074,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -24771,7 +25100,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", @@ -24803,7 +25132,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -24813,7 +25142,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.0.0", @@ -24829,7 +25158,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -24844,7 +25173,7 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "devOptional": true, + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -24874,7 +25203,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -24892,7 +25221,7 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -24905,7 +25234,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -24920,7 +25249,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/test-result": "^29.7.0", @@ -24940,7 +25269,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -26064,6 +26393,16 @@ "integrity": "sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==", "license": "MIT" }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -26374,6 +26713,557 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/mdast-util-mdx-expression/node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/mdast-util-mdx-jsx/node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/mdast-util-mdx-jsx/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-mdxjs-esm/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/mdast-util-mdxjs-esm/node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm/node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm/node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/mdast-util-mdxjs-esm/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-to-markdown/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/mdast-util-to-markdown/node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-to-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz", @@ -27515,6 +28405,413 @@ "parse-entities": "^2.0.0" } }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -27898,7 +29195,7 @@ "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/mock-property": { @@ -28247,14 +29544,14 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/natural-compare-lite": { @@ -28324,7 +29621,7 @@ "version": "3.67.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.67.0.tgz", "integrity": "sha512-bLn/fU/ALVBE9wj+p4Y21ZJWYFjUXLXPi/IewyLZkx3ApxKDNBWCKdReeKOtD8dWpOdDCeMyLh6ZewzcLsG2Nw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "semver": "^7.3.5" @@ -28337,7 +29634,7 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "devOptional": true, + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -30253,7 +31550,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "detect-libc": "^2.0.0", @@ -30280,7 +31577,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -30503,6 +31800,16 @@ "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==", "license": "MIT" }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/protobufjs": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", @@ -30597,7 +31904,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "devOptional": true, + "dev": true, "funding": [ { "type": "individual", @@ -30916,7 +32223,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "devOptional": true, + "dev": true, "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "dependencies": { "deep-extend": "^0.6.0", @@ -30932,7 +32239,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -31154,6 +32461,42 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, + "node_modules/react-markdown": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz", + "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-markdown/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/react-native": { "version": "0.71.10", "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.71.10.tgz", @@ -32186,6 +33529,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.3.1.tgz", "integrity": "sha512-KkAgygexHUkQqtvvx/otwxtuFu5cVjfzTCtjXLH9boS19/Nbtg84zS7wIQn39G8IlrhThBpQsMKkq5ZHZIYFXA==", + "dev": true, "license": "MIT", "dependencies": { "react-is": "^18.3.1", @@ -32200,6 +33544,7 @@ "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" @@ -32599,6 +33944,148 @@ "jsesc": "bin/jsesc" } }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/remark-parse/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/remark-parse/node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse/node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse/node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/remark-parse/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/remove-accents": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", @@ -32723,7 +34210,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "resolve-from": "^5.0.0" @@ -32746,7 +34233,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -32766,7 +34253,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -34015,6 +35502,16 @@ "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", @@ -34074,7 +35571,7 @@ "version": "5.1.7", "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", - "devOptional": true, + "dev": true, "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -34099,7 +35596,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/sqs-consumer": { @@ -34422,7 +35919,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "char-regex": "^1.0.2", @@ -34596,6 +36093,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities/node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/stringify-object": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", @@ -34640,7 +36161,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -34668,7 +36189,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -34712,6 +36233,24 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, "node_modules/styled-components": { "version": "5.3.11", "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.11.tgz", @@ -35193,7 +36732,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "chownr": "^1.1.1", @@ -35206,14 +36745,14 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/tar-stream": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "bl": "^4.0.3", @@ -35230,7 +36769,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "buffer": "^5.5.0", @@ -35242,7 +36781,7 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -35427,7 +36966,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2", @@ -35442,7 +36981,7 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -35454,7 +36993,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -35475,7 +37014,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -35881,6 +37420,16 @@ "tree-kill": "cli.js" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/trim-repeated": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-2.0.0.tgz", @@ -35914,6 +37463,16 @@ "node": ">= 14.0.0" } }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -36106,7 +37665,6 @@ "os": [ "aix" ], - "peer": true, "engines": { "node": ">=18" } @@ -36124,7 +37682,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -36142,7 +37699,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -36160,7 +37716,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -36178,7 +37733,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } @@ -36196,7 +37750,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } @@ -36214,7 +37767,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -36232,7 +37784,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -36250,7 +37801,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -36268,7 +37818,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -36286,7 +37835,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -36304,7 +37852,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -36322,7 +37869,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -36340,7 +37886,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -36358,7 +37903,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -36376,7 +37920,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -36394,7 +37937,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -36412,7 +37954,6 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -36430,7 +37971,6 @@ "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -36448,7 +37988,6 @@ "os": [ "sunos" ], - "peer": true, "engines": { "node": ">=18" } @@ -36466,7 +38005,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -36484,7 +38022,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -36502,7 +38039,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -36917,6 +38453,43 @@ "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", "license": "MIT" }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/unified/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unique-filename": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", @@ -36955,6 +38528,44 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/unist-util-stringify-position": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", @@ -36969,6 +38580,47 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/unist-util-visit/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -37117,7 +38769,7 @@ "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", @@ -37160,6 +38812,59 @@ "extsprintf": "^1.2.0" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/vfile-message/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/victory-vendor": { "version": "36.9.2", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", @@ -38482,7 +40187,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", @@ -38713,6 +40418,16 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "packages/api-client": { "name": "@tamanu/api-client", "version": "2.54.0", @@ -39031,36 +40746,6 @@ "node": ">= 12" } }, - "packages/central-server/node_modules/react": { - "version": "16.8.5", - "resolved": "https://registry.npmjs.org/react/-/react-16.8.5.tgz", - "integrity": "sha512-daCb9TD6FZGvJ3sg8da1tRAtIuw29PbKZW++NN4wqkbEvxL+bZpaaYb4xuftW/SpXmgacf1skXl/ddX6CdOlDw==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.13.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "packages/central-server/node_modules/react-dom": { - "version": "16.8.5", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.5.tgz", - "integrity": "sha512-VIEIvZLpFafsfu4kgmftP5L8j7P1f0YThfVTrANMhZUFMDOsA6e0kfR6wxw/8xxKs4NB59TZYbxNdPCDW34x4w==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.13.5" - }, - "peerDependencies": { - "react": "^16.0.0" - } - }, "packages/central-server/node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -39081,16 +40766,6 @@ ], "license": "MIT" }, - "packages/central-server/node_modules/scheduler": { - "version": "0.13.6", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz", - "integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, "packages/central-server/node_modules/socket.io": { "version": "4.8.1", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", @@ -39766,21 +41441,6 @@ "node": ">= 12" } }, - "packages/facility-server/node_modules/react": { - "version": "16.8.5", - "resolved": "https://registry.npmjs.org/react/-/react-16.8.5.tgz", - "integrity": "sha512-daCb9TD6FZGvJ3sg8da1tRAtIuw29PbKZW++NN4wqkbEvxL+bZpaaYb4xuftW/SpXmgacf1skXl/ddX6CdOlDw==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.13.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, "packages/facility-server/node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -39801,16 +41461,6 @@ ], "license": "MIT" }, - "packages/facility-server/node_modules/scheduler": { - "version": "0.13.6", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz", - "integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, "packages/facility-server/node_modules/socket.io": { "version": "4.8.1", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", @@ -40142,41 +41792,6 @@ "yup": "*" } }, - "packages/mobile/node_modules/babel-plugin-macros": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/runtime": "^7.12.5", - "cosmiconfig": "^7.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - } - }, - "packages/mobile/node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, "packages/mobile/node_modules/dayjs": { "version": "1.11.19", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", @@ -41706,6 +43321,7 @@ "version": "2.54.0", "license": "GPL-3.0-or-later AND BUSL-1.1", "dependencies": { + "@boundaryml/baml": "^0.220.0", "@casl/ability": "^6.8.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/semantic-conventions": "^1.28.0", @@ -42021,6 +43637,7 @@ "react-dom": "^18.2.0", "react-draggable": "^4.4.6", "react-idle-timer": "=5.4.2", + "react-markdown": "^9.0.1", "react-redux": "8.0.2", "react-refresh": "^0.14.2", "react-router": "^7.13.1", diff --git a/packages/central-server/app/askAi.js b/packages/central-server/app/askAi.js new file mode 100644 index 00000000000..95a2f2405cf --- /dev/null +++ b/packages/central-server/app/askAi.js @@ -0,0 +1,122 @@ +import express from 'express'; +import asyncHandler from 'express-async-handler'; +import config from 'config'; +import { NotFoundError } from '@tamanu/errors'; +import { chat } from '@tamanu/shared/services/AskAiService'; + +export const askAiRoutes = express.Router(); + +askAiRoutes.use( + asyncHandler(async (req, res, next) => { + if (!config.get('askAi').enabled) { + req.flagPermissionChecked(); + res.status(503).json({ error: 'Ask AI is not enabled on this server' }); + return; + } + next(); + }), +); + +// POST /ask-ai/conversations +askAiRoutes.post( + '/conversations', + asyncHandler(async (req, res) => { + req.flagPermissionChecked(); + const { models } = req; + const { title } = req.body; + + const conversation = await models.AskAiConversation.create({ + userId: req.user.id, + title: title ?? 'New conversation', + }); + + res.send(conversation); + }), +); + +// GET /ask-ai/conversations +askAiRoutes.get( + '/conversations', + asyncHandler(async (req, res) => { + req.flagPermissionChecked(); + const { models } = req; + + const conversations = await models.AskAiConversation.findAll({ + where: { userId: req.user.id }, + order: [['createdAt', 'DESC']], + }); + + res.send({ data: conversations, count: conversations.length }); + }), +); + +// GET /ask-ai/conversations/:id +askAiRoutes.get( + '/conversations/:id', + asyncHandler(async (req, res) => { + req.flagPermissionChecked(); + const { models } = req; + + const conversation = await models.AskAiConversation.findOne({ + where: { id: req.params.id, userId: req.user.id }, + }); + + if (!conversation) throw new NotFoundError(); + + const messages = await models.AskAiMessage.findAll({ + where: { conversationId: conversation.id }, + order: [['createdAt', 'ASC']], + }); + + res.send({ ...conversation.toJSON(), messages }); + }), +); + +// POST /ask-ai/conversations/:id/messages +askAiRoutes.post( + '/conversations/:id/messages', + asyncHandler(async (req, res) => { + req.flagPermissionChecked(); + const { models } = req; + const askAiConfig = config.get('askAi'); + + const conversation = await models.AskAiConversation.findOne({ + where: { id: req.params.id, userId: req.user.id }, + }); + + if (!conversation) throw new NotFoundError(); + + const { content } = req.body; + + const response = await chat({ + conversationId: conversation.id, + userMessage: content, + ragDatabaseUrl: askAiConfig.ragDatabaseUrl, + models, + voyageApiKey: askAiConfig.voyageApiKey, + anthropicApiKey: askAiConfig.anthropicApiKey, + ragNamespace: askAiConfig.ragNamespace, + }); + + res.send(response); + }), +); + +// DELETE /ask-ai/conversations/:id +askAiRoutes.delete( + '/conversations/:id', + asyncHandler(async (req, res) => { + req.flagPermissionChecked(); + const { models } = req; + + const conversation = await models.AskAiConversation.findOne({ + where: { id: req.params.id, userId: req.user.id }, + }); + + if (!conversation) throw new NotFoundError(); + + await conversation.destroy(); + + res.send({}); + }), +); diff --git a/packages/central-server/app/buildRoutes.js b/packages/central-server/app/buildRoutes.js index 733280f4a7a..d7df8097ef3 100644 --- a/packages/central-server/app/buildRoutes.js +++ b/packages/central-server/app/buildRoutes.js @@ -7,6 +7,7 @@ import { healthRoutes } from './health'; import { integrationRoutes } from './integrations'; import { adminRoutes } from './admin'; import { suggestionsRoutes } from './suggestions'; +import { askAiRoutes } from './askAi'; export const buildRoutes = ctx => { const routes = express.Router(); @@ -18,6 +19,7 @@ export const buildRoutes = ctx => { routes.use('/integration', integrationRoutes); routes.use('/admin', adminRoutes); routes.use('/suggestions', suggestionsRoutes); + routes.use('/ask-ai', askAiRoutes); return routes; }; diff --git a/packages/central-server/config/default.json5 b/packages/central-server/config/default.json5 index 8063845aafc..55c74ee5281 100644 --- a/packages/central-server/config/default.json5 +++ b/packages/central-server/config/default.json5 @@ -518,5 +518,12 @@ "tokenDuration": "24h", "loginTokenDurationMinutes": 20, "registerTokenDurationMinutes": 43800 // 1 month - } + }, + askAi: { + enabled: false, // Set to true in local.json5 once API keys are configured + anthropicApiKey: '', // Anthropic API key for LLM calls + voyageApiKey: '', // Voyage AI API key for query embedding + ragNamespace: 'tamanu', // Namespace used when indexing with github-repo-rag + ragDatabaseUrl: '', // PostgreSQL connection URL for the RAG database, e.g. postgresql://user:pass@localhost:5432/tamanu-rag + }, } diff --git a/packages/database/src/migrations/1772000000000-createAskAiSchemas.ts b/packages/database/src/migrations/1772000000000-createAskAiSchemas.ts new file mode 100755 index 00000000000..fb113d8de37 --- /dev/null +++ b/packages/database/src/migrations/1772000000000-createAskAiSchemas.ts @@ -0,0 +1,21 @@ +import { type QueryInterface } from 'sequelize'; + +// Creates the rag and ask_ai schemas and enables the pgvector extension. +// +// The rag schema is populated by the github-repo-rag Python sidecar — Tamanu +// only needs the schema to exist so that queries against it don't fail on +// startup when the feature is disabled. +// +// The ask_ai schema holds conversation history for the Ask AI chatbot. + +export async function up(query: QueryInterface): Promise { + await query.sequelize.query('CREATE EXTENSION IF NOT EXISTS vector'); + await query.createSchema('rag', {}); + await query.createSchema('ask_ai', {}); +} + +export async function down(query: QueryInterface): Promise { + await query.dropSchema('ask_ai', {}); + await query.dropSchema('rag', {}); + // Intentionally not dropping the vector extension — it may be used by other schemas. +} diff --git a/packages/database/src/migrations/1772000000001-createAskAiTables.ts b/packages/database/src/migrations/1772000000001-createAskAiTables.ts new file mode 100755 index 00000000000..6290452fa42 --- /dev/null +++ b/packages/database/src/migrations/1772000000001-createAskAiTables.ts @@ -0,0 +1,87 @@ +import { DataTypes, type QueryInterface } from 'sequelize'; + +// Creates conversation history tables for the Ask AI chatbot in the ask_ai schema. +// These tables are not synced between facility and central servers. + +const CONVERSATIONS = { tableName: 'conversations', schema: 'ask_ai' }; +const MESSAGES = { tableName: 'messages', schema: 'ask_ai' }; + +export async function up(query: QueryInterface): Promise { + await query.createTable(CONVERSATIONS, { + id: { + type: DataTypes.STRING, + primaryKey: true, + allowNull: false, + }, + user_id: { + type: DataTypes.STRING, + allowNull: false, + references: { model: { tableName: 'users', schema: 'public' }, key: 'id' }, + }, + title: { + type: DataTypes.TEXT, + allowNull: true, + }, + created_at: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + updated_at: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + deleted_at: { + type: DataTypes.DATE, + allowNull: true, + }, + }); + + await query.createTable(MESSAGES, { + id: { + type: DataTypes.STRING, + primaryKey: true, + allowNull: false, + }, + conversation_id: { + type: DataTypes.STRING, + allowNull: false, + references: { model: CONVERSATIONS, key: 'id' }, + }, + role: { + type: DataTypes.TEXT, + allowNull: false, + comment: 'user | assistant', + }, + content: { + type: DataTypes.TEXT, + allowNull: false, + }, + created_at: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + updated_at: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + }); + + await query.addIndex(CONVERSATIONS, ['user_id'], { + name: 'idx_ask_ai_conversations_user_id', + }); + + await query.addIndex(MESSAGES, ['conversation_id'], { + name: 'idx_ask_ai_messages_conversation_id', + }); +} + +export async function down(query: QueryInterface): Promise { + await query.removeIndex(MESSAGES, 'idx_ask_ai_messages_conversation_id'); + await query.removeIndex(CONVERSATIONS, 'idx_ask_ai_conversations_user_id'); + await query.dropTable(MESSAGES); + await query.dropTable(CONVERSATIONS); +} diff --git a/packages/database/src/models/AskAiConversation.ts b/packages/database/src/models/AskAiConversation.ts new file mode 100755 index 00000000000..e0b6313b277 --- /dev/null +++ b/packages/database/src/models/AskAiConversation.ts @@ -0,0 +1,50 @@ +import { DataTypes } from 'sequelize'; + +import { SYNC_DIRECTIONS } from '@tamanu/constants'; + +import { Model } from './Model'; +import type { InitOptions, Models } from '../types/model'; + +export class AskAiConversation extends Model { + declare id: string; + declare userId: string; + declare title: string | null; + + static initModel({ primaryKey, ...options }: InitOptions) { + super.init( + { + id: primaryKey, + userId: { + type: DataTypes.STRING, + allowNull: false, + references: { + model: 'users', + key: 'id', + }, + }, + title: { + type: DataTypes.TEXT, + allowNull: true, + }, + }, + { + ...options, + tableName: 'conversations', + schema: 'ask_ai', + syncDirection: SYNC_DIRECTIONS.DO_NOT_SYNC, + }, + ); + } + + static initRelations(models: Models) { + this.belongsTo(models.User, { + foreignKey: 'userId', + as: 'user', + }); + + this.hasMany(models.AskAiMessage, { + foreignKey: 'conversationId', + as: 'messages', + }); + } +} diff --git a/packages/database/src/models/AskAiMessage.ts b/packages/database/src/models/AskAiMessage.ts new file mode 100644 index 00000000000..759da998d56 --- /dev/null +++ b/packages/database/src/models/AskAiMessage.ts @@ -0,0 +1,52 @@ +import { DataTypes } from 'sequelize'; + +import { SYNC_DIRECTIONS } from '@tamanu/constants'; + +import { Model } from './Model'; +import type { InitOptions, Models } from '../types/model'; + +export class AskAiMessage extends Model { + declare id: string; + declare conversationId: string; + declare role: 'user' | 'assistant'; + declare content: string; + + static initModel({ primaryKey, ...options }: InitOptions) { + super.init( + { + id: primaryKey, + conversationId: { + type: DataTypes.STRING, + allowNull: false, + references: { + model: { tableName: 'conversations', schema: 'ask_ai' }, + key: 'id', + }, + }, + role: { + type: DataTypes.TEXT, + allowNull: false, + comment: 'user | assistant', + }, + content: { + type: DataTypes.TEXT, + allowNull: false, + }, + }, + { + ...options, + tableName: 'messages', + schema: 'ask_ai', + syncDirection: SYNC_DIRECTIONS.DO_NOT_SYNC, + paranoid: false, + }, + ); + } + + static initRelations(models: Models) { + this.belongsTo(models.AskAiConversation, { + foreignKey: 'conversationId', + as: 'conversation', + }); + } +} diff --git a/packages/database/src/models/index.ts b/packages/database/src/models/index.ts index 9ea97c9b844..df20275ff2d 100644 --- a/packages/database/src/models/index.ts +++ b/packages/database/src/models/index.ts @@ -158,3 +158,5 @@ export * from './LocationAssignmentTemplate'; export * from './LocationAssignment'; export * from './DHIS2PushLog'; export * from './MSupplyPushLog'; +export * from './AskAiConversation'; +export * from './AskAiMessage'; diff --git a/packages/facility-server/app/routes/apiv1/askAi.js b/packages/facility-server/app/routes/apiv1/askAi.js new file mode 100644 index 00000000000..ff6c4e12858 --- /dev/null +++ b/packages/facility-server/app/routes/apiv1/askAi.js @@ -0,0 +1,122 @@ +import express from 'express'; +import asyncHandler from 'express-async-handler'; +import config from 'config'; +import { NotFoundError } from '@tamanu/errors'; +import { chat } from '@tamanu/shared/services/AskAiService'; + +export const askAi = express.Router(); + +askAi.use( + asyncHandler(async (req, res, next) => { + if (!config.get('askAi').enabled) { + req.flagPermissionChecked(); + res.status(503).json({ error: 'Ask AI is not enabled on this server' }); + return; + } + next(); + }), +); + +// POST /ask-ai/conversations +askAi.post( + '/conversations', + asyncHandler(async (req, res) => { + req.flagPermissionChecked(); + const { models } = req; + const { title } = req.body; + + const conversation = await models.AskAiConversation.create({ + userId: req.user.id, + title: title ?? 'New conversation', + }); + + res.send(conversation); + }), +); + +// GET /ask-ai/conversations +askAi.get( + '/conversations', + asyncHandler(async (req, res) => { + req.flagPermissionChecked(); + const { models } = req; + + const conversations = await models.AskAiConversation.findAll({ + where: { userId: req.user.id }, + order: [['createdAt', 'DESC']], + }); + + res.send({ data: conversations, count: conversations.length }); + }), +); + +// GET /ask-ai/conversations/:id +askAi.get( + '/conversations/:id', + asyncHandler(async (req, res) => { + req.flagPermissionChecked(); + const { models } = req; + + const conversation = await models.AskAiConversation.findOne({ + where: { id: req.params.id, userId: req.user.id }, + }); + + if (!conversation) throw new NotFoundError(); + + const messages = await models.AskAiMessage.findAll({ + where: { conversationId: conversation.id }, + order: [['createdAt', 'ASC']], + }); + + res.send({ ...conversation.toJSON(), messages }); + }), +); + +// POST /ask-ai/conversations/:id/messages +askAi.post( + '/conversations/:id/messages', + asyncHandler(async (req, res) => { + req.flagPermissionChecked(); + const { models } = req; + const askAiConfig = config.get('askAi'); + + const conversation = await models.AskAiConversation.findOne({ + where: { id: req.params.id, userId: req.user.id }, + }); + + if (!conversation) throw new NotFoundError(); + + const { content } = req.body; + + const response = await chat({ + conversationId: conversation.id, + userMessage: content, + ragDatabaseUrl: askAiConfig.ragDatabaseUrl, + models, + voyageApiKey: askAiConfig.voyageApiKey, + anthropicApiKey: askAiConfig.anthropicApiKey, + ragNamespace: askAiConfig.ragNamespace, + }); + + res.send(response); + }), +); + +// DELETE /ask-ai/conversations/:id +askAi.delete( + '/conversations/:id', + asyncHandler(async (req, res) => { + req.flagPermissionChecked(); + const { models } = req; + + const conversation = await models.AskAiConversation.findOne({ + where: { id: req.params.id, userId: req.user.id }, + }); + + if (!conversation) throw new NotFoundError(); + + await conversation.destroy(); + + res.send({}); + }), +); diff --git a/packages/facility-server/app/routes/apiv1/index.js b/packages/facility-server/app/routes/apiv1/index.js index f801287e7f2..3318d2b9752 100644 --- a/packages/facility-server/app/routes/apiv1/index.js +++ b/packages/facility-server/app/routes/apiv1/index.js @@ -15,6 +15,7 @@ import asyncHandler from 'express-async-handler'; import { keyBy, mapValues } from 'lodash'; import { allergy } from './allergy'; +import { askAi } from './askAi'; import { appointments } from './appointments'; import { asset } from './asset'; import { attachment } from './attachment'; @@ -131,6 +132,7 @@ apiv1.use(patientDataRoutes); // see below for specifics apiv1.use(referenceDataRoutes); // see below for specifics apiv1.use(syncRoutes); // see below for specifics apiv1.use('/telegram', telegramRoutes); +apiv1.use('/ask-ai', askAi); // patient data endpoints patientDataRoutes.use('/allergy', allergy); diff --git a/packages/facility-server/config/default.json5 b/packages/facility-server/config/default.json5 index 04ada9e4f94..cf3f87af9a0 100644 --- a/packages/facility-server/config/default.json5 +++ b/packages/facility-server/config/default.json5 @@ -172,4 +172,11 @@ "medicationAdministrationRecord": { "upcomingRecordsShouldBeGeneratedTimeFrame": 72, // hours }, + askAi: { + enabled: false, // Set to true in local.json5 once API keys are configured + anthropicApiKey: '', // Anthropic API key for LLM calls + voyageApiKey: '', // Voyage AI API key for query embedding + ragNamespace: 'tamanu', // Namespace used when indexing with github-repo-rag + ragDatabaseUrl: '', // PostgreSQL connection URL for the RAG database, e.g. postgresql://user:pass@localhost:5432/tamanu-rag + }, } diff --git a/packages/shared/package.json b/packages/shared/package.json index 3a6e900b698..ae11e2350c9 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -38,6 +38,10 @@ "require": "./dist/cjs/schemas/facility/requests/*.js", "import": "./dist/esm/schemas/facility/requests/*.js" }, + "./services/AskAiService": { + "require": "./dist/cjs/services/AskAiService.js", + "import": "./dist/esm/services/AskAiService.js" + }, "./sync/*": { "require": "./dist/cjs/sync/*.js", "import": "./dist/esm/sync/*.js" @@ -118,7 +122,8 @@ "author": "Beyond Essential Systems Pty. Ltd.", "license": "GPL-3.0-or-later AND BUSL-1.1", "scripts": { - "build": "npm run clean && npm run build:esm && npm run build:cjs && npm run build:types", + "build": "npm run clean && npm run baml:generate && npm run build:esm && npm run build:cjs && npm run build:types", + "baml:generate": "baml-cli generate --from src/baml_src", "build:dev": "npm run build", "build:dev:watch": "npm run clean && concurrently \"npm run build:esm -- --watch\" \"npm run build:cjs -- --watch\"", "build:esm": "swc --out-dir dist/esm --copy-files --source-maps true src", @@ -136,6 +141,7 @@ "rimraf": "^6.0.1" }, "dependencies": { + "@boundaryml/baml": "^0.220.0", "@casl/ability": "^6.8.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/semantic-conventions": "^1.28.0", diff --git a/packages/shared/src/baml_src/ask-ai.baml b/packages/shared/src/baml_src/ask-ai.baml new file mode 100644 index 00000000000..998d18bf4db --- /dev/null +++ b/packages/shared/src/baml_src/ask-ai.baml @@ -0,0 +1,55 @@ +generator target { + output_type "typescript" + output_dir "." + version "0.220.0" +} + +client AnthropicClient { + provider anthropic + options { + model "claude-sonnet-4-6" + api_key env.ANTHROPIC_API_KEY + } +} + +class RagSource { + filePath string + excerpt string +} + +class AskAiResponse { + answer string + sources RagSource[] + cannotAnswer bool +} + +function AskTamanu( + userQuestion: string, + ragContext: string, + conversationHistory: string, +) -> AskAiResponse { + client AnthropicClient + prompt #" + You are a helpful assistant for Tamanu, a healthcare management system used by + clinical staff in Pacific Island nations and similar contexts. + Answer questions about how Tamanu works — features, workflows, and known errors. + Use only the provided context. Do not guess or invent features. + If the context does not contain enough information to answer, set cannotAnswer to true + and leave answer and sources empty. + Use plain language suitable for clinical staff who are not software developers. + Tamanu has two server types: the facility server (used at individual health facilities) + and the central server (used for system-wide administration and reporting). + If a feature or workflow is only available on one of these server types, say so clearly + (e.g. "This is only available on the facility server." or "This is only available on the central server."). + + Context from Tamanu documentation and codebase: + {{ ragContext }} + + Conversation so far: + {{ conversationHistory }} + + User question: {{ userQuestion }} + + {{ ctx.output_format }} + "# +} diff --git a/packages/shared/src/baml_src/baml_client/async_client.ts b/packages/shared/src/baml_src/baml_client/async_client.ts new file mode 100644 index 00000000000..a74d8ae25e1 --- /dev/null +++ b/packages/shared/src/baml_src/baml_client/async_client.ts @@ -0,0 +1,247 @@ +/************************************************************************************************* + +Welcome to Baml! To use this generated code, please run one of the following: + +$ npm install @boundaryml/baml +$ yarn add @boundaryml/baml +$ pnpm add @boundaryml/baml + +*************************************************************************************************/ + +// This file was generated by BAML: please do not edit it. Instead, edit the +// BAML files and re-generate this code using: baml-cli generate +// You can install baml-cli with: +// $ npm install @boundaryml/baml +// +/* eslint-disable */ +// tslint:disable +// @ts-nocheck +// biome-ignore format: autogenerated code + +import type { BamlRuntime, FunctionResult, BamlCtxManager, Image, Audio, Pdf, Video, FunctionLog, +HTTPRequest } from "@boundaryml/baml" +import { toBamlError, BamlStream, BamlAbortError, Collector, ClientRegistry } from "@boundaryml/baml" +import type { Checked, Check, RecursivePartialNull as MovedRecursivePartialNull } from "./types" +import type { partial_types } from "./partial_types" +import type * as types from "./types" +import type {AskAiResponse, RagSource} from "./types" +import type TypeBuilder from "./type_builder" +import { AsyncHttpRequest, AsyncHttpStreamRequest } from "./async_request" +import { LlmResponseParser, LlmStreamParser } from "./parser" +import { DO_NOT_USE_DIRECTLY_UNLESS_YOU_KNOW_WHAT_YOURE_DOING_CTX, +DO_NOT_USE_DIRECTLY_UNLESS_YOU_KNOW_WHAT_YOURE_DOING_RUNTIME } from "./globals" +import type * as events from "./events" + +/** +* @deprecated Use RecursivePartialNull from 'baml_client/types' instead. +*/ +export type RecursivePartialNull = MovedRecursivePartialNull + + type TickReason = "Unknown"; + + type BamlCallOptions = { + tb?: TypeBuilder + clientRegistry?: ClientRegistry + client?: string + collector?: Collector | Collector[] + env?: Record + tags?: Record + signal?: AbortSignal + onTick?: (reason: TickReason, log: FunctionLog | null) => void + watchers?: WatchersT + } + + export class BamlAsyncClient { + private runtime: BamlRuntime + private ctxManager: BamlCtxManager + private streamClient: BamlStreamClient + private httpRequest: AsyncHttpRequest + private httpStreamRequest: AsyncHttpStreamRequest + private llmResponseParser: LlmResponseParser + private llmStreamParser: LlmStreamParser + private bamlOptions: BamlCallOptions + + constructor(runtime: BamlRuntime, ctxManager: BamlCtxManager, bamlOptions?: BamlCallOptions) { + this.runtime = runtime + this.ctxManager = ctxManager + this.streamClient = new BamlStreamClient(runtime, ctxManager, bamlOptions) + this.httpRequest = new AsyncHttpRequest(runtime, ctxManager) + this.httpStreamRequest = new AsyncHttpStreamRequest(runtime, ctxManager) + this.llmResponseParser = new LlmResponseParser(runtime, ctxManager) + this.llmStreamParser = new LlmStreamParser(runtime, ctxManager) + this.bamlOptions = bamlOptions || {} + } + + withOptions(bamlOptions: BamlCallOptions) { + return new BamlAsyncClient(this.runtime, this.ctxManager, bamlOptions) + } + + get stream() { + return this.streamClient + } + + get request() { + return this.httpRequest + } + + get streamRequest() { + return this.httpStreamRequest + } + + get parse() { + return this.llmResponseParser + } + + get parseStream() { + return this.llmStreamParser + } + + + async AskTamanu( + userQuestion: string,ragContext: string,conversationHistory: string, + __baml_options__?: BamlCallOptions + ): Promise { + try { + const __options__ = { ...this.bamlOptions, ...(__baml_options__ || {}) } + const __signal__ = __options__.signal; + + if (__signal__?.aborted) { + throw new BamlAbortError('Operation was aborted', __signal__.reason); + } + + // Check if onTick is provided - route through streaming if so + if (__options__.onTick) { + const __stream__ = this.stream.AskTamanu( + userQuestion,ragContext,conversationHistory, + __baml_options__ + ); + + return await __stream__.getFinalResponse(); + } + + const __collector__ = __options__.collector ? (Array.isArray(__options__.collector) ? __options__.collector : + [__options__.collector]) : []; + const __rawEnv__ = __baml_options__?.env ? { ...process.env, ...__baml_options__.env } : { ...process.env }; + const __env__: Record = Object.fromEntries( + Object.entries(__rawEnv__).filter(([_, value]) => value !== undefined) as [string, string][] + ); + + // Resolve client option to clientRegistry (client takes precedence) + let __clientRegistry__ = __options__.clientRegistry; + if (__options__.client) { + __clientRegistry__ = __clientRegistry__ || new ClientRegistry(); + __clientRegistry__.setPrimary(__options__.client); + } + + const __raw__ = await this.runtime.callFunction( + "AskTamanu", + { + "userQuestion": userQuestion,"ragContext": ragContext,"conversationHistory": conversationHistory + }, + this.ctxManager.cloneContext(), + __options__.tb?.__tb(), + __clientRegistry__, + __collector__, + __options__.tags || {}, + __env__, + __signal__, + __options__.watchers, + ) + return __raw__.parsed(false) as types.AskAiResponse + } catch (error) { + throw toBamlError(error); + } + } + + } + + class BamlStreamClient { + private runtime: BamlRuntime + private ctxManager: BamlCtxManager + private bamlOptions: BamlCallOptions + + constructor(runtime: BamlRuntime, ctxManager: BamlCtxManager, bamlOptions?: BamlCallOptions) { + this.runtime = runtime + this.ctxManager = ctxManager + this.bamlOptions = bamlOptions || {} + } + + + AskTamanu( + userQuestion: string,ragContext: string,conversationHistory: string, + __baml_options__?: BamlCallOptions + ): BamlStream + { + try { + const __options__ = { ...this.bamlOptions, ...(__baml_options__ || {}) } + const __signal__ = __options__.signal; + + if (__signal__?.aborted) { + throw new BamlAbortError('Operation was aborted', __signal__.reason); + } + + let __collector__ = __options__.collector ? (Array.isArray(__options__.collector) ? __options__.collector : + [__options__.collector]) : []; + + let __onTickWrapper__: (() => void) | undefined; + + // Create collector and wrap onTick if provided + if (__options__.onTick) { + const __tickCollector__ = new Collector("on-tick-collector"); + __collector__ = [...__collector__, __tickCollector__]; + + __onTickWrapper__ = () => { + const __log__ = __tickCollector__.last; + if (__log__) { + try { + __options__.onTick!("Unknown", __log__); + } catch (error) { + console.error("Error in onTick callback for AskTamanu", error); + } + } + }; + } + + const __rawEnv__ = __baml_options__?.env ? { ...process.env, ...__baml_options__.env } : { ...process.env }; + const __env__: Record = Object.fromEntries( + Object.entries(__rawEnv__).filter(([_, value]) => value !== undefined) as [string, string][] + ); + + // Resolve client option to clientRegistry (client takes precedence) + let __clientRegistry__ = __options__.clientRegistry; + if (__options__.client) { + __clientRegistry__ = __clientRegistry__ || new ClientRegistry(); + __clientRegistry__.setPrimary(__options__.client); + } + + const __raw__ = this.runtime.streamFunction( + "AskTamanu", + { + "userQuestion": userQuestion,"ragContext": ragContext,"conversationHistory": conversationHistory + }, + undefined, + this.ctxManager.cloneContext(), + __options__.tb?.__tb(), + __clientRegistry__, + __collector__, + __options__.tags || {}, + __env__, + __signal__, + __onTickWrapper__, + ) + return new BamlStream( + __raw__, + (a): partial_types.AskAiResponse => a, + (a): types.AskAiResponse => a, + this.ctxManager.cloneContext(), + __options__.signal, + ) + } catch (error) { + throw toBamlError(error); + } + } + + } + + export const b = new BamlAsyncClient(DO_NOT_USE_DIRECTLY_UNLESS_YOU_KNOW_WHAT_YOURE_DOING_RUNTIME, + DO_NOT_USE_DIRECTLY_UNLESS_YOU_KNOW_WHAT_YOURE_DOING_CTX) \ No newline at end of file diff --git a/packages/shared/src/baml_src/baml_client/async_request.ts b/packages/shared/src/baml_src/baml_client/async_request.ts new file mode 100644 index 00000000000..efbd2ce8024 --- /dev/null +++ b/packages/shared/src/baml_src/baml_client/async_request.ts @@ -0,0 +1,117 @@ +/************************************************************************************************* + +Welcome to Baml! To use this generated code, please run one of the following: + +$ npm install @boundaryml/baml +$ yarn add @boundaryml/baml +$ pnpm add @boundaryml/baml + +*************************************************************************************************/ + +// This file was generated by BAML: please do not edit it. Instead, edit the +// BAML files and re-generate this code using: baml-cli generate +// You can install baml-cli with: +// $ npm install @boundaryml/baml +// +/* eslint-disable */ +// tslint:disable +// @ts-nocheck +// biome-ignore format: autogenerated code + +import type { BamlRuntime, BamlCtxManager, Image, Audio, Pdf, Video, FunctionLog } from +"@boundaryml/baml" +import { toBamlError, HTTPRequest, ClientRegistry } from "@boundaryml/baml" +import type { Checked, Check } from "./types" +import type * as types from "./types" +import type {AskAiResponse, RagSource} from "./types" +import type TypeBuilder from "./type_builder" +import type * as events from "./events" + +type TickReason = "Unknown"; + +type BamlCallOptions = { +tb?: TypeBuilder +clientRegistry?: ClientRegistry +client?: string +env?: Record + onTick?: (reason: TickReason, log: FunctionLog | null) => void + events?: EventsT + } + + export class AsyncHttpRequest { + constructor(private runtime: BamlRuntime, private ctxManager: BamlCtxManager) {} + + + async AskTamanu( + userQuestion: string,ragContext: string,conversationHistory: string, + __baml_options__?: BamlCallOptions + ): Promise { + try { + const __rawEnv__ = __baml_options__?.env ? { ...process.env, ...__baml_options__.env } : { ...process.env }; + const __env__: Record = Object.fromEntries( + Object.entries(__rawEnv__).filter(([_, value]) => value !== undefined) as [string, string][] + ); + + // Resolve client option to clientRegistry (client takes precedence) + let __clientRegistry__ = __baml_options__?.clientRegistry; + if (__baml_options__?.client) { + __clientRegistry__ = __clientRegistry__ || new ClientRegistry(); + __clientRegistry__.setPrimary(__baml_options__.client); + } + + return await this.runtime.buildRequest( + "AskTamanu", + { + "userQuestion": userQuestion,"ragContext": ragContext,"conversationHistory": conversationHistory + }, + this.ctxManager.cloneContext(), + __baml_options__?.tb?.__tb(), + __clientRegistry__, + false, + __env__ + ) + } catch (error) { + throw toBamlError(error); + } + } + + } + + export class AsyncHttpStreamRequest { + constructor(private runtime: BamlRuntime, private ctxManager: BamlCtxManager) {} + + + async AskTamanu( + userQuestion: string,ragContext: string,conversationHistory: string, + __baml_options__?: BamlCallOptions + ): Promise { + try { + const __rawEnv__ = __baml_options__?.env ? { ...process.env, ...__baml_options__.env } : { ...process.env }; + const __env__: Record = Object.fromEntries( + Object.entries(__rawEnv__).filter(([_, value]) => value !== undefined) as [string, string][] + ); + + // Resolve client option to clientRegistry (client takes precedence) + let __clientRegistry__ = __baml_options__?.clientRegistry; + if (__baml_options__?.client) { + __clientRegistry__ = __clientRegistry__ || new ClientRegistry(); + __clientRegistry__.setPrimary(__baml_options__.client); + } + + return await this.runtime.buildRequest( + "AskTamanu", + { + "userQuestion": userQuestion,"ragContext": ragContext,"conversationHistory": conversationHistory + }, + this.ctxManager.cloneContext(), + __baml_options__?.tb?.__tb(), + __clientRegistry__, + true, + __env__ + ) + } catch (error) { + throw toBamlError(error); + } + } + + } \ No newline at end of file diff --git a/packages/shared/src/baml_src/baml_client/config.ts b/packages/shared/src/baml_src/baml_client/config.ts new file mode 100644 index 00000000000..cbbc74d724e --- /dev/null +++ b/packages/shared/src/baml_src/baml_client/config.ts @@ -0,0 +1,22 @@ +/************************************************************************************************* + +Welcome to Baml! To use this generated code, please run one of the following: + +$ npm install @boundaryml/baml +$ yarn add @boundaryml/baml +$ pnpm add @boundaryml/baml + +*************************************************************************************************/ + +// This file was generated by BAML: please do not edit it. Instead, edit the +// BAML files and re-generate this code using: baml-cli generate +// You can install baml-cli with: +// $ npm install @boundaryml/baml +// +/* eslint-disable */ +// tslint:disable +// @ts-nocheck +// biome-ignore format: autogenerated code + +export { setLogLevel, getLogLevel, setLogJsonMode } from "@boundaryml/baml/logging"; +export { resetBamlEnvVars } from "./globals"; \ No newline at end of file diff --git a/packages/shared/src/baml_src/baml_client/globals.ts b/packages/shared/src/baml_src/baml_client/globals.ts new file mode 100644 index 00000000000..cf3d3d7e9e8 --- /dev/null +++ b/packages/shared/src/baml_src/baml_client/globals.ts @@ -0,0 +1,45 @@ +/************************************************************************************************* + +Welcome to Baml! To use this generated code, please run one of the following: + +$ npm install @boundaryml/baml +$ yarn add @boundaryml/baml +$ pnpm add @boundaryml/baml + +*************************************************************************************************/ + +// This file was generated by BAML: please do not edit it. Instead, edit the +// BAML files and re-generate this code using: baml-cli generate +// You can install baml-cli with: +// $ npm install @boundaryml/baml +// +/* eslint-disable */ +// tslint:disable +// @ts-nocheck +// biome-ignore format: autogenerated code + +import { BamlRuntime, BamlCtxManager } from '@boundaryml/baml' +import { getBamlFiles } from './inlinedbaml' + +// Create a copy of process.env to avoid mutations +const env = { ...process.env } + +export const DO_NOT_USE_DIRECTLY_UNLESS_YOU_KNOW_WHAT_YOURE_DOING_RUNTIME = BamlRuntime.fromFiles( + 'baml_src', + getBamlFiles(), + env +) +export const DO_NOT_USE_DIRECTLY_UNLESS_YOU_KNOW_WHAT_YOURE_DOING_CTX = new BamlCtxManager(DO_NOT_USE_DIRECTLY_UNLESS_YOU_KNOW_WHAT_YOURE_DOING_RUNTIME) + +/** + * @deprecated resetBamlEnvVars is deprecated and is safe to remove, since environment variables are now lazily loaded on each function call + */ +export function resetBamlEnvVars(envVars: Record) { + if (DO_NOT_USE_DIRECTLY_UNLESS_YOU_KNOW_WHAT_YOURE_DOING_CTX.allowResets()) { + const envVarsToReset = Object.fromEntries(Object.entries(envVars).filter((kv): kv is [string, string] => kv[1] !== undefined)); + DO_NOT_USE_DIRECTLY_UNLESS_YOU_KNOW_WHAT_YOURE_DOING_RUNTIME.reset('baml_src', getBamlFiles(), envVarsToReset) + DO_NOT_USE_DIRECTLY_UNLESS_YOU_KNOW_WHAT_YOURE_DOING_CTX.reset() + } else { + throw new Error('BamlError: Cannot reset BAML environment variables while there are active BAML contexts.') + } +} diff --git a/packages/shared/src/baml_src/baml_client/index.ts b/packages/shared/src/baml_src/baml_client/index.ts new file mode 100644 index 00000000000..332dd81fe85 --- /dev/null +++ b/packages/shared/src/baml_src/baml_client/index.ts @@ -0,0 +1,51 @@ +/************************************************************************************************* + +Welcome to Baml! To use this generated code, please run one of the following: + +$ npm install @boundaryml/baml +$ yarn add @boundaryml/baml +$ pnpm add @boundaryml/baml + +*************************************************************************************************/ + +// This file was generated by BAML: please do not edit it. Instead, edit the +// BAML files and re-generate this code using: baml-cli generate +// You can install baml-cli with: +// $ npm install @boundaryml/baml +// +/* eslint-disable */ +// tslint:disable +// @ts-nocheck +// biome-ignore format: autogenerated code + +/** + * If this import fails, you may need to upgrade @boundaryml/baml. + * + * Please upgrade @boundaryml/baml to 0.220.0. + * + * $ npm install @boundaryml/baml@0.220.0 + * $ yarn add @boundaryml/baml@0.220.0 + * $ pnpm add @boundaryml/baml@0.220.0 + * + * If nothing else works, please ask for help: + * + * https://github.com/boundaryml/baml/issues + * https://boundaryml.com/discord + * + **/ +import { ThrowIfVersionMismatch } from "@boundaryml/baml"; + +export const version = "0.220.0"; + +ThrowIfVersionMismatch(version); + + + +export { b } from "./async_client" + +export * from "./types" +export type { partial_types } from "./partial_types" +export * from "./tracing" +export * as watchers from "./watchers" +export { resetBamlEnvVars } from "./globals" +export { BamlClientHttpError, BamlValidationError, BamlClientFinishReasonError } from "@boundaryml/baml" \ No newline at end of file diff --git a/packages/shared/src/baml_src/baml_client/inlinedbaml.ts b/packages/shared/src/baml_src/baml_client/inlinedbaml.ts new file mode 100644 index 00000000000..b762a63fb09 --- /dev/null +++ b/packages/shared/src/baml_src/baml_client/inlinedbaml.ts @@ -0,0 +1,27 @@ +/************************************************************************************************* + +Welcome to Baml! To use this generated code, please run one of the following: + +$ npm install @boundaryml/baml +$ yarn add @boundaryml/baml +$ pnpm add @boundaryml/baml + +*************************************************************************************************/ + +// This file was generated by BAML: please do not edit it. Instead, edit the +// BAML files and re-generate this code using: baml-cli generate +// You can install baml-cli with: +// $ npm install @boundaryml/baml +// +/* eslint-disable */ +// tslint:disable +// @ts-nocheck +// biome-ignore format: autogenerated code + +const fileMap = { + + "ask-ai.baml": "generator target {\n output_type \"typescript\"\n output_dir \".\"\n version \"0.220.0\"\n}\n\nclient AnthropicClient {\n provider anthropic\n options {\n model \"claude-sonnet-4-6\"\n api_key env.ANTHROPIC_API_KEY\n }\n}\n\nclass RagSource {\n filePath string\n excerpt string\n}\n\nclass AskAiResponse {\n answer string\n sources RagSource[]\n cannotAnswer bool\n}\n\nfunction AskTamanu(\n userQuestion: string,\n ragContext: string,\n conversationHistory: string,\n) -> AskAiResponse {\n client AnthropicClient\n prompt #\"\n You are a helpful assistant for Tamanu, a healthcare management system used by\n clinical staff in Pacific Island nations and similar contexts.\n Answer questions about how Tamanu works — features, workflows, and known errors.\n Use only the provided context. Do not guess or invent features.\n If the context does not contain enough information to answer, set cannotAnswer to true\n and leave answer and sources empty.\n Use plain language suitable for clinical staff who are not software developers.\n Tamanu has two server types: the facility server (used at individual health facilities)\n and the central server (used for system-wide administration and reporting).\n If a feature or workflow is only available on one of these server types, say so clearly\n (e.g. \"This is only available on the facility server.\" or \"This is only available on the central server.\").\n\n Context from Tamanu documentation and codebase:\n {{ ragContext }}\n\n Conversation so far:\n {{ conversationHistory }}\n\n User question: {{ userQuestion }}\n\n {{ ctx.output_format }}\n \"#\n}\n", +} +export const getBamlFiles = () => { + return fileMap; +} \ No newline at end of file diff --git a/packages/shared/src/baml_src/baml_client/parser.ts b/packages/shared/src/baml_src/baml_client/parser.ts new file mode 100644 index 00000000000..daf7a156e4e --- /dev/null +++ b/packages/shared/src/baml_src/baml_client/parser.ts @@ -0,0 +1,85 @@ +/************************************************************************************************* + +Welcome to Baml! To use this generated code, please run one of the following: + +$ npm install @boundaryml/baml +$ yarn add @boundaryml/baml +$ pnpm add @boundaryml/baml + +*************************************************************************************************/ + +// This file was generated by BAML: please do not edit it. Instead, edit the +// BAML files and re-generate this code using: baml-cli generate +// You can install baml-cli with: +// $ npm install @boundaryml/baml +// +/* eslint-disable */ +// tslint:disable +// @ts-nocheck +// biome-ignore format: autogenerated code + +import type { BamlRuntime, BamlCtxManager, ClientRegistry, Image, Audio, Pdf, Video, Collector } from "@boundaryml/baml" +import { toBamlError } from "@boundaryml/baml" +import type { Checked, Check } from "./types" +import type { partial_types } from "./partial_types" +import type * as types from "./types" +import type {AskAiResponse, RagSource} from "./types" +import type TypeBuilder from "./type_builder" + +export class LlmResponseParser { + constructor(private runtime: BamlRuntime, private ctxManager: BamlCtxManager) {} + + + AskTamanu( + llmResponse: string, + __baml_options__?: { tb?: TypeBuilder, clientRegistry?: ClientRegistry, env?: Record } + ): types.AskAiResponse { + try { + const __rawEnv__ = __baml_options__?.env ? { ...process.env, ...__baml_options__.env } : { ...process.env }; + const __env__: Record = Object.fromEntries( + Object.entries(__rawEnv__).filter(([_, value]) => value !== undefined) as [string, string][] + ); + return this.runtime.parseLlmResponse( + "AskTamanu", + llmResponse, + false, + this.ctxManager.cloneContext(), + __baml_options__?.tb?.__tb(), + __baml_options__?.clientRegistry, + __env__, + ) as types.AskAiResponse + } catch (error) { + throw toBamlError(error); + } + } + +} + +export class LlmStreamParser { + constructor(private runtime: BamlRuntime, private ctxManager: BamlCtxManager) {} + + + AskTamanu( + llmResponse: string, + __baml_options__?: { tb?: TypeBuilder, clientRegistry?: ClientRegistry, env?: Record } + ): partial_types.AskAiResponse { + try { + const __rawEnv__ = __baml_options__?.env ? { ...process.env, ...__baml_options__.env } : { ...process.env }; + const __env__: Record = Object.fromEntries( + Object.entries(__rawEnv__).filter(([_, value]) => value !== undefined) as [string, string][] + ); + return this.runtime.parseLlmResponse( + "AskTamanu", + llmResponse, + true, + this.ctxManager.cloneContext(), + __baml_options__?.tb?.__tb(), + __baml_options__?.clientRegistry, + __env__, + ) as partial_types.AskAiResponse + } catch (error) { + throw toBamlError(error); + } + } + +} \ No newline at end of file diff --git a/packages/shared/src/baml_src/baml_client/partial_types.ts b/packages/shared/src/baml_src/baml_client/partial_types.ts new file mode 100644 index 00000000000..7060ed36e51 --- /dev/null +++ b/packages/shared/src/baml_src/baml_client/partial_types.ts @@ -0,0 +1,48 @@ +/************************************************************************************************* + +Welcome to Baml! To use this generated code, please run one of the following: + +$ npm install @boundaryml/baml +$ yarn add @boundaryml/baml +$ pnpm add @boundaryml/baml + +*************************************************************************************************/ + +// This file was generated by BAML: please do not edit it. Instead, edit the +// BAML files and re-generate this code using: baml-cli generate +// You can install baml-cli with: +// $ npm install @boundaryml/baml +// +/* eslint-disable */ +// tslint:disable +// @ts-nocheck +// biome-ignore format: autogenerated code + +import type { Image, Audio, Pdf, Video } from "@boundaryml/baml" +import type { Checked, Check } from "./types" +import type { AskAiResponse, RagSource } from "./types" +import type * as types from "./types" + +/****************************************************************************** +* +* These types are used for streaming, for when an instance of a type +* is still being built up and any of its fields is not yet fully available. +* +******************************************************************************/ + +export interface StreamState { + value: T + state: "Pending" | "Incomplete" | "Complete" +} + +export namespace partial_types { + export interface AskAiResponse { + answer?: string | null + sources: RagSource[] + cannotAnswer?: boolean | null + } + export interface RagSource { + filePath?: string | null + excerpt?: string | null + } +} \ No newline at end of file diff --git a/packages/shared/src/baml_src/baml_client/sync_client.ts b/packages/shared/src/baml_src/baml_client/sync_client.ts new file mode 100644 index 00000000000..63a425d0f35 --- /dev/null +++ b/packages/shared/src/baml_src/baml_client/sync_client.ts @@ -0,0 +1,152 @@ +/************************************************************************************************* + +Welcome to Baml! To use this generated code, please run one of the following: + +$ npm install @boundaryml/baml +$ yarn add @boundaryml/baml +$ pnpm add @boundaryml/baml + +*************************************************************************************************/ + +// This file was generated by BAML: please do not edit it. Instead, edit the +// BAML files and re-generate this code using: baml-cli generate +// You can install baml-cli with: +// $ npm install @boundaryml/baml +// +/* eslint-disable */ +// tslint:disable +// @ts-nocheck +// biome-ignore format: autogenerated code + +import type { BamlRuntime, FunctionResult, BamlCtxManager, Image, Audio, Pdf, Video, Collector, FunctionLog } from "@boundaryml/baml" +import { toBamlError, BamlAbortError, ClientRegistry, type HTTPRequest } from "@boundaryml/baml" +import type { Checked, Check, RecursivePartialNull as MovedRecursivePartialNull } from "./types" +import type * as types from "./types" +import type {AskAiResponse, RagSource} from "./types" +import type TypeBuilder from "./type_builder" +import { HttpRequest, HttpStreamRequest } from "./sync_request" +import { LlmResponseParser, LlmStreamParser } from "./parser" +import { DO_NOT_USE_DIRECTLY_UNLESS_YOU_KNOW_WHAT_YOURE_DOING_CTX, DO_NOT_USE_DIRECTLY_UNLESS_YOU_KNOW_WHAT_YOURE_DOING_RUNTIME } from "./globals" +import type * as events from "./events" + +/** + * @deprecated Use RecursivePartialNull from 'baml_client/types' instead. + * Example: + * ```ts + * import { RecursivePartialNull } from './baml_client/types' + * ``` + */ +export type RecursivePartialNull = MovedRecursivePartialNull; + +type TickReason = "Unknown"; + +type BamlCallOptions = { + tb?: TypeBuilder + clientRegistry?: ClientRegistry + client?: string + collector?: Collector | Collector[] + env?: Record + tags?: Record + signal?: AbortSignal + onTick?: (reason: TickReason, log: FunctionLog | null) => void + watchers?: WatchersT +} + +export class BamlSyncClient { + private httpRequest: HttpRequest + private httpStreamRequest: HttpStreamRequest + private llmResponseParser: LlmResponseParser + private llmStreamParser: LlmStreamParser + private bamlOptions: BamlCallOptions + + constructor(private runtime: BamlRuntime, private ctxManager: BamlCtxManager, bamlOptions?: BamlCallOptions) { + this.httpRequest = new HttpRequest(runtime, ctxManager) + this.httpStreamRequest = new HttpStreamRequest(runtime, ctxManager) + this.llmResponseParser = new LlmResponseParser(runtime, ctxManager) + this.llmStreamParser = new LlmStreamParser(runtime, ctxManager) + this.bamlOptions = bamlOptions || {} + } + + withOptions(bamlOptions: BamlCallOptions) { + return new BamlSyncClient(this.runtime, this.ctxManager, bamlOptions) + } + + /* + * @deprecated NOT IMPLEMENTED as streaming must by async. We + * are not providing an async version as we want to reserve the + * right to provide a sync version in the future. + */ + get stream() { + throw new Error("stream is not available in BamlSyncClient. Use `import { b } from 'baml_client/async_client") + } + + get request() { + return this.httpRequest + } + + get streamRequest() { + return this.httpStreamRequest + } + + get parse() { + return this.llmResponseParser + } + + get parseStream() { + return this.llmStreamParser + } + + + AskTamanu( + userQuestion: string,ragContext: string,conversationHistory: string, + __baml_options__?: BamlCallOptions + ): types.AskAiResponse { + try { + const __options__ = { ...this.bamlOptions, ...(__baml_options__ || {}) } + const __signal__ = __options__.signal; + + if (__signal__?.aborted) { + throw new BamlAbortError('Operation was aborted', __signal__.reason); + } + + // Check if onTick is provided and reject for sync operations + if (__options__.onTick) { + throw new Error("onTick is not supported for synchronous functions. Please use the async client instead."); + } + + const __collector__ = __options__.collector ? (Array.isArray(__options__.collector) ? __options__.collector : [__options__.collector]) : []; + const __rawEnv__ = __baml_options__?.env ? { ...process.env, ...__baml_options__.env } : { ...process.env }; + const __env__: Record = Object.fromEntries( + Object.entries(__rawEnv__).filter(([_, value]) => value !== undefined) as [string, string][] + ); + + // Resolve client option to clientRegistry (client takes precedence) + let __clientRegistry__ = __options__.clientRegistry; + if (__options__.client) { + __clientRegistry__ = __clientRegistry__ || new ClientRegistry(); + __clientRegistry__.setPrimary(__options__.client); + } + + const __raw__ = this.runtime.callFunctionSync( + "AskTamanu", + { + "userQuestion": userQuestion,"ragContext": ragContext,"conversationHistory": conversationHistory + }, + this.ctxManager.cloneContext(), + __options__.tb?.__tb(), + __clientRegistry__, + __collector__, + __options__.tags || {}, + __env__, + __signal__, + __options__.watchers, + ) + return __raw__.parsed(false) as types.AskAiResponse + } catch (error: any) { + throw toBamlError(error); + } + } + +} + +export const b = new BamlSyncClient(DO_NOT_USE_DIRECTLY_UNLESS_YOU_KNOW_WHAT_YOURE_DOING_RUNTIME, DO_NOT_USE_DIRECTLY_UNLESS_YOU_KNOW_WHAT_YOURE_DOING_CTX) \ No newline at end of file diff --git a/packages/shared/src/baml_src/baml_client/sync_request.ts b/packages/shared/src/baml_src/baml_client/sync_request.ts new file mode 100644 index 00000000000..e7a0a902072 --- /dev/null +++ b/packages/shared/src/baml_src/baml_client/sync_request.ts @@ -0,0 +1,113 @@ +/************************************************************************************************* + +Welcome to Baml! To use this generated code, please run one of the following: + +$ npm install @boundaryml/baml +$ yarn add @boundaryml/baml +$ pnpm add @boundaryml/baml + +*************************************************************************************************/ + +// This file was generated by BAML: please do not edit it. Instead, edit the +// BAML files and re-generate this code using: baml-cli generate +// You can install baml-cli with: +// $ npm install @boundaryml/baml +// +/* eslint-disable */ +// tslint:disable +// @ts-nocheck +// biome-ignore format: autogenerated code + +import type { BamlRuntime, BamlCtxManager, Image, Audio, Pdf, Video } from "@boundaryml/baml" +import { toBamlError, HTTPRequest, ClientRegistry } from "@boundaryml/baml" +import type { Checked, Check } from "./types" +import type * as types from "./types" +import type {AskAiResponse, RagSource} from "./types" +import type TypeBuilder from "./type_builder" +import type * as events from "./events" + +type BamlCallOptions = { + tb?: TypeBuilder + clientRegistry?: ClientRegistry + client?: string + env?: Record + events?: EventsT +} + +export class HttpRequest { + constructor(private runtime: BamlRuntime, private ctxManager: BamlCtxManager) {} + + + AskTamanu( + userQuestion: string,ragContext: string,conversationHistory: string, + __baml_options__?: BamlCallOptions + ): HTTPRequest { + try { + const __rawEnv__ = __baml_options__?.env ? { ...process.env, ...__baml_options__.env } : { ...process.env }; + const __env__: Record = Object.fromEntries( + Object.entries(__rawEnv__).filter(([_, value]) => value !== undefined) as [string, string][] + ); + + // Resolve client option to clientRegistry (client takes precedence) + let __clientRegistry__ = __baml_options__?.clientRegistry; + if (__baml_options__?.client) { + __clientRegistry__ = __clientRegistry__ || new ClientRegistry(); + __clientRegistry__.setPrimary(__baml_options__.client); + } + + return this.runtime.buildRequestSync( + "AskTamanu", + { + "userQuestion": userQuestion,"ragContext": ragContext,"conversationHistory": conversationHistory + }, + this.ctxManager.cloneContext(), + __baml_options__?.tb?.__tb(), + __clientRegistry__, + false, + __env__, + ) + } catch (error) { + throw toBamlError(error); + } + } + +} + +export class HttpStreamRequest { + constructor(private runtime: BamlRuntime, private ctxManager: BamlCtxManager) {} + + + AskTamanu( + userQuestion: string,ragContext: string,conversationHistory: string, + __baml_options__?: BamlCallOptions + ): HTTPRequest { + try { + const __rawEnv__ = __baml_options__?.env ? { ...process.env, ...__baml_options__.env } : { ...process.env }; + const __env__: Record = Object.fromEntries( + Object.entries(__rawEnv__).filter(([_, value]) => value !== undefined) as [string, string][] + ); + + // Resolve client option to clientRegistry (client takes precedence) + let __clientRegistry__ = __baml_options__?.clientRegistry; + if (__baml_options__?.client) { + __clientRegistry__ = __clientRegistry__ || new ClientRegistry(); + __clientRegistry__.setPrimary(__baml_options__.client); + } + + return this.runtime.buildRequestSync( + "AskTamanu", + { + "userQuestion": userQuestion,"ragContext": ragContext,"conversationHistory": conversationHistory + }, + this.ctxManager.cloneContext(), + __baml_options__?.tb?.__tb(), + __clientRegistry__, + true, + __env__, + ) + } catch (error) { + throw toBamlError(error); + } + } + +} \ No newline at end of file diff --git a/packages/shared/src/baml_src/baml_client/tracing.ts b/packages/shared/src/baml_src/baml_client/tracing.ts new file mode 100644 index 00000000000..d65313c781f --- /dev/null +++ b/packages/shared/src/baml_src/baml_client/tracing.ts @@ -0,0 +1,36 @@ +/************************************************************************************************* + +Welcome to Baml! To use this generated code, please run one of the following: + +$ npm install @boundaryml/baml +$ yarn add @boundaryml/baml +$ pnpm add @boundaryml/baml + +*************************************************************************************************/ + +// This file was generated by BAML: please do not edit it. Instead, edit the +// BAML files and re-generate this code using: baml-cli generate +// You can install baml-cli with: +// $ npm install @boundaryml/baml +// +/* eslint-disable */ +// tslint:disable +// @ts-nocheck +// biome-ignore format: autogenerated code + +import type { BamlLogEvent } from '@boundaryml/baml'; +import { DO_NOT_USE_DIRECTLY_UNLESS_YOU_KNOW_WHAT_YOURE_DOING_CTX } from './globals'; + +const traceAsync = +DO_NOT_USE_DIRECTLY_UNLESS_YOU_KNOW_WHAT_YOURE_DOING_CTX.traceFnAsync.bind(DO_NOT_USE_DIRECTLY_UNLESS_YOU_KNOW_WHAT_YOURE_DOING_CTX) +const traceSync = +DO_NOT_USE_DIRECTLY_UNLESS_YOU_KNOW_WHAT_YOURE_DOING_CTX.traceFnSync.bind(DO_NOT_USE_DIRECTLY_UNLESS_YOU_KNOW_WHAT_YOURE_DOING_CTX) +const setTags = +DO_NOT_USE_DIRECTLY_UNLESS_YOU_KNOW_WHAT_YOURE_DOING_CTX.upsertTags.bind(DO_NOT_USE_DIRECTLY_UNLESS_YOU_KNOW_WHAT_YOURE_DOING_CTX) +const flush = () => { + DO_NOT_USE_DIRECTLY_UNLESS_YOU_KNOW_WHAT_YOURE_DOING_CTX.flush.bind(DO_NOT_USE_DIRECTLY_UNLESS_YOU_KNOW_WHAT_YOURE_DOING_CTX)() +} +const onLogEvent = (callback: undefined | ((event: BamlLogEvent) => void)) => +DO_NOT_USE_DIRECTLY_UNLESS_YOU_KNOW_WHAT_YOURE_DOING_CTX.onLogEvent(callback) + +export { traceAsync, traceSync, setTags, flush, onLogEvent } \ No newline at end of file diff --git a/packages/shared/src/baml_src/baml_client/type_builder.ts b/packages/shared/src/baml_src/baml_client/type_builder.ts new file mode 100644 index 00000000000..052cc5e4122 --- /dev/null +++ b/packages/shared/src/baml_src/baml_client/type_builder.ts @@ -0,0 +1,129 @@ +/************************************************************************************************* + +Welcome to Baml! To use this generated code, please run one of the following: + +$ npm install @boundaryml/baml +$ yarn add @boundaryml/baml +$ pnpm add @boundaryml/baml + +*************************************************************************************************/ + +// This file was generated by BAML: please do not edit it. Instead, edit the +// BAML files and re-generate this code using: baml-cli generate +// You can install baml-cli with: +// $ npm install @boundaryml/baml +// +/* eslint-disable */ +// tslint:disable +// @ts-nocheck +// biome-ignore format: autogenerated code + +import { FieldType } from '@boundaryml/baml/native' +import { TypeBuilder as _TypeBuilder, EnumBuilder, EnumViewer, ClassBuilder, ClassViewer } from '@boundaryml/baml/type_builder' +import { DO_NOT_USE_DIRECTLY_UNLESS_YOU_KNOW_WHAT_YOURE_DOING_RUNTIME } from "./globals" + +export { FieldType, EnumBuilder, ClassBuilder } + +export default class TypeBuilder { + private tb: _TypeBuilder; + + AskAiResponse: ClassViewer<'AskAiResponse', "answer" | "sources" | "cannotAnswer">; + + RagSource: ClassViewer<'RagSource', "filePath" | "excerpt">; + + + + constructor() { + this.tb = new _TypeBuilder({ + classes: new Set([ + "AskAiResponse","RagSource", + ]), + enums: new Set([ + + ]), + runtime: DO_NOT_USE_DIRECTLY_UNLESS_YOU_KNOW_WHAT_YOURE_DOING_RUNTIME + }); + + this.AskAiResponse = this.tb.classViewer("AskAiResponse", [ + "answer","sources","cannotAnswer", + ]); + + this.RagSource = this.tb.classViewer("RagSource", [ + "filePath","excerpt", + ]); + + + } + + reset(): void { + this.tb.reset(); + // TODO: This should happen in Rust. Problem is, when we construct the + // typebuilder we instantiate class builders once and it seems to make + // a JS copy, bypassing the Rust side? In Python however, every time we + // access a class builder with @property, we get a new instance that + // wraps over the Rust type builder, so we only need to call tb.reset(). + // In JS it's not possible unless we refactor the way class builders are + // accessed. + + } + + __tb() { + return this.tb._tb(); + } + + string(): FieldType { + return this.tb.string() + } + + literalString(value: string): FieldType { + return this.tb.literalString(value) + } + + literalInt(value: number): FieldType { + return this.tb.literalInt(value) + } + + literalBool(value: boolean): FieldType { + return this.tb.literalBool(value) + } + + int(): FieldType { + return this.tb.int() + } + + float(): FieldType { + return this.tb.float() + } + + bool(): FieldType { + return this.tb.bool() + } + + list(type: FieldType): FieldType { + return this.tb.list(type) + } + + null(): FieldType { + return this.tb.null() + } + + map(key: FieldType, value: FieldType): FieldType { + return this.tb.map(key, value) + } + + union(types: FieldType[]): FieldType { + return this.tb.union(types) + } + + addClass(name: Name): ClassBuilder { + return this.tb.addClass(name); + } + + addEnum(name: Name): EnumBuilder { + return this.tb.addEnum(name); + } + + addBaml(baml: string): void { + this.tb.addBaml(baml); + } +} \ No newline at end of file diff --git a/packages/shared/src/baml_src/baml_client/types.ts b/packages/shared/src/baml_src/baml_client/types.ts new file mode 100644 index 00000000000..62ff70f1fce --- /dev/null +++ b/packages/shared/src/baml_src/baml_client/types.ts @@ -0,0 +1,61 @@ +/************************************************************************************************* + +Welcome to Baml! To use this generated code, please run one of the following: + +$ npm install @boundaryml/baml +$ yarn add @boundaryml/baml +$ pnpm add @boundaryml/baml + +*************************************************************************************************/ + +// This file was generated by BAML: please do not edit it. Instead, edit the +// BAML files and re-generate this code using: baml-cli generate +// You can install baml-cli with: +// $ npm install @boundaryml/baml +// +/* eslint-disable */ +// tslint:disable +// @ts-nocheck +// biome-ignore format: autogenerated code + +import type { Image, Audio, Pdf, Video } from "@boundaryml/baml" +/** + * Recursively partial type that can be null. + * + * @deprecated Use types from the `partial_types` namespace instead, which provides type-safe partial implementations + * @template T The type to make recursively partial. + */ +export type RecursivePartialNull = T extends object + ? { [P in keyof T]?: RecursivePartialNull } + : T | null; + +export interface Checked { + value: T, + checks: Record, +} + +export interface Check { + name: string, + expr: string + status: "succeeded" | "failed" +} + +export function all_succeeded(checks: Record): boolean { + return get_checks(checks).every(check => check.status === "succeeded") +} + +export function get_checks(checks: Record): Check[] { + return Object.values(checks) +} +export interface AskAiResponse { + answer: string + sources: RagSource[] + cannotAnswer: boolean + +} + +export interface RagSource { + filePath: string + excerpt: string + +} diff --git a/packages/shared/src/baml_src/baml_client/watchers.ts b/packages/shared/src/baml_src/baml_client/watchers.ts new file mode 100644 index 00000000000..4c30010ea11 --- /dev/null +++ b/packages/shared/src/baml_src/baml_client/watchers.ts @@ -0,0 +1,100 @@ +/************************************************************************************************* + +Welcome to Baml! To use this generated code, please run one of the following: + +$ npm install @boundaryml/baml +$ yarn add @boundaryml/baml +$ pnpm add @boundaryml/baml + +*************************************************************************************************/ + +// This file was generated by BAML: please do not edit it. Instead, edit the +// BAML files and re-generate this code using: baml-cli generate +// You can install baml-cli with: +// $ npm install @boundaryml/baml +// +/* eslint-disable */ +// tslint:disable +// @ts-nocheck +// biome-ignore format: autogenerated code + +import { BamlStream } from "@boundaryml/baml" + +export interface VarNotification { + variable_name: string + value: T + timestamp: string + function_name: string +} + +// Internal stream notification from Rust FFI +interface InternalStreamEvent { + streamId: string + notificationType: "start" | "update" | "end" + value?: any +} + +// Simple async iterable stream for watch notifications +class NotificationStream implements BamlStream { + private eventQueue: (PartialT | null)[] = [] + private isComplete = false + + pushValue(value: PartialT): void { + this.eventQueue.push(value) + } + + complete(): void { + this.isComplete = true + this.eventQueue.push(null) + } + + async *[Symbol.asyncIterator](): AsyncIterableIterator { + while (true) { + const event = this.eventQueue.shift() + + if (event === undefined) { + if (this.isComplete) { + break + } + // Wait a bit for more events + await new Promise(resolve => setTimeout(resolve, 10)) + continue + } + + if (event === null) { + break + } + + yield event + } + } + + async getFinalResponse(): Promise { + // For emit streams, just get the last value + let lastValue: PartialT | null = null + for await (const value of this) { + lastValue = value + } + return lastValue as unknown as FinalT + } + + toStreamable(): ReadableStream { + throw new Error("toStreamable not implemented for EmitStream") + } +} + +type VarHandler = (event: VarNotification) => void +type StreamHandler = (event: VarNotification>) => void +type InternalStreamHandler = (event: InternalStreamEvent) => void + +export interface InternalEventBindings { + functionName: string + vars: Record[]> + streams: Record + functions: Record +} + +export interface EventCollectorInternal { + __handlers(): InternalEventBindings +} + diff --git a/packages/shared/src/services/AskAiService.ts b/packages/shared/src/services/AskAiService.ts new file mode 100644 index 00000000000..9a0f13bff6b --- /dev/null +++ b/packages/shared/src/services/AskAiService.ts @@ -0,0 +1,188 @@ +import { Sequelize } from 'sequelize'; +import { b } from '../baml_src/baml_client/index'; +import type { AskAiResponse, RagSource } from '../baml_src/baml_client/types'; + +const RAG_TOP_K = 10; +const CONVERSATION_HISTORY_LIMIT = 20; + +interface SearchRagResult { + chunks: string; + sources: RagSource[]; +} + +interface ChatParams { + conversationId: string; + userMessage: string; + ragDatabaseUrl: string; + models: Record; + voyageApiKey: string; + anthropicApiKey: string; + ragNamespace: string; +} + +// Cache Sequelize connections to the RAG database by URL to avoid reconnecting on every request +const ragDbCache = new Map(); + +function getRagDb(url: string): Sequelize { + if (!ragDbCache.has(url)) { + ragDbCache.set(url, new Sequelize(url, { logging: false })); + } + return ragDbCache.get(url)!; +} + +async function embedQuery(query: string, voyageApiKey: string): Promise { + const response = await fetch('https://api.voyageai.com/v1/embeddings', { + method: 'POST', + headers: { + Authorization: `Bearer ${voyageApiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + input: [query], + model: 'voyage-code-3', + output_dimension: 1024, + }), + }); + + if (!response.ok) { + throw new Error(`Voyage AI embedding failed: ${response.status} ${response.statusText}`); + } + + const data = (await response.json()) as { data: Array<{ embedding: number[] }> }; + return data.data[0].embedding; +} + +async function searchRag( + query: string, + ragDatabaseUrl: string, + namespace: string, + voyageApiKey: string, +): Promise { + const embedding = await embedQuery(query, voyageApiKey); + const embeddingLiteral = `'[${embedding.join(',')}]'`; + const db = getRagDb(ragDatabaseUrl); + + // Hybrid search: vector (cosine) + full-text, RRF-ranked + const sql = ` + WITH vector_search AS ( + (SELECT file_path, text, 1 - (embedding <=> ${embeddingLiteral}::vector) AS score + FROM ${namespace}_code + WHERE embedding IS NOT NULL + ORDER BY embedding <=> ${embeddingLiteral}::vector + LIMIT ${RAG_TOP_K * 2}) + UNION ALL + (SELECT file_path, text, 1 - (embedding <=> ${embeddingLiteral}::vector) AS score + FROM ${namespace}_docs + WHERE embedding IS NOT NULL + ORDER BY embedding <=> ${embeddingLiteral}::vector + LIMIT ${RAG_TOP_K * 2}) + ), + fts_search AS ( + (SELECT file_path, text, ts_rank(to_tsvector('english', text), plainto_tsquery('english', :query)) AS score + FROM ${namespace}_code + WHERE to_tsvector('english', text) @@ plainto_tsquery('english', :query) + LIMIT ${RAG_TOP_K * 2}) + UNION ALL + (SELECT file_path, text, ts_rank(to_tsvector('english', text), plainto_tsquery('english', :query)) AS score + FROM ${namespace}_docs + WHERE to_tsvector('english', text) @@ plainto_tsquery('english', :query) + LIMIT ${RAG_TOP_K * 2}) + ), + rrf AS ( + SELECT + COALESCE(v.file_path, f.file_path) AS file_path, + COALESCE(v.text, f.text) AS text, + COALESCE(1.0 / (60 + ROW_NUMBER() OVER (ORDER BY v.score DESC)), 0) + + COALESCE(1.0 / (60 + ROW_NUMBER() OVER (ORDER BY f.score DESC)), 0) AS rrf_score + FROM vector_search v + FULL OUTER JOIN fts_search f ON v.file_path = f.file_path AND v.text = f.text + ) + SELECT file_path, text + FROM rrf + ORDER BY rrf_score DESC + LIMIT ${RAG_TOP_K}; + `; + + let rows: Array<{ file_path: string; text: string }>; + try { + [rows] = (await db.query(sql, { replacements: { query } })) as [ + Array<{ file_path: string; text: string }>, + unknown, + ]; + } catch (err: any) { + // RAG tables not yet indexed — return empty context so the LLM can still answer + if (err?.message?.includes('does not exist')) { + return { chunks: '', sources: [] }; + } + throw err; + } + + if (rows.length === 0) { + return { chunks: '', sources: [] }; + } + + const sources: RagSource[] = rows.map(row => ({ + filePath: row.file_path, + excerpt: row.text.slice(0, 200), + })); + + const chunks = rows.map(row => `[${row.file_path}]\n${row.text}`).join('\n\n---\n\n'); + + return { chunks, sources }; +} + +export async function chat({ + conversationId, + userMessage, + ragDatabaseUrl, + models, + voyageApiKey, + anthropicApiKey, + ragNamespace, +}: ChatParams): Promise { + // Load recent conversation history + const recentMessages = await models.AskAiMessage.findAll({ + where: { conversationId }, + order: [['createdAt', 'ASC']], + limit: CONVERSATION_HISTORY_LIMIT, + }); + + const conversationHistory = recentMessages + .map((m: any) => `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content}`) + .join('\n'); + + // Search RAG + const { chunks: ragContext, sources } = await searchRag( + userMessage, + ragDatabaseUrl, + ragNamespace, + voyageApiKey, + ); + + // BAML lazily reads env vars on each call — set the key before invoking + process.env.ANTHROPIC_API_KEY = anthropicApiKey; + const response = await b.AskTamanu(userMessage, ragContext, conversationHistory); + + // Attach RAG sources to response (BAML returns empty sources; we fill them from our search) + // Sources from the tamanu namespace are codebase file paths — not meaningful for end users + const includeSources = !response.cannotAnswer && ragNamespace !== 'tamanu'; + const result: AskAiResponse = { + answer: response.answer, + cannotAnswer: response.cannotAnswer, + sources: includeSources ? sources : [], + }; + + // Persist messages + await models.AskAiMessage.create({ + conversationId, + role: 'user', + content: userMessage, + }); + await models.AskAiMessage.create({ + conversationId, + role: 'assistant', + content: result.answer, + }); + + return result; +} diff --git a/packages/web/app/components/AskAi/AskAiPanel.jsx b/packages/web/app/components/AskAi/AskAiPanel.jsx new file mode 100644 index 00000000000..83101784164 --- /dev/null +++ b/packages/web/app/components/AskAi/AskAiPanel.jsx @@ -0,0 +1,305 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import styled from 'styled-components'; +import { CircularProgress, IconButton, Paper, TextField, Typography } from '@material-ui/core'; +import { Close, Send } from '@material-ui/icons'; +import Markdown from 'react-markdown'; +import { Colors } from '../../constants'; +import { useApi } from '../../api'; + +const PanelContainer = styled(Paper)` + position: fixed; + bottom: 0; + left: 280px; + width: 380px; + height: 500px; + display: flex; + flex-direction: column; + background: ${Colors.primaryDark}; + color: ${Colors.white}; + box-shadow: 0 -2px 16px rgba(0, 0, 0, 0.4); + border-radius: 8px 8px 0 0; + z-index: 1300; + overflow: hidden; +`; + +const Header = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border-bottom: 1px solid rgba(255, 255, 255, 0.15); + flex-shrink: 0; +`; + +const HeaderTitle = styled(Typography)` + color: ${Colors.white}; + font-weight: 600; + font-size: 15px; +`; + +const CloseButton = styled(IconButton)` + padding: 4px; + color: ${Colors.white}; + &:hover { + background-color: rgba(255, 255, 255, 0.1); + } +`; + +const MessageList = styled.div` + flex: 1; + overflow-y: auto; + padding: 16px; + display: flex; + flex-direction: column; + gap: 12px; +`; + +const MessageBubble = styled.div` + max-width: 90%; + padding: 8px 12px; + border-radius: ${props => (props.$role === 'user' ? '12px 12px 2px 12px' : '12px 12px 12px 2px')}; + background: ${props => + props.$role === 'user' ? 'rgba(255,255,255,0.15)' : 'rgba(255,255,255,0.07)'}; + align-self: ${props => (props.$role === 'user' ? 'flex-end' : 'flex-start')}; + color: ${Colors.white}; + font-size: 13px; + line-height: 1.5; + word-break: break-word; + + p { margin: 0 0 6px 0; } + p:last-child { margin-bottom: 0; } + ul, ol { margin: 4px 0; padding-left: 18px; } + li { margin: 2px 0; } + code { background: rgba(255,255,255,0.15); border-radius: 3px; padding: 1px 4px; font-size: 12px; } + pre { background: rgba(0,0,0,0.3); border-radius: 4px; padding: 8px; overflow-x: auto; } + pre code { background: none; padding: 0; } + strong { font-weight: 600; } +`; + +const CannotAnswerBubble = styled(MessageBubble)` + background: rgba(255, 200, 100, 0.15); + border: 1px solid rgba(255, 200, 100, 0.3); + color: rgba(255, 255, 255, 0.8); + font-style: italic; +`; + +const SourcesBox = styled.div` + margin-top: 6px; + font-size: 11px; + color: rgba(255, 255, 255, 0.5); + align-self: flex-start; +`; + +const SourceItem = styled.div` + font-family: monospace; + font-size: 11px; +`; + +const LoadingRow = styled.div` + display: flex; + align-items: center; + gap: 8px; + color: rgba(255, 255, 255, 0.6); + font-size: 13px; + align-self: flex-start; + padding: 4px 0; +`; + +const InputArea = styled.div` + display: flex; + align-items: flex-end; + gap: 8px; + padding: 12px 16px; + border-top: 1px solid rgba(255, 255, 255, 0.15); + flex-shrink: 0; +`; + +const StyledTextField = styled(TextField)` + flex: 1; + & .MuiInputBase-root { + color: ${Colors.white}; + background: rgba(255, 255, 255, 0.08); + border-radius: 6px; + font-size: 13px; + padding: 8px 12px; + } + & .MuiInputBase-root:hover { + background: rgba(255, 255, 255, 0.12); + } + & .MuiOutlinedInput-notchedOutline { + border-color: rgba(255, 255, 255, 0.2); + } + & .MuiOutlinedInput-root:hover .MuiOutlinedInput-notchedOutline { + border-color: rgba(255, 255, 255, 0.4); + } + & .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline { + border-color: rgba(255, 255, 255, 0.6); + } + & .MuiInputBase-input::placeholder { + color: rgba(255, 255, 255, 0.4); + opacity: 1; + } +`; + +const SendButton = styled(IconButton)` + padding: 8px; + color: ${Colors.white}; + background: rgba(255, 255, 255, 0.12); + border-radius: 6px; + &:hover { + background: rgba(255, 255, 255, 0.2); + } + &:disabled { + color: rgba(255, 255, 255, 0.3); + background: rgba(255, 255, 255, 0.05); + } +`; + +export const AskAiPanel = ({ open, onClose }) => { + const api = useApi(); + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(''); + const [conversationId, setConversationId] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const scrollRef = useRef(null); + + // Auto-scroll to bottom when messages change + useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, [messages, isLoading]); + + // Reset state when panel closes + const handleClose = useCallback(() => { + setMessages([]); + setInput(''); + setConversationId(null); + setIsLoading(false); + onClose(); + }, [onClose]); + + const handleSend = useCallback(async () => { + const trimmed = input.trim(); + if (!trimmed || isLoading) return; + + setInput(''); + setMessages(prev => [...prev, { role: 'user', content: trimmed }]); + setIsLoading(true); + + try { + let activeConversationId = conversationId; + if (!activeConversationId) { + const conv = await api.post('ask-ai/conversations', { title: trimmed.slice(0, 60) }); + activeConversationId = conv.id; + setConversationId(activeConversationId); + } + + const response = await api.post(`ask-ai/conversations/${activeConversationId}/messages`, { + content: trimmed, + }); + + setMessages(prev => [ + ...prev, + { + role: 'assistant', + content: response.cannotAnswer ? null : response.answer, + sources: response.sources ?? [], + cannotAnswer: response.cannotAnswer, + }, + ]); + } catch { + setMessages(prev => [ + ...prev, + { role: 'error', content: 'Something went wrong. Please try again.' }, + ]); + } finally { + setIsLoading(false); + } + }, [api, conversationId, input, isLoading]); + + const handleKeyDown = useCallback( + e => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }, + [handleSend], + ); + + if (!open) return null; + + return ( + +
+ Ask AI + + + +
+ + + {messages.length === 0 && ( +
+ Ask a question about how Tamanu works. +
+ )} + + {messages.map((msg, index) => { + if (msg.cannotAnswer) { + return ( + + I don't have enough information to answer that question. + + ); + } + + return ( + + + {msg.content} + + {msg.role === 'assistant' && msg.sources?.length > 0 && ( + + Sources: + {msg.sources.map((source, i) => ( + {source.filePath} + ))} + + )} + + ); + })} + + {isLoading && ( + + + Thinking… + + )} +
+ + + setInput(e.target.value)} + onKeyDown={handleKeyDown} + disabled={isLoading} + inputProps={{ 'data-testid': 'askai-input' }} + /> + + {isLoading ? ( + + ) : ( + + )} + + +
+ ); +}; diff --git a/packages/web/app/components/AskAi/index.js b/packages/web/app/components/AskAi/index.js new file mode 100644 index 00000000000..87610c9e4d5 --- /dev/null +++ b/packages/web/app/components/AskAi/index.js @@ -0,0 +1 @@ +export { AskAiPanel } from './AskAiPanel'; diff --git a/packages/web/app/components/Sidebar/Sidebar.jsx b/packages/web/app/components/Sidebar/Sidebar.jsx index 04fd83f0372..b7556c902fa 100644 --- a/packages/web/app/components/Sidebar/Sidebar.jsx +++ b/packages/web/app/components/Sidebar/Sidebar.jsx @@ -2,6 +2,7 @@ import React, { useCallback, useMemo, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import styled from 'styled-components'; import { Box, Button, Divider, IconButton, List, Typography } from '@material-ui/core'; +import ChatBubbleOutline from '@mui/icons-material/ChatBubbleOutline'; import NavigateBefore from '@mui/icons-material/NavigateBefore'; import NavigateNext from '@mui/icons-material/NavigateNext'; import { useNavigate, useLocation } from 'react-router'; @@ -18,6 +19,7 @@ import { useAuth } from '../../contexts/Auth'; import { useApi } from '../../api'; import { KebabMenu } from './KebabMenu'; import { NoteModalActionBlocker } from '../NoteModalActionBlocker'; +import { AskAiPanel } from '../AskAi'; const Container = styled.div` display: flex; @@ -138,6 +140,32 @@ const StyledMetadataBox = styled(Box)` margin-bottom: 5px; `; +const AskAiToggleButton = styled.button` + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: ${props => (props.$retracted ? '10px 0' : '10px 13px')}; + background: none; + border: none; + border-radius: 4px; + color: ${Colors.white}; + cursor: pointer; + font-size: 14px; + justify-content: ${props => (props.$retracted ? 'center' : 'flex-start')}; + transition: background 0.15s; + margin-bottom: 4px; + + &:hover { + background: rgba(255, 255, 255, 0.1); + } + + & svg { + font-size: 20px; + flex-shrink: 0; + } +`; + const getInitials = string => string .match(/\b(\w)/g) @@ -165,6 +193,7 @@ const isHighlighted = (currentPath, menuItemPath, sectionIsOpen, isRetracted) => export const Sidebar = React.memo(({ items }) => { const [selectedParentItem, setSelectedParentItem] = useState(''); const [isRetracted, setIsRetracted] = useState(false); + const [askAiOpen, setAskAiOpen] = useState(false); const api = useApi(); const { facilityId, currentUser, onLogout, currentRole } = useAuth(); const location = useLocation(); @@ -332,6 +361,14 @@ export const Sidebar = React.memo(({ items }) => { })}
+ setAskAiOpen(o => !o)} + data-testid="askai-toggle-button" + > + + {!isRetracted && 'Ask AI'} + { )}
+ setAskAiOpen(false)} /> ); }); diff --git a/packages/web/package.json b/packages/web/package.json index 88b21420458..18f51c08752 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -87,6 +87,7 @@ "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", + "react-markdown": "^9.0.1", "react-draggable": "^4.4.6", "react-idle-timer": "=5.4.2", "react-redux": "8.0.2", diff --git a/packages/web/vite.config.js b/packages/web/vite.config.js index 609d02eee2a..e3b5acca868 100644 --- a/packages/web/vite.config.js +++ b/packages/web/vite.config.js @@ -61,15 +61,15 @@ export default async () => { host: 'localhost', proxy: { '/api': { - target: process.env.TAMANU_VITE_TARGET ?? 'https://facility-1.main.cd.tamanu.app', + target: process.env.TAMANU_VITE_TARGET ?? 'http://localhost:4000', // specify other servers to use as backend by setting the environment variable, e.g. - // TAMANU_VITE_TARGET=http://localhost:3000 - // TAMANU_VITE_TARGET=http://localhost:4000 - // TAMANU_VITE_TARGET=https://central.main.cd.tamanu.app + // TAMANU_VITE_TARGET=http://localhost:4000 (facility) + // TAMANU_VITE_TARGET=http://localhost:3000 (central) + // TAMANU_VITE_TARGET=https://facility-1.main.cd.tamanu.app changeOrigin: true, }, '/api/socket.io': { - target: process.env.TAMANU_VITE_TARGET ?? 'https://facility-1.main.cd.tamanu.app', + target: process.env.TAMANU_VITE_TARGET ?? 'http://localhost:4000', ws: true, }, }, From 363418d849096fd894abccb23ae4b0e348574b59 Mon Sep 17 00:00:00 2001 From: julianam-w <77252335+julianam-w@users.noreply.github.com> Date: Sat, 4 Apr 2026 20:40:30 +1100 Subject: [PATCH 02/46] Add AI access to config and settings --- .gitignore | 1 + packages/central-server/app/askAi.js | 7 +- .../facility-server/app/routes/apiv1/askAi.js | 7 +- packages/shared/src/baml_src/ask-ai.baml | 56 ++- .../src/baml_src/baml_client/async_client.ts | 12 +- .../src/baml_src/baml_client/async_request.ts | 10 +- .../src/baml_src/baml_client/inlinedbaml.ts | 2 +- .../shared/src/baml_src/baml_client/parser.ts | 2 +- .../src/baml_src/baml_client/partial_types.ts | 8 +- .../src/baml_src/baml_client/sync_client.ts | 6 +- .../src/baml_src/baml_client/sync_request.ts | 10 +- .../src/baml_src/baml_client/type_builder.ts | 12 +- .../shared/src/baml_src/baml_client/types.ts | 8 +- packages/shared/src/services/AskAiService.ts | 72 +++- packages/web/app/App.jsx | 26 +- .../web/app/components/AskAi/AskAiPanel.jsx | 336 +++++++++++------- .../web/app/components/Sidebar/Sidebar.jsx | 37 -- 17 files changed, 375 insertions(+), 237 deletions(-) diff --git a/.gitignore b/.gitignore index 8833206f879..18a79e622b8 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ dev deploy build packages/*/dist +packages/shared/src/baml_src/baml_client *.tsbuildinfo packages/*/{src,app}/**/*.d.ts database/docs diff --git a/packages/central-server/app/askAi.js b/packages/central-server/app/askAi.js index 95a2f2405cf..1846c7f3d15 100644 --- a/packages/central-server/app/askAi.js +++ b/packages/central-server/app/askAi.js @@ -2,7 +2,7 @@ import express from 'express'; import asyncHandler from 'express-async-handler'; import config from 'config'; import { NotFoundError } from '@tamanu/errors'; -import { chat } from '@tamanu/shared/services/AskAiService'; +import { chat, sanitiseConfigForAi } from '@tamanu/shared/services/AskAiService'; export const askAiRoutes = express.Router(); @@ -88,6 +88,9 @@ askAiRoutes.post( const { content } = req.body; + const serverConfig = JSON.stringify(sanitiseConfigForAi(config.util.toObject()), null, 2); + const appSettings = JSON.stringify(await req.settings.getFrontEndSettings(), null, 2); + const response = await chat({ conversationId: conversation.id, userMessage: content, @@ -96,6 +99,8 @@ askAiRoutes.post( voyageApiKey: askAiConfig.voyageApiKey, anthropicApiKey: askAiConfig.anthropicApiKey, ragNamespace: askAiConfig.ragNamespace, + serverConfig, + appSettings, }); res.send(response); diff --git a/packages/facility-server/app/routes/apiv1/askAi.js b/packages/facility-server/app/routes/apiv1/askAi.js index ff6c4e12858..3fbf8b9b12b 100644 --- a/packages/facility-server/app/routes/apiv1/askAi.js +++ b/packages/facility-server/app/routes/apiv1/askAi.js @@ -2,7 +2,7 @@ import express from 'express'; import asyncHandler from 'express-async-handler'; import config from 'config'; import { NotFoundError } from '@tamanu/errors'; -import { chat } from '@tamanu/shared/services/AskAiService'; +import { chat, sanitiseConfigForAi } from '@tamanu/shared/services/AskAiService'; export const askAi = express.Router(); @@ -88,6 +88,9 @@ askAi.post( const { content } = req.body; + const serverConfig = JSON.stringify(sanitiseConfigForAi(config.util.toObject()), null, 2); + const appSettings = JSON.stringify(await req.settings[req.facilityId]?.getFrontEndSettings(), null, 2); + const response = await chat({ conversationId: conversation.id, userMessage: content, @@ -96,6 +99,8 @@ askAi.post( voyageApiKey: askAiConfig.voyageApiKey, anthropicApiKey: askAiConfig.anthropicApiKey, ragNamespace: askAiConfig.ragNamespace, + serverConfig, + appSettings, }); res.send(response); diff --git a/packages/shared/src/baml_src/ask-ai.baml b/packages/shared/src/baml_src/ask-ai.baml index 998d18bf4db..a302b7d4518 100644 --- a/packages/shared/src/baml_src/ask-ai.baml +++ b/packages/shared/src/baml_src/ask-ai.baml @@ -12,38 +12,56 @@ client AnthropicClient { } } -class RagSource { - filePath string - excerpt string -} - class AskAiResponse { - answer string - sources RagSource[] - cannotAnswer bool + answer string @description("The answer to the user's question. Empty when cannotAnswer is true or clarifyingQuestion is non-empty.") + cannotAnswer bool @description("True only if the provided documentation contained no useful information to answer the question.") + clarifyingQuestion string @description("A question to ask the user when their question is ambiguous or needs more context. Empty string if not needed.") } function AskTamanu( userQuestion: string, - ragContext: string, conversationHistory: string, + serverConfig: string, + appSettings: string, + ragContext: string, ) -> AskAiResponse { client AnthropicClient prompt #" - You are a helpful assistant for Tamanu, a healthcare management system used by - clinical staff in Pacific Island nations and similar contexts. - Answer questions about how Tamanu works — features, workflows, and known errors. - Use only the provided context. Do not guess or invent features. - If the context does not contain enough information to answer, set cannotAnswer to true - and leave answer and sources empty. - Use plain language suitable for clinical staff who are not software developers. + You are the Tamanu Assistant — a helpful, calm, and knowledgeable guide for clinical staff + and administrators using the Tamanu healthcare management system. + + Use plain language. Be concise. Clinical staff are busy; get to the point. + Be honest about the limits of your knowledge. + Tamanu has two server types: the facility server (used at individual health facilities) and the central server (used for system-wide administration and reporting). - If a feature or workflow is only available on one of these server types, say so clearly - (e.g. "This is only available on the facility server." or "This is only available on the central server."). + If a feature is only available on one server type, say so clearly. + + {% if serverConfig %} + This server's current configuration (credentials removed): + {{ serverConfig }} + {% endif %} + + {% if appSettings %} + This server's current application settings: + {{ appSettings }} + {% endif %} - Context from Tamanu documentation and codebase: + {% if ragContext %} + Relevant documentation found: {{ ragContext }} + {% endif %} + + How to respond: + 1. If the question is unclear or ambiguous and you cannot give a useful answer even with the + documentation above, set clarifyingQuestion to ask what you need, and leave answer empty. + 2. Answer based on the documentation provided above. + 3. If the documentation does not contain enough information to answer, set cannotAnswer to true. + Do not invent features or workflows not found in the documentation. + + If your answer may not fully resolve the issue, or the user appears to need hands-on + support with their system, end your answer with: + "If you need further support, visit https://bes-support.zendesk.com/hc/en-us/" Conversation so far: {{ conversationHistory }} diff --git a/packages/shared/src/baml_src/baml_client/async_client.ts b/packages/shared/src/baml_src/baml_client/async_client.ts index a74d8ae25e1..e358e3b0619 100644 --- a/packages/shared/src/baml_src/baml_client/async_client.ts +++ b/packages/shared/src/baml_src/baml_client/async_client.ts @@ -24,7 +24,7 @@ import { toBamlError, BamlStream, BamlAbortError, Collector, ClientRegistry } fr import type { Checked, Check, RecursivePartialNull as MovedRecursivePartialNull } from "./types" import type { partial_types } from "./partial_types" import type * as types from "./types" -import type {AskAiResponse, RagSource} from "./types" +import type {AskAiResponse} from "./types" import type TypeBuilder from "./type_builder" import { AsyncHttpRequest, AsyncHttpStreamRequest } from "./async_request" import { LlmResponseParser, LlmStreamParser } from "./parser" @@ -98,7 +98,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull async AskTamanu( - userQuestion: string,ragContext: string,conversationHistory: string, + userQuestion: string,conversationHistory: string,serverConfig: string,appSettings: string,ragContext: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -112,7 +112,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull // Check if onTick is provided - route through streaming if so if (__options__.onTick) { const __stream__ = this.stream.AskTamanu( - userQuestion,ragContext,conversationHistory, + userQuestion,conversationHistory,serverConfig,appSettings,ragContext, __baml_options__ ); @@ -136,7 +136,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull const __raw__ = await this.runtime.callFunction( "AskTamanu", { - "userQuestion": userQuestion,"ragContext": ragContext,"conversationHistory": conversationHistory + "userQuestion": userQuestion,"conversationHistory": conversationHistory,"serverConfig": serverConfig,"appSettings": appSettings,"ragContext": ragContext }, this.ctxManager.cloneContext(), __options__.tb?.__tb(), @@ -168,7 +168,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull AskTamanu( - userQuestion: string,ragContext: string,conversationHistory: string, + userQuestion: string,conversationHistory: string,serverConfig: string,appSettings: string,ragContext: string, __baml_options__?: BamlCallOptions ): BamlStream { @@ -217,7 +217,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull const __raw__ = this.runtime.streamFunction( "AskTamanu", { - "userQuestion": userQuestion,"ragContext": ragContext,"conversationHistory": conversationHistory + "userQuestion": userQuestion,"conversationHistory": conversationHistory,"serverConfig": serverConfig,"appSettings": appSettings,"ragContext": ragContext }, undefined, this.ctxManager.cloneContext(), diff --git a/packages/shared/src/baml_src/baml_client/async_request.ts b/packages/shared/src/baml_src/baml_client/async_request.ts index efbd2ce8024..3b489f5e4a7 100644 --- a/packages/shared/src/baml_src/baml_client/async_request.ts +++ b/packages/shared/src/baml_src/baml_client/async_request.ts @@ -23,7 +23,7 @@ import type { BamlRuntime, BamlCtxManager, Image, Audio, Pdf, Video, FunctionLog import { toBamlError, HTTPRequest, ClientRegistry } from "@boundaryml/baml" import type { Checked, Check } from "./types" import type * as types from "./types" -import type {AskAiResponse, RagSource} from "./types" +import type {AskAiResponse} from "./types" import type TypeBuilder from "./type_builder" import type * as events from "./events" @@ -43,7 +43,7 @@ env?: Record async AskTamanu( - userQuestion: string,ragContext: string,conversationHistory: string, + userQuestion: string,conversationHistory: string,serverConfig: string,appSettings: string,ragContext: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -62,7 +62,7 @@ env?: Record return await this.runtime.buildRequest( "AskTamanu", { - "userQuestion": userQuestion,"ragContext": ragContext,"conversationHistory": conversationHistory + "userQuestion": userQuestion,"conversationHistory": conversationHistory,"serverConfig": serverConfig,"appSettings": appSettings,"ragContext": ragContext }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -82,7 +82,7 @@ env?: Record async AskTamanu( - userQuestion: string,ragContext: string,conversationHistory: string, + userQuestion: string,conversationHistory: string,serverConfig: string,appSettings: string,ragContext: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -101,7 +101,7 @@ env?: Record return await this.runtime.buildRequest( "AskTamanu", { - "userQuestion": userQuestion,"ragContext": ragContext,"conversationHistory": conversationHistory + "userQuestion": userQuestion,"conversationHistory": conversationHistory,"serverConfig": serverConfig,"appSettings": appSettings,"ragContext": ragContext }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), diff --git a/packages/shared/src/baml_src/baml_client/inlinedbaml.ts b/packages/shared/src/baml_src/baml_client/inlinedbaml.ts index b762a63fb09..66b0702e480 100644 --- a/packages/shared/src/baml_src/baml_client/inlinedbaml.ts +++ b/packages/shared/src/baml_src/baml_client/inlinedbaml.ts @@ -20,7 +20,7 @@ $ pnpm add @boundaryml/baml const fileMap = { - "ask-ai.baml": "generator target {\n output_type \"typescript\"\n output_dir \".\"\n version \"0.220.0\"\n}\n\nclient AnthropicClient {\n provider anthropic\n options {\n model \"claude-sonnet-4-6\"\n api_key env.ANTHROPIC_API_KEY\n }\n}\n\nclass RagSource {\n filePath string\n excerpt string\n}\n\nclass AskAiResponse {\n answer string\n sources RagSource[]\n cannotAnswer bool\n}\n\nfunction AskTamanu(\n userQuestion: string,\n ragContext: string,\n conversationHistory: string,\n) -> AskAiResponse {\n client AnthropicClient\n prompt #\"\n You are a helpful assistant for Tamanu, a healthcare management system used by\n clinical staff in Pacific Island nations and similar contexts.\n Answer questions about how Tamanu works — features, workflows, and known errors.\n Use only the provided context. Do not guess or invent features.\n If the context does not contain enough information to answer, set cannotAnswer to true\n and leave answer and sources empty.\n Use plain language suitable for clinical staff who are not software developers.\n Tamanu has two server types: the facility server (used at individual health facilities)\n and the central server (used for system-wide administration and reporting).\n If a feature or workflow is only available on one of these server types, say so clearly\n (e.g. \"This is only available on the facility server.\" or \"This is only available on the central server.\").\n\n Context from Tamanu documentation and codebase:\n {{ ragContext }}\n\n Conversation so far:\n {{ conversationHistory }}\n\n User question: {{ userQuestion }}\n\n {{ ctx.output_format }}\n \"#\n}\n", + "ask-ai.baml": "generator target {\n output_type \"typescript\"\n output_dir \".\"\n version \"0.220.0\"\n}\n\nclient AnthropicClient {\n provider anthropic\n options {\n model \"claude-sonnet-4-6\"\n api_key env.ANTHROPIC_API_KEY\n }\n}\n\nclass AskAiResponse {\n answer string @description(\"The answer to the user's question. Empty when cannotAnswer is true or clarifyingQuestion is non-empty.\")\n cannotAnswer bool @description(\"True only if the provided documentation contained no useful information to answer the question.\")\n clarifyingQuestion string @description(\"A question to ask the user when their question is ambiguous or needs more context. Empty string if not needed.\")\n}\n\nfunction AskTamanu(\n userQuestion: string,\n conversationHistory: string,\n serverConfig: string,\n appSettings: string,\n ragContext: string,\n) -> AskAiResponse {\n client AnthropicClient\n prompt #\"\n You are the Tamanu Assistant — a helpful, calm, and knowledgeable guide for clinical staff\n and administrators using the Tamanu healthcare management system.\n\n Use plain language. Be concise. Clinical staff are busy; get to the point.\n Be honest about the limits of your knowledge.\n\n Tamanu has two server types: the facility server (used at individual health facilities)\n and the central server (used for system-wide administration and reporting).\n If a feature is only available on one server type, say so clearly.\n\n {% if serverConfig %}\n This server's current configuration (credentials removed):\n {{ serverConfig }}\n {% endif %}\n\n {% if appSettings %}\n This server's current application settings:\n {{ appSettings }}\n {% endif %}\n\n {% if ragContext %}\n Relevant documentation found:\n {{ ragContext }}\n {% endif %}\n\n How to respond:\n 1. If the question is unclear or ambiguous and you cannot give a useful answer even with the\n documentation above, set clarifyingQuestion to ask what you need, and leave answer empty.\n 2. Answer based on the documentation provided above.\n 3. If the documentation does not contain enough information to answer, set cannotAnswer to true.\n Do not invent features or workflows not found in the documentation.\n\n If your answer may not fully resolve the issue, or the user appears to need hands-on\n support with their system, end your answer with:\n \"If you need further support, visit https://bes-support.zendesk.com/hc/en-us/\"\n\n Conversation so far:\n {{ conversationHistory }}\n\n User question: {{ userQuestion }}\n\n {{ ctx.output_format }}\n \"#\n}\n", } export const getBamlFiles = () => { return fileMap; diff --git a/packages/shared/src/baml_src/baml_client/parser.ts b/packages/shared/src/baml_src/baml_client/parser.ts index daf7a156e4e..6320271f225 100644 --- a/packages/shared/src/baml_src/baml_client/parser.ts +++ b/packages/shared/src/baml_src/baml_client/parser.ts @@ -23,7 +23,7 @@ import { toBamlError } from "@boundaryml/baml" import type { Checked, Check } from "./types" import type { partial_types } from "./partial_types" import type * as types from "./types" -import type {AskAiResponse, RagSource} from "./types" +import type {AskAiResponse} from "./types" import type TypeBuilder from "./type_builder" export class LlmResponseParser { diff --git a/packages/shared/src/baml_src/baml_client/partial_types.ts b/packages/shared/src/baml_src/baml_client/partial_types.ts index 7060ed36e51..f3fa96173ee 100644 --- a/packages/shared/src/baml_src/baml_client/partial_types.ts +++ b/packages/shared/src/baml_src/baml_client/partial_types.ts @@ -20,7 +20,7 @@ $ pnpm add @boundaryml/baml import type { Image, Audio, Pdf, Video } from "@boundaryml/baml" import type { Checked, Check } from "./types" -import type { AskAiResponse, RagSource } from "./types" +import type { AskAiResponse } from "./types" import type * as types from "./types" /****************************************************************************** @@ -38,11 +38,7 @@ export interface StreamState { export namespace partial_types { export interface AskAiResponse { answer?: string | null - sources: RagSource[] cannotAnswer?: boolean | null - } - export interface RagSource { - filePath?: string | null - excerpt?: string | null + clarifyingQuestion?: string | null } } \ No newline at end of file diff --git a/packages/shared/src/baml_src/baml_client/sync_client.ts b/packages/shared/src/baml_src/baml_client/sync_client.ts index 63a425d0f35..33a47c409a4 100644 --- a/packages/shared/src/baml_src/baml_client/sync_client.ts +++ b/packages/shared/src/baml_src/baml_client/sync_client.ts @@ -22,7 +22,7 @@ import type { BamlRuntime, FunctionResult, BamlCtxManager, Image, Audio, Pdf, Vi import { toBamlError, BamlAbortError, ClientRegistry, type HTTPRequest } from "@boundaryml/baml" import type { Checked, Check, RecursivePartialNull as MovedRecursivePartialNull } from "./types" import type * as types from "./types" -import type {AskAiResponse, RagSource} from "./types" +import type {AskAiResponse} from "./types" import type TypeBuilder from "./type_builder" import { HttpRequest, HttpStreamRequest } from "./sync_request" import { LlmResponseParser, LlmStreamParser } from "./parser" @@ -98,7 +98,7 @@ export class BamlSyncClient { AskTamanu( - userQuestion: string,ragContext: string,conversationHistory: string, + userQuestion: string,conversationHistory: string,serverConfig: string,appSettings: string,ragContext: string, __baml_options__?: BamlCallOptions ): types.AskAiResponse { try { @@ -130,7 +130,7 @@ export class BamlSyncClient { const __raw__ = this.runtime.callFunctionSync( "AskTamanu", { - "userQuestion": userQuestion,"ragContext": ragContext,"conversationHistory": conversationHistory + "userQuestion": userQuestion,"conversationHistory": conversationHistory,"serverConfig": serverConfig,"appSettings": appSettings,"ragContext": ragContext }, this.ctxManager.cloneContext(), __options__.tb?.__tb(), diff --git a/packages/shared/src/baml_src/baml_client/sync_request.ts b/packages/shared/src/baml_src/baml_client/sync_request.ts index e7a0a902072..e11474897d2 100644 --- a/packages/shared/src/baml_src/baml_client/sync_request.ts +++ b/packages/shared/src/baml_src/baml_client/sync_request.ts @@ -22,7 +22,7 @@ import type { BamlRuntime, BamlCtxManager, Image, Audio, Pdf, Video } from "@bou import { toBamlError, HTTPRequest, ClientRegistry } from "@boundaryml/baml" import type { Checked, Check } from "./types" import type * as types from "./types" -import type {AskAiResponse, RagSource} from "./types" +import type {AskAiResponse} from "./types" import type TypeBuilder from "./type_builder" import type * as events from "./events" @@ -39,7 +39,7 @@ export class HttpRequest { AskTamanu( - userQuestion: string,ragContext: string,conversationHistory: string, + userQuestion: string,conversationHistory: string,serverConfig: string,appSettings: string,ragContext: string, __baml_options__?: BamlCallOptions ): HTTPRequest { try { @@ -58,7 +58,7 @@ export class HttpRequest { return this.runtime.buildRequestSync( "AskTamanu", { - "userQuestion": userQuestion,"ragContext": ragContext,"conversationHistory": conversationHistory + "userQuestion": userQuestion,"conversationHistory": conversationHistory,"serverConfig": serverConfig,"appSettings": appSettings,"ragContext": ragContext }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -78,7 +78,7 @@ export class HttpStreamRequest { AskTamanu( - userQuestion: string,ragContext: string,conversationHistory: string, + userQuestion: string,conversationHistory: string,serverConfig: string,appSettings: string,ragContext: string, __baml_options__?: BamlCallOptions ): HTTPRequest { try { @@ -97,7 +97,7 @@ export class HttpStreamRequest { return this.runtime.buildRequestSync( "AskTamanu", { - "userQuestion": userQuestion,"ragContext": ragContext,"conversationHistory": conversationHistory + "userQuestion": userQuestion,"conversationHistory": conversationHistory,"serverConfig": serverConfig,"appSettings": appSettings,"ragContext": ragContext }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), diff --git a/packages/shared/src/baml_src/baml_client/type_builder.ts b/packages/shared/src/baml_src/baml_client/type_builder.ts index 052cc5e4122..f88461e8d31 100644 --- a/packages/shared/src/baml_src/baml_client/type_builder.ts +++ b/packages/shared/src/baml_src/baml_client/type_builder.ts @@ -27,16 +27,14 @@ export { FieldType, EnumBuilder, ClassBuilder } export default class TypeBuilder { private tb: _TypeBuilder; - AskAiResponse: ClassViewer<'AskAiResponse', "answer" | "sources" | "cannotAnswer">; - - RagSource: ClassViewer<'RagSource', "filePath" | "excerpt">; + AskAiResponse: ClassViewer<'AskAiResponse', "answer" | "cannotAnswer" | "clarifyingQuestion">; constructor() { this.tb = new _TypeBuilder({ classes: new Set([ - "AskAiResponse","RagSource", + "AskAiResponse", ]), enums: new Set([ @@ -45,11 +43,7 @@ export default class TypeBuilder { }); this.AskAiResponse = this.tb.classViewer("AskAiResponse", [ - "answer","sources","cannotAnswer", - ]); - - this.RagSource = this.tb.classViewer("RagSource", [ - "filePath","excerpt", + "answer","cannotAnswer","clarifyingQuestion", ]); diff --git a/packages/shared/src/baml_src/baml_client/types.ts b/packages/shared/src/baml_src/baml_client/types.ts index 62ff70f1fce..e04d8bc1e8c 100644 --- a/packages/shared/src/baml_src/baml_client/types.ts +++ b/packages/shared/src/baml_src/baml_client/types.ts @@ -49,13 +49,7 @@ export function get_checks(checks: Record 10 || value === null || value === undefined) return value; + if (typeof value !== 'object') return value; + if (Array.isArray(value)) return value.map(item => sanitiseConfigForAi(item, depth + 1)); + const result: Record = {}; + for (const [k, v] of Object.entries(value as Record)) { + result[k] = SENSITIVE_KEY_PATTERN.test(k) ? '[REDACTED]' : sanitiseConfigForAi(v, depth + 1); + } + return result; +} const RAG_TOP_K = 10; const CONVERSATION_HISTORY_LIMIT = 20; +interface RagSource { + filePath: string; + excerpt: string; +} + interface SearchRagResult { chunks: string; sources: RagSource[]; @@ -18,6 +35,15 @@ interface ChatParams { voyageApiKey: string; anthropicApiKey: string; ragNamespace: string; + serverConfig?: string; + appSettings?: string; +} + +interface ChatResponse { + answer: string; + cannotAnswer: boolean; + clarifyingQuestion: string; + sources: RagSource[]; } // Cache Sequelize connections to the RAG database by URL to avoid reconnecting on every request @@ -139,7 +165,9 @@ export async function chat({ voyageApiKey, anthropicApiKey, ragNamespace, -}: ChatParams): Promise { + serverConfig, + appSettings, +}: ChatParams): Promise { // Load recent conversation history const recentMessages = await models.AskAiMessage.findAll({ where: { conversationId }, @@ -151,8 +179,8 @@ export async function chat({ .map((m: any) => `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content}`) .join('\n'); - // Search RAG - const { chunks: ragContext, sources } = await searchRag( + // Search RAG before calling the LLM so it has relevant documentation to answer from + const { chunks, sources: collectedSources } = await searchRag( userMessage, ragDatabaseUrl, ragNamespace, @@ -161,28 +189,40 @@ export async function chat({ // BAML lazily reads env vars on each call — set the key before invoking process.env.ANTHROPIC_API_KEY = anthropicApiKey; - const response = await b.AskTamanu(userMessage, ragContext, conversationHistory); - // Attach RAG sources to response (BAML returns empty sources; we fill them from our search) - // Sources from the tamanu namespace are codebase file paths — not meaningful for end users - const includeSources = !response.cannotAnswer && ragNamespace !== 'tamanu'; - const result: AskAiResponse = { + const response = await b.AskTamanu( + userMessage, + conversationHistory, + serverConfig ?? '', + appSettings ?? '', + chunks, + ); + + // Sources from the tamanu codebase namespace are file paths — not meaningful for end users + const includeSources = collectedSources.length > 0 && ragNamespace !== 'tamanu'; + + const result: ChatResponse = { answer: response.answer, cannotAnswer: response.cannotAnswer, - sources: includeSources ? sources : [], + clarifyingQuestion: response.clarifyingQuestion, + sources: includeSources ? collectedSources : [], }; - // Persist messages + // Persist messages — always save the user message; save assistant response + // regardless of whether it's an answer or a clarifying question + const assistantContent = response.clarifyingQuestion || response.answer; await models.AskAiMessage.create({ conversationId, role: 'user', content: userMessage, }); - await models.AskAiMessage.create({ - conversationId, - role: 'assistant', - content: result.answer, - }); + if (assistantContent) { + await models.AskAiMessage.create({ + conversationId, + role: 'assistant', + content: assistantContent, + }); + } return result; } diff --git a/packages/web/app/App.jsx b/packages/web/app/App.jsx index d1e2221ff72..9d6fd9af29b 100644 --- a/packages/web/app/App.jsx +++ b/packages/web/app/App.jsx @@ -1,7 +1,10 @@ -import React from 'react'; +import React, { useState } from 'react'; import { useSelector } from 'react-redux'; import styled from 'styled-components'; import Bowser from 'bowser'; +import { IconButton } from '@material-ui/core'; +import { ChatBubbleOutline } from '@material-ui/icons'; +import { AskAiPanel } from './components/AskAi'; import 'typeface-roboto'; import { Colors } from './constants'; import { checkIsLoggedIn, checkIsFacilitySelected, getServerType } from './store/auth'; @@ -33,7 +36,24 @@ const AppContentsContainer = styled.div` border-top: 1px solid ${Colors.softOutline}; `; +const AskAiFab = styled(IconButton)` + position: fixed; + bottom: 24px; + right: 24px; + width: 48px; + height: 48px; + background: ${Colors.primaryDark}; + color: white; + z-index: 1200; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3); + + &:hover { + background: #4e5f71; + } +`; + export function App({ sidebar, children }) { + const [askAiOpen, setAskAiOpen] = useState(false); const { data: isServerAlive, isLoading } = useCheckServerAliveQuery(); const isUserLoggedIn = useSelector(checkIsLoggedIn); const isFacilitySelected = useSelector(checkIsFacilitySelected); @@ -76,6 +96,10 @@ export function App({ sidebar, children }) { + setAskAiOpen(o => !o)} title="Chat"> + + + setAskAiOpen(false)} /> ); } diff --git a/packages/web/app/components/AskAi/AskAiPanel.jsx b/packages/web/app/components/AskAi/AskAiPanel.jsx index 83101784164..e9efc3d3ff7 100644 --- a/packages/web/app/components/AskAi/AskAiPanel.jsx +++ b/packages/web/app/components/AskAi/AskAiPanel.jsx @@ -1,95 +1,123 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; -import { CircularProgress, IconButton, Paper, TextField, Typography } from '@material-ui/core'; -import { Close, Send } from '@material-ui/icons'; +import { CircularProgress, IconButton, TextField, Typography } from '@material-ui/core'; +import { Close, Remove, Send } from '@material-ui/icons'; +import Draggable from 'react-draggable'; +import { Resizable } from 're-resizable'; import Markdown from 'react-markdown'; import { Colors } from '../../constants'; import { useApi } from '../../api'; -const PanelContainer = styled(Paper)` +const OuterContainer = styled.div` position: fixed; - bottom: 0; - left: 280px; - width: 380px; - height: 500px; + bottom: 80px; + right: 24px; + z-index: 1300; display: flex; flex-direction: column; - background: ${Colors.primaryDark}; - color: ${Colors.white}; - box-shadow: 0 -2px 16px rgba(0, 0, 0, 0.4); - border-radius: 8px 8px 0 0; - z-index: 1300; - overflow: hidden; + filter: drop-shadow(0 4px 20px rgba(0, 0, 0, 0.15)); + border: 1px solid ${Colors.softOutline}; + border-radius: 8px; `; const Header = styled.div` display: flex; align-items: center; justify-content: space-between; - padding: 12px 16px; - border-bottom: 1px solid rgba(255, 255, 255, 0.15); + padding: 10px 14px; + background: ${Colors.white}; + border-radius: ${props => (props.$minimised ? '8px' : '8px 8px 0 0')}; + border-bottom: ${props => (props.$minimised ? 'none' : `1px solid ${Colors.softOutline}`)}; flex-shrink: 0; + cursor: grab; + user-select: none; + + &:active { + cursor: grabbing; + } `; const HeaderTitle = styled(Typography)` - color: ${Colors.white}; + color: ${Colors.darkestText}; font-weight: 600; - font-size: 15px; + font-size: 14px; `; -const CloseButton = styled(IconButton)` +const HeaderButtons = styled.div` + display: flex; + gap: 2px; +`; + +const HeaderButton = styled(IconButton)` padding: 4px; - color: ${Colors.white}; + color: ${Colors.midText}; &:hover { - background-color: rgba(255, 255, 255, 0.1); + background-color: ${Colors.background}; } `; +const PanelBody = styled.div` + display: flex; + flex-direction: column; + background: ${Colors.white}; + border-radius: 0 0 8px 8px; + overflow: hidden; + height: 100%; +`; + const MessageList = styled.div` flex: 1; overflow-y: auto; - padding: 16px; + padding: 14px; display: flex; flex-direction: column; - gap: 12px; + gap: 10px; `; const MessageBubble = styled.div` - max-width: 90%; + max-width: 88%; padding: 8px 12px; border-radius: ${props => (props.$role === 'user' ? '12px 12px 2px 12px' : '12px 12px 12px 2px')}; - background: ${props => - props.$role === 'user' ? 'rgba(255,255,255,0.15)' : 'rgba(255,255,255,0.07)'}; + background: ${props => (props.$role === 'user' ? Colors.primary10 : Colors.background)}; align-self: ${props => (props.$role === 'user' ? 'flex-end' : 'flex-start')}; - color: ${Colors.white}; + color: ${Colors.darkestText}; font-size: 13px; line-height: 1.5; word-break: break-word; + border: 1px solid ${props => (props.$role === 'user' ? Colors.softOutline : Colors.outline)}; p { margin: 0 0 6px 0; } p:last-child { margin-bottom: 0; } ul, ol { margin: 4px 0; padding-left: 18px; } li { margin: 2px 0; } - code { background: rgba(255,255,255,0.15); border-radius: 3px; padding: 1px 4px; font-size: 12px; } - pre { background: rgba(0,0,0,0.3); border-radius: 4px; padding: 8px; overflow-x: auto; } + code { background: rgba(0,0,0,0.07); border-radius: 3px; padding: 1px 4px; font-size: 12px; } + pre { background: rgba(0,0,0,0.06); border-radius: 4px; padding: 8px; overflow-x: auto; } pre code { background: none; padding: 0; } strong { font-weight: 600; } `; const CannotAnswerBubble = styled(MessageBubble)` - background: rgba(255, 200, 100, 0.15); - border: 1px solid rgba(255, 200, 100, 0.3); - color: rgba(255, 255, 255, 0.8); + background: #fff8e6; + border: 1px solid #f0d070; + color: #7a5c00; font-style: italic; `; const SourcesBox = styled.div` - margin-top: 6px; + margin-top: 4px; font-size: 11px; - color: rgba(255, 255, 255, 0.5); + color: ${Colors.midText}; align-self: flex-start; `; +const DisclaimerNote = styled.div` + font-size: 11px; + color: ${Colors.softText}; + font-style: italic; + align-self: flex-start; + margin-top: 2px; +`; + const SourceItem = styled.div` font-family: monospace; font-size: 11px; @@ -99,7 +127,7 @@ const LoadingRow = styled.div` display: flex; align-items: center; gap: 8px; - color: rgba(255, 255, 255, 0.6); + color: ${Colors.midText}; font-size: 13px; align-self: flex-start; padding: 4px 0; @@ -109,34 +137,35 @@ const InputArea = styled.div` display: flex; align-items: flex-end; gap: 8px; - padding: 12px 16px; - border-top: 1px solid rgba(255, 255, 255, 0.15); + padding: 10px 12px; + border-top: 1px solid ${Colors.outline}; flex-shrink: 0; + background: ${Colors.white}; `; const StyledTextField = styled(TextField)` flex: 1; & .MuiInputBase-root { - color: ${Colors.white}; - background: rgba(255, 255, 255, 0.08); + color: ${Colors.darkestText}; + background: ${Colors.background}; border-radius: 6px; font-size: 13px; padding: 8px 12px; } & .MuiInputBase-root:hover { - background: rgba(255, 255, 255, 0.12); + background: ${Colors.softOutline}; } & .MuiOutlinedInput-notchedOutline { - border-color: rgba(255, 255, 255, 0.2); + border-color: ${Colors.outline}; } & .MuiOutlinedInput-root:hover .MuiOutlinedInput-notchedOutline { - border-color: rgba(255, 255, 255, 0.4); + border-color: ${Colors.softText}; } & .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline { - border-color: rgba(255, 255, 255, 0.6); + border-color: ${Colors.primary}; } & .MuiInputBase-input::placeholder { - color: rgba(255, 255, 255, 0.4); + color: ${Colors.softText}; opacity: 1; } `; @@ -144,14 +173,14 @@ const StyledTextField = styled(TextField)` const SendButton = styled(IconButton)` padding: 8px; color: ${Colors.white}; - background: rgba(255, 255, 255, 0.12); + background: ${Colors.primary}; border-radius: 6px; &:hover { - background: rgba(255, 255, 255, 0.2); + background: ${Colors.primaryDark}; } &:disabled { - color: rgba(255, 255, 255, 0.3); - background: rgba(255, 255, 255, 0.05); + color: ${Colors.softText}; + background: ${Colors.softOutline}; } `; @@ -161,21 +190,23 @@ export const AskAiPanel = ({ open, onClose }) => { const [input, setInput] = useState(''); const [conversationId, setConversationId] = useState(null); const [isLoading, setIsLoading] = useState(false); + const [minimised, setMinimised] = useState(false); + const [size, setSize] = useState({ width: 380, height: 500 }); const scrollRef = useRef(null); + const nodeRef = useRef(null); - // Auto-scroll to bottom when messages change useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, [messages, isLoading]); - // Reset state when panel closes const handleClose = useCallback(() => { setMessages([]); setInput(''); setConversationId(null); setIsLoading(false); + setMinimised(false); onClose(); }, [onClose]); @@ -183,6 +214,7 @@ export const AskAiPanel = ({ open, onClose }) => { const trimmed = input.trim(); if (!trimmed || isLoading) return; + setMinimised(false); setInput(''); setMessages(prev => [...prev, { role: 'user', content: trimmed }]); setIsLoading(true); @@ -199,13 +231,14 @@ export const AskAiPanel = ({ open, onClose }) => { content: trimmed, }); + const isClarifying = Boolean(response.clarifyingQuestion); setMessages(prev => [ ...prev, { role: 'assistant', - content: response.cannotAnswer ? null : response.answer, + content: isClarifying ? response.clarifyingQuestion : response.answer, sources: response.sources ?? [], - cannotAnswer: response.cannotAnswer, + cannotAnswer: !isClarifying && response.cannotAnswer, }, ]); } catch { @@ -231,75 +264,140 @@ export const AskAiPanel = ({ open, onClose }) => { if (!open) return null; return ( - -
- Ask AI - - - -
- - - {messages.length === 0 && ( -
- Ask a question about how Tamanu works. -
- )} + + +
+ Chat + + setMinimised(m => !m)} + size="small" + title={minimised ? 'Restore' : 'Minimise'} + > + + + + + + +
+ + {!minimised && ( + { + setSize(prev => ({ + width: prev.width + delta.width, + height: prev.height + delta.height, + })); + }} + minWidth={280} + minHeight={200} + maxWidth={700} + maxHeight={800} + enable={{ + top: true, + left: true, + topLeft: true, + topRight: true, + bottomLeft: true, + bottom: false, + right: false, + bottomRight: false, + }} + > + + + {messages.length === 0 && ( +
+ Ask a question about how Tamanu works. +
+ )} + + {messages.map((msg, index) => { + if (msg.cannotAnswer) { + return ( + + + I don't have enough information to answer that question. For + direct support, visit{' '} + + bes-support.zendesk.com + + . + + + ); + } + + return ( + + + {msg.content} + + {msg.role === 'assistant' && msg.sources?.length > 0 && ( + + Sources: + {msg.sources.map((source, i) => ( + {source.filePath} + ))} + + )} + {msg.role === 'assistant' && ( + Note: This answer may need verification + )} + + ); + })} + + {isLoading && ( + + + Thinking… + + )} +
- {messages.map((msg, index) => { - if (msg.cannotAnswer) { - return ( - - I don't have enough information to answer that question. - - ); - } - - return ( - - - {msg.content} - - {msg.role === 'assistant' && msg.sources?.length > 0 && ( - - Sources: - {msg.sources.map((source, i) => ( - {source.filePath} - ))} - - )} - - ); - })} - - {isLoading && ( - - - Thinking… - + + setInput(e.target.value)} + onKeyDown={handleKeyDown} + disabled={isLoading} + inputProps={{ 'data-testid': 'askai-input' }} + /> + + {isLoading ? ( + + ) : ( + + )} + + +
+
)} -
- - - setInput(e.target.value)} - onKeyDown={handleKeyDown} - disabled={isLoading} - inputProps={{ 'data-testid': 'askai-input' }} - /> - - {isLoading ? ( - - ) : ( - - )} - - -
+ + ); }; diff --git a/packages/web/app/components/Sidebar/Sidebar.jsx b/packages/web/app/components/Sidebar/Sidebar.jsx index b7556c902fa..47da21c881c 100644 --- a/packages/web/app/components/Sidebar/Sidebar.jsx +++ b/packages/web/app/components/Sidebar/Sidebar.jsx @@ -2,7 +2,6 @@ import React, { useCallback, useMemo, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import styled from 'styled-components'; import { Box, Button, Divider, IconButton, List, Typography } from '@material-ui/core'; -import ChatBubbleOutline from '@mui/icons-material/ChatBubbleOutline'; import NavigateBefore from '@mui/icons-material/NavigateBefore'; import NavigateNext from '@mui/icons-material/NavigateNext'; import { useNavigate, useLocation } from 'react-router'; @@ -19,7 +18,6 @@ import { useAuth } from '../../contexts/Auth'; import { useApi } from '../../api'; import { KebabMenu } from './KebabMenu'; import { NoteModalActionBlocker } from '../NoteModalActionBlocker'; -import { AskAiPanel } from '../AskAi'; const Container = styled.div` display: flex; @@ -140,31 +138,6 @@ const StyledMetadataBox = styled(Box)` margin-bottom: 5px; `; -const AskAiToggleButton = styled.button` - display: flex; - align-items: center; - gap: 10px; - width: 100%; - padding: ${props => (props.$retracted ? '10px 0' : '10px 13px')}; - background: none; - border: none; - border-radius: 4px; - color: ${Colors.white}; - cursor: pointer; - font-size: 14px; - justify-content: ${props => (props.$retracted ? 'center' : 'flex-start')}; - transition: background 0.15s; - margin-bottom: 4px; - - &:hover { - background: rgba(255, 255, 255, 0.1); - } - - & svg { - font-size: 20px; - flex-shrink: 0; - } -`; const getInitials = string => string @@ -193,7 +166,6 @@ const isHighlighted = (currentPath, menuItemPath, sectionIsOpen, isRetracted) => export const Sidebar = React.memo(({ items }) => { const [selectedParentItem, setSelectedParentItem] = useState(''); const [isRetracted, setIsRetracted] = useState(false); - const [askAiOpen, setAskAiOpen] = useState(false); const api = useApi(); const { facilityId, currentUser, onLogout, currentRole } = useAuth(); const location = useLocation(); @@ -361,14 +333,6 @@ export const Sidebar = React.memo(({ items }) => { })}
- setAskAiOpen(o => !o)} - data-testid="askai-toggle-button" - > - - {!isRetracted && 'Ask AI'} - { )}
- setAskAiOpen(false)} /> ); }); From 3b2a212a311a782d7324065ca654c460083b9c46 Mon Sep 17 00:00:00 2001 From: julianam-w <77252335+julianam-w@users.noreply.github.com> Date: Sat, 4 Apr 2026 20:49:36 +1100 Subject: [PATCH 03/46] chore: no-issue: add Ask AI planning doc and cheatsheet Co-Authored-By: Claude Sonnet 4.6 --- ASK-AI-PLAN.md | 308 +++++++++++++++++++++++++++++++++++++++++++++++++ cheatsheet.md | 104 +++++++++++++++++ 2 files changed, 412 insertions(+) create mode 100644 ASK-AI-PLAN.md create mode 100644 cheatsheet.md diff --git a/ASK-AI-PLAN.md b/ASK-AI-PLAN.md new file mode 100644 index 00000000000..8615786a6ad --- /dev/null +++ b/ASK-AI-PLAN.md @@ -0,0 +1,308 @@ +# Ask AI Chatbot — Implementation Plan + +## Context + +Tamanu end users need help understanding the system — "Can I book recurring appointments?", "Why am I getting this error?" — without needing to raise a support ticket. This feature adds an "Ask AI" chatbot panel to the Tamanu sidebar that answers questions about Tamanu's features and known errors using RAG over indexed codebase and documentation content. It has no access to live Tamanu data in this iteration. + +**Next iteration** (out of scope now): admin-only access to logs, settings, and config files for deeper debugging. + +## Architecture + +- **RAG**: github-repo-rag Python sidecar populates a `rag` schema in Tamanu's existing Postgres DB using pgvector. Tamanu's Node.js server queries it directly with raw SQL, embedding the user's query via Voyage AI. +- **LLM interaction**: Managed via BAML (BoundaryML) — `.baml` files define prompts and structured response types, generating type-safe TypeScript clients. +- **Agent definition**: GitAgent standard — `SOUL.md`, `SKILL.md`, and `agent.yaml` files define the agent's identity and capabilities in version control. +- **LLM calls**: Server-side via `@anthropic-ai/sdk`. Key never exposed to browser. +- **Conversation history**: Stored in a dedicated `ask_ai` Postgres schema. Not synced. +- **No data access**: RAG only — no live settings, logs, or config in this iteration. +- **Access**: All authenticated users. +- **Servers**: Both facility-server and central-server. +- **API keys required**: `ANTHROPIC_API_KEY` (LLM calls) + `VOYAGE_API_KEY` (query embedding for RAG search). + +--- + +## Phase 1: Configuration +*Testable: immediately, with no other phases complete.* + +Add to `packages/facility-server/config/default.json5` and `packages/central-server/config/default.json5`: + +```json5 +askAi: { + enabled: false, // Set to true in local.json5 + anthropicApiKey: '', // Anthropic API key + voyageApiKey: '', // Voyage AI API key (for query embedding) + ragNamespace: 'tamanu', // Namespace used when indexing with github-repo-rag +} +``` + +**Checkpoint:** `config.get('askAi.enabled')` returns `false` in tests; set to `true` in `local.json5` and confirm it reads correctly. + +--- + +## Phase 2: GitAgent Definition +*Testable: review only — no runtime dependencies.* + +Create version-controlled agent specification files at `ask-ai/agent/`: + +**`agent.yaml`** — agent manifest: +```yaml +name: tamanu-ask-ai +version: 1.0.0 +description: End-user assistant for Tamanu healthcare management system +model: claude-sonnet-4-6 +skills: + - rag-search +memory: false +``` + +**`SOUL.md`** — agent identity: friendly, plain-language helper for clinical staff. Cannot access live patient data or system configs. + +**`SKILL.md`** — agent capabilities: feature how-tos, error explanations, workflow guidance. Documents what it cannot answer (live data, patient records). + +**Checkpoint:** Files exist in repo, reviewed for accuracy. No code to run. + +--- + +## Phase 3: BAML Setup +*Testable: independently, needs only an Anthropic API key.* + +BAML manages the LLM interaction contract — prompts and typed response shapes compile to TypeScript. + +**Install:** `@boundaryml/baml` in `packages/shared/package.json`. Add `baml-cli generate` step to `packages/shared`'s build script. + +**File:** `packages/shared/src/baml/ask-ai.baml` + +```baml +client AnthropicClient { + provider anthropic + options { + model "claude-sonnet-4-6" + api_key env.ANTHROPIC_API_KEY + } +} + +class RagSource { + filePath string + excerpt string +} + +class AskAiResponse { + answer string + sources RagSource[] + cannotAnswer bool +} + +function AskTamanu( + userQuestion: string, + ragContext: string, + conversationHistory: string +) -> AskAiResponse { + client AnthropicClient + prompt #" + You are a helpful assistant for Tamanu, a healthcare management system. + Answer questions from clinical staff about how Tamanu works. + Use only the provided context — do not guess or invent features. + If the context does not contain enough information to answer, set cannotAnswer to true. + + Context from Tamanu documentation and codebase: + {{ ragContext }} + + Conversation so far: + {{ conversationHistory }} + + User question: {{ userQuestion }} + + {{ ctx.output_format }} + "# +} +``` + +BAML generates `packages/shared/src/baml/baml_client/` — the TypeScript client used by `AskAiService`. + +**Checkpoint:** `baml-cli generate` runs without errors. Generated TypeScript compiles. Write a standalone script that calls `b.AskTamanu("Can I book recurring appointments?", "mock context", "")` with a real Anthropic key and verify a typed `AskAiResponse` comes back. + +--- + +## Phase 4: Database — `rag` + `ask_ai` schemas +*Testable: independently, needs only a local Postgres instance.* + +### 4a. Migration: Enable pgvector and create schemas +**File:** `packages/database/src/migrations/{timestamp}-createAskAiSchemas.ts` + +```sql +CREATE EXTENSION IF NOT EXISTS vector; +CREATE SCHEMA IF NOT EXISTS rag; +CREATE SCHEMA IF NOT EXISTS ask_ai; +``` + +The `rag` tables are populated by the github-repo-rag sidecar. Tamanu only needs the schema to exist. + +### 4b. Migration: Create conversation tables (DDL) +**File:** `packages/database/src/migrations/{timestamp}-createAskAiTables.ts` + +```sql +CREATE TABLE ask_ai.conversations ( + id uuid PRIMARY KEY, + user_id uuid NOT NULL REFERENCES users(id), + title text, + created_at timestamp NOT NULL DEFAULT now(), + updated_at timestamp NOT NULL DEFAULT now(), + deleted_at timestamp +); + +CREATE TABLE ask_ai.messages ( + id uuid PRIMARY KEY, + conversation_id uuid NOT NULL REFERENCES ask_ai.conversations(id), + role text NOT NULL, -- 'user' | 'assistant' + content text NOT NULL, + created_at timestamp NOT NULL DEFAULT now(), + updated_at timestamp NOT NULL DEFAULT now() +); + +CREATE INDEX idx_ask_ai_conversations_user_id ON ask_ai.conversations(user_id); +CREATE INDEX idx_ask_ai_messages_conversation_id ON ask_ai.messages(conversation_id); +``` + +No `updated_at_sync_tick` — not sync-eligible. + +### 4c. Sequelize models +- `packages/database/src/models/AskAiConversation.ts` — maps to `ask_ai.conversations` +- `packages/database/src/models/AskAiMessage.ts` — maps to `ask_ai.messages` + +Both extend `Model`. `syncDirection: SYNC_DIRECTIONS.DO_NOT_SYNC`. Use `schema: 'ask_ai'` in `initModel` options. Register in `packages/database/src/models/index.ts`. + +**Checkpoint:** Run migrations (`npm run facility-migrate`). Verify `ask_ai.conversations` and `ask_ai.messages` tables exist. Verify `rag` schema exists. Create and query a test conversation record via Sequelize model in a test script. + +--- + +## Phase 5: AskAiService +*Testable: unit tests with mocks (no API keys); integration test with real keys + populated `rag` schema.* + +**File:** `packages/shared/src/services/AskAiService.ts` + +### 5a. RAG query embedding + search +```typescript +async searchRag(query: string, db: Sequelize, namespace: string, voyageApiKey: string): Promise<{ chunks: string, sources: RagSource[] }> +``` + +1. POST to Voyage AI REST API to embed the query (`voyage-code-3`, 1024 dimensions) +2. Raw SQL with pgvector cosine distance + full-text search, RRF-ranked, against `rag.{namespace}_code` and `rag.{namespace}_docs` +3. Return top-6 chunks with file paths + +Mirrors the pattern in `rag/query.py` from github-repo-rag. + +### 5b. Main chat method +```typescript +async chat(params: { + conversationId: string, + userMessage: string, + db: Sequelize, + models: Models, + voyageApiKey: string, + ragNamespace: string, +}): Promise +``` + +1. Load last 20 messages from `ask_ai.messages` +2. Call `searchRag` with the user's message +3. Call BAML `b.AskTamanu(userMessage, ragContext, conversationHistory)` +4. Persist user + assistant messages to `ask_ai.messages` +5. Return `AskAiResponse` + +**Checkpoint (unit):** Mock Voyage API, mock DB queries, mock BAML client. Verify `chat()` persists messages and returns a well-formed `AskAiResponse`. Verify `cannotAnswer: true` is returned when RAG context is empty. + +**Checkpoint (integration):** With real API keys and a `rag` schema populated by github-repo-rag, run a script calling `chat()` directly. Verify a meaningful answer to "Can I book recurring appointments?" comes back with sources. + +--- + +## Phase 6: API Routes +*Testable: facility-server integration tests with a test database.* + +**File:** `packages/facility-server/app/routes/apiv1/askAi.js` +**Register in:** `packages/facility-server/app/routes/apiv1/index.js` +**Central server:** same routes added to central-server's route builder + +``` +POST /api/v1/ask-ai/conversations — create conversation +GET /api/v1/ask-ai/conversations — list user's own conversations +GET /api/v1/ask-ai/conversations/:id — get conversation + messages +POST /api/v1/ask-ai/conversations/:id/messages — send message, get AI response +DELETE /api/v1/ask-ai/conversations/:id — soft-delete conversation +``` + +- `req.checkPermission('write', 'AskAiMessage')` on message send +- Users access only their own conversations (`userId = req.user.id`) +- Returns 503 if `askAi.enabled` is false + +**Checkpoint:** Integration tests (existing facility-server test harness): +- `POST /ask-ai/conversations` returns 200 + conversation object +- `POST /ask-ai/conversations/:id/messages` returns 200 + assistant response (mock `AskAiService` in tests) +- Unauthenticated request returns 401 +- Request with `askAi.enabled = false` returns 503 +- User cannot fetch another user's conversation (403) + +--- + +## Phase 7: Frontend — Sidebar Chat Panel +*Testable: manually in browser once Phase 6 is working.* + +### 7a. AskAiPanel component +**File:** `packages/web/app/components/Sidebar/AskAiPanel.jsx` + +- Collapsible panel with local `isOpen` state +- **Collapsed**: icon + "Ask AI" label (icon-only when sidebar retracted) +- **Expanded**: ~300px scrollable message list + text input; shows file path sources beneath each assistant response; graceful "I don't have enough context" display when `cannotAnswer: true` +- `useApi()` for API calls; lazily creates conversation on first message send +- Styled to `Colors.primaryDark`, white text + +### 7b. Sidebar integration +**File:** `packages/web/app/components/Sidebar/Sidebar.jsx` + +Insert between `` and `