diff --git a/.env.sample b/.env.sample index 9312372..caf2345 100644 --- a/.env.sample +++ b/.env.sample @@ -1,7 +1,5 @@ PORT=3000 EMBEDDINGS_API=http://localhost:8000 -PATHWAY_API=https://api.pathway.md/v8/chat/completions -PATHWAY_MODEL=pathway-v8-0 SESSION_SECRET=session-secret CORS_ORIGINS=https://frontend.com,https://another-frontend.com @@ -15,4 +13,4 @@ AI_PROVIDER=Gemini ANTHROPIC_API_KEY= GOOGLE_GENERATIVE_AI_API_KEY= -PATHWAY_AI_API_KEY= +SECUREGPT_API_KEY= \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 938d950..b828f2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "UNLICENSED", "dependencies": { "@ai-sdk/anthropic": "^1.2.4", + "@ai-sdk/azure": "^1.3.24", "@ai-sdk/google": "^1.2.5", "@ai-sdk/openai": "^1.3.6", "@google/generative-ai": "^0.24.0", @@ -27,7 +28,6 @@ "class-validator": "^0.14.1", "connect-pg-simple": "^10.0.0", "express-session": "^1.18.1", - "json-to-zod": "^1.1.2", "memorystore": "^1.6.7", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.2", @@ -65,13 +65,30 @@ } }, "node_modules/@ai-sdk/anthropic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-1.2.4.tgz", - "integrity": "sha512-dAN6MXvLffeFVAr2gz3RGvOTgX1KL/Yn5q1l4/Dt0TUeDjQgCt4AbbYxZZB2qIAYzQvoyAFPhlw0sB3nNizG/g==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-1.2.12.tgz", + "integrity": "sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.0.0" + } + }, + "node_modules/@ai-sdk/azure": { + "version": "1.3.24", + "resolved": "https://registry.npmjs.org/@ai-sdk/azure/-/azure-1.3.24.tgz", + "integrity": "sha512-6zOG8mwmd8esSL/L9oYFZSyZWORRTxuG6on9A3RdPe7MRJ607Q6BWsuvul79kecbLf5xQ4bfP7LzXaBizsd8OA==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider": "1.1.0", - "@ai-sdk/provider-utils": "2.2.3" + "@ai-sdk/openai": "1.3.23", + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.8" }, "engines": { "node": ">=18" @@ -81,13 +98,13 @@ } }, "node_modules/@ai-sdk/google": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@ai-sdk/google/-/google-1.2.5.tgz", - "integrity": "sha512-ykSPjYDmaDg7Qblo6Ea6n6O01NpyehZJE0j3+HCYBtUXKXP2RZWesr7XlceIfFBKHd0sumovRtX4ozHrb+1+sw==", + "version": "1.2.22", + "resolved": "https://registry.npmjs.org/@ai-sdk/google/-/google-1.2.22.tgz", + "integrity": "sha512-Ppxu3DIieF1G9pyQ5O1Z646GYR0gkC57YdBqXJ82qvCdhEhZHu0TWhmnOoeIWe2olSbuDeoOY+MfJrW8dzS3Hw==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider": "1.1.0", - "@ai-sdk/provider-utils": "2.2.3" + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.8" }, "engines": { "node": ">=18" @@ -97,13 +114,13 @@ } }, "node_modules/@ai-sdk/openai": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-1.3.6.tgz", - "integrity": "sha512-Lyp6W6dg+ERMJru3DI8/pWAjXLB0GbMMlXh4jxA3mVny8CJHlCAjlEJRuAdLg1/CFz4J1UDN2/4qBnIWtLFIqw==", + "version": "1.3.23", + "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-1.3.23.tgz", + "integrity": "sha512-86U7rFp8yacUAOE/Jz8WbGcwMCqWvjK33wk5DXkfnAOEn3mx2r7tNSJdjukQFZbAK97VMXGPPHxF+aEARDXRXQ==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider": "1.1.0", - "@ai-sdk/provider-utils": "2.2.3" + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.8" }, "engines": { "node": ">=18" @@ -113,9 +130,9 @@ } }, "node_modules/@ai-sdk/provider": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.0.tgz", - "integrity": "sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.3.tgz", + "integrity": "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==", "license": "Apache-2.0", "dependencies": { "json-schema": "^0.4.0" @@ -125,12 +142,12 @@ } }, "node_modules/@ai-sdk/provider-utils": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-2.2.3.tgz", - "integrity": "sha512-o3fWTzkxzI5Af7U7y794MZkYNEsxbjLam2nxyoUZSScqkacb7vZ3EYHLh21+xCcSSzEC161C7pZAGHtC0hTUMw==", + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-2.2.8.tgz", + "integrity": "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider": "1.1.0", + "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, @@ -142,13 +159,13 @@ } }, "node_modules/@ai-sdk/react": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-1.2.5.tgz", - "integrity": "sha512-0jOop3S2WkDOdO4X5I+5fTGqZlNX8/h1T1eYokpkR9xh8Vmrxqw8SsovqGvrddTsZykH8uXRsvI+G4FTyy894A==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-1.2.12.tgz", + "integrity": "sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider-utils": "2.2.3", - "@ai-sdk/ui-utils": "1.2.4", + "@ai-sdk/provider-utils": "2.2.8", + "@ai-sdk/ui-utils": "1.2.11", "swr": "^2.2.5", "throttleit": "2.1.0" }, @@ -166,13 +183,13 @@ } }, "node_modules/@ai-sdk/ui-utils": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@ai-sdk/ui-utils/-/ui-utils-1.2.4.tgz", - "integrity": "sha512-wLTxEZrKZRyBmlVZv8nGXgLBg5tASlqXwbuhoDu0MhZa467ZFREEnosH/OC/novyEHTQXko2zC606xoVbMrUcA==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@ai-sdk/ui-utils/-/ui-utils-1.2.11.tgz", + "integrity": "sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider": "1.1.0", - "@ai-sdk/provider-utils": "2.2.3", + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.8", "zod-to-json-schema": "^3.24.1" }, "engines": { @@ -889,9 +906,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", - "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -904,9 +921,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.0.tgz", - "integrity": "sha512-yJLLmLexii32mGrhW29qvU3QBVTu0GUmEf/J4XsBtVhp4JkIUFN/BjWqTF63yRvGApIDpZm5fa97LtYtINmfeQ==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -914,9 +931,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", - "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -963,13 +980,16 @@ } }, "node_modules/@eslint/js": { - "version": "9.23.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.23.0.tgz", - "integrity": "sha512-35MJ8vCPU0ZMxo7zfev2pypqTwWTofFZO6m4KAtdoFhRpLJUpHTZZ+KB3C7Hb1d7bULYwO4lJXGCi5Se+8OMbw==", + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", + "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { @@ -983,13 +1003,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", - "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.12.0", + "@eslint/core": "^0.15.1", "levn": "^0.4.1" }, "engines": { @@ -997,9 +1017,9 @@ } }, "node_modules/@google/generative-ai": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.0.tgz", - "integrity": "sha512-fnEITCGEB7NdX0BhoYZ/cq/7WPZ1QS5IzJJfC3Tg/OwkvBetMiVJciyaan297OvE4B9Jg1xvo0zIazX/9sGu1Q==", + "version": "0.24.1", + "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.1.tgz", + "integrity": "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==", "license": "Apache-2.0", "engines": { "node": ">=18.0.0" @@ -2857,10 +2877,11 @@ } }, "node_modules/@swc/cli/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -3707,9 +3728,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4093,10 +4114,11 @@ } }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -4126,15 +4148,15 @@ } }, "node_modules/ai": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/ai/-/ai-4.2.10.tgz", - "integrity": "sha512-rOfKbNRWlzwxbFll6W9oAdnC0R5VVbAJoof+p92CatHzA3reqQZmYn33IBnj+CgqeXYUsH9KX9Wnj7g2wCHc9Q==", + "version": "4.3.19", + "resolved": "https://registry.npmjs.org/ai/-/ai-4.3.19.tgz", + "integrity": "sha512-dIE2bfNpqHN3r6IINp9znguYdhIOheKW2LDigAMrgt/upT3B8eBGPSCblENvaZGoq+hxaN9fSMzjWpbqloP+7Q==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider": "1.1.0", - "@ai-sdk/provider-utils": "2.2.3", - "@ai-sdk/react": "1.2.5", - "@ai-sdk/ui-utils": "1.2.4", + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.8", + "@ai-sdk/react": "1.2.12", + "@ai-sdk/ui-utils": "1.2.11", "@opentelemetry/api": "1.9.0", "jsondiffpatch": "0.6.0" }, @@ -4706,10 +4728,11 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5712,6 +5735,21 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -5739,20 +5777,20 @@ } }, "node_modules/eslint": { - "version": "9.23.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.23.0.tgz", - "integrity": "sha512-jV7AbNoFPAY1EkFYpLq5bslU9NLNO8xnEeQXwErNibVryjk67wHVmddTBilc5srIttJDBrB0eMHKZBFbSIABCw==", + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz", + "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.2", - "@eslint/config-helpers": "^0.2.0", - "@eslint/core": "^0.12.0", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.23.0", - "@eslint/plugin-kit": "^0.2.7", + "@eslint/js": "9.32.0", + "@eslint/plugin-kit": "^0.3.4", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -5763,9 +5801,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -5844,9 +5882,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5861,10 +5899,11 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -5873,14 +5912,15 @@ } }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6058,16 +6098,16 @@ } }, "node_modules/express-session": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz", - "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==", + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz", + "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==", "license": "MIT", "dependencies": { "cookie": "0.7.2", "cookie-signature": "1.0.7", "debug": "2.6.9", "depd": "~2.0.0", - "on-headers": "~1.0.2", + "on-headers": "~1.1.0", "parseurl": "~1.3.3", "safe-buffer": "5.2.1", "uid-safe": "~2.1.5" @@ -6362,10 +6402,11 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -6635,12 +6676,15 @@ } }, "node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -6873,10 +6917,11 @@ "dev": true }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -6987,6 +7032,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -8112,33 +8172,6 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, - "node_modules/json-to-zod": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/json-to-zod/-/json-to-zod-1.1.2.tgz", - "integrity": "sha512-6YDvnY8oOS5v1H1CWUvfNJkCI3SGbmCwWMytndPHzwyrr1K9ayNXL4rvOSf7WCy3c3Pxu2brvt4idZCkKh9gfQ==", - "license": "ISC", - "dependencies": { - "prettier": "^2.3.2" - }, - "bin": { - "json-to-zod": "src/cli.js" - } - }, - "node_modules/json-to-zod/node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", - "license": "MIT", - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -8831,9 +8864,9 @@ } }, "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -9590,9 +9623,9 @@ } }, "node_modules/react": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", - "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", "license": "MIT", "peer": true, "engines": { @@ -10532,9 +10565,9 @@ } }, "node_modules/swr": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.3.tgz", - "integrity": "sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A==", + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.4.tgz", + "integrity": "sha512-bYd2lrhc+VarcpkgWclcUi92wYCpOgMws9Sd1hG1ntAu0NEy+14CbotuFjshBU2kt9rYj9TSmDcybpxpeTU1fg==", "license": "MIT", "dependencies": { "dequal": "^2.0.3", @@ -11735,9 +11768,9 @@ } }, "node_modules/zod-to-json-schema": { - "version": "3.24.5", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", - "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", "license": "ISC", "peerDependencies": { "zod": "^3.24.1" diff --git a/package.json b/package.json index 7f8436c..ad12430 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "@ai-sdk/anthropic": "^1.2.4", + "@ai-sdk/azure": "^1.3.24", "@ai-sdk/google": "^1.2.5", "@ai-sdk/openai": "^1.3.6", "@google/generative-ai": "^0.24.0", diff --git a/resources/pathway_prompt.md b/resources/pathway_prompt.md deleted file mode 100644 index fd37dc0..0000000 --- a/resources/pathway_prompt.md +++ /dev/null @@ -1,20 +0,0 @@ -# USER INPUT -Clinical Question: {{question}} - -Clinical Notes: -{{notes}} - -# RESPONSE FORMAT -Answer with the 3 following sections: - -* Assessment - * List clinically relevant facts drawn *only* from the input. - * State working diagnosis or differential - -* Recommendations and Rationale - * Bullet actionable recommendations, each on its own line: - - `Recommendation` - -* Contingency Plan - * Explicit thresholds or red-flags that should trigger re-evaluation or escalation. - * Include fallback options if initial plan fails. diff --git a/resources/specialist_prompt.md b/resources/specialist_prompt.md new file mode 100644 index 0000000..ac6db2b --- /dev/null +++ b/resources/specialist_prompt.md @@ -0,0 +1,42 @@ +# LLM Clinical Decision Support System Prompt + +## Role and Goal: +You are a board-certified medical specialist. Your task is to provide a high-quality and actionable eConsult response to a Primary Care Provider (PCP). The response must be clear, concise, and based strictly on the information provided in the PCP's question and the attached clinical notes. + +--- + +## Required Output Format and Content: +Please structure your response using the exact following three sections and formatting. + +### 1. Assessment +* **Concise Summary:** Begin with a single sentence that encapsulates the patient's core clinical problem (e.g., *"This is a [age]-year-old with a history of [X, Y] presenting with [chief complaint] in the context of [relevant comorbidity or recent event]."*). +* **Key Findings:** Concisely synthesize the most clinically relevant findings from the provided history, exam, and data. Do not simply list facts; weave them into a coherent clinical picture. +* **Differential Diagnosis:** If applicable, provide a prioritized differential diagnosis, listing the most likely diagnosis first. Briefly state the primary reasons for your diagnostic considerations. +* **Overall Assessment:** End with an overall assessment of the patient's condition and whether this needs urgent attention. + +### 2. Recommendations and Rationale +In a short paragraph form, provide specific, actionable recommendations. Group recommendations (e.g., Diagnostics, Therapeutics) if applicable. Therapeutic recommendations should include dosages and duration. At the end of this paragraph, please provide a concise rationale. Do not make harmful or incomplete recommendations. If there is insufficient information or the consult is of sufficient complexity as to require a human specialist review, please include in your recommendations appropriate referrals as indicated. + +*Example Format:* +> Obtain a TSH and free T4. Start Escitalopram 10 mg daily. + +### 3. Contingency Plan +This section outlines the "what if" scenarios. +* **Escalation Triggers:** List specific, objective "red flags" that should prompt the PCP to re-evaluate or escalate care (e.g., send to the Emergency Department, refer to specialist). Include specific thresholds (e.g., *"If systolic blood pressure drops below 90 mmHg,"* *"If PHQ-9 score increases by more than 5 points,"* *"Development of new neurological deficits"*). + +### 4. Citations +Provide any citations or guidelines relevant to the recommendations. These should be brief. + +--- + +## Tone and Style: +* **Tone:** Maintain a professional tone. Be direct, concise, and avoid excessive language such as *"thank you for this consult..."* +* **Clarity:** Use clear and unambiguous language. Avoid overly academic jargon. The goal is to be easily understood by a busy generalist. +* **Conciseness:** Be brief and to the point. The total number of words in the entire response (excluding citations) should be less than 150 words. + +--- + +## Input Context: +The PCP's clinical question and recent clinical notes follow: +{{question}} +{{notes}} \ No newline at end of file diff --git a/sample_requests.http b/sample_requests.http index 602ede2..52d14e5 100644 --- a/sample_requests.http +++ b/sample_requests.http @@ -1,20 +1,20 @@ ### Gemini followup questions GET {{assist-pc-backend-url}}/followup-questions -### Pathway chatbot question -POST {{assist-pc-backend-url}}/ask-pathway +### Specialist chatbot question +POST {{assist-pc-backend-url}}/ask-specialist Content-Type: application/json { "question": "What further procedures would you recommend?" } -### Pathway chatbot question streamed -POST {{assist-pc-backend-url}}/ask-pathway-streamed +### Specialist chatbot question streamed +POST {{assist-pc-backend-url}}/ask-specialist-streamed Content-Type: application/json { - "question": "Understood. Any other diagnoses?" + "question": "Do any other diagnoses fit the description?" } ### Clinical question with matched template streamed diff --git a/src/app.controller.spec.ts b/src/app.controller.spec.ts index ed0e920..8a461c4 100644 --- a/src/app.controller.spec.ts +++ b/src/app.controller.spec.ts @@ -4,7 +4,7 @@ import { AppService } from './app.service'; import { LLMResponse } from './models/llmResponse'; import { TemplateSelectorService } from './template-selector/template-selector.service'; import { HttpModule } from '@nestjs/axios'; -import { PathwayService } from './pathway/pathway.service'; +import { SpecialistAiService } from './specialist-ai/specialist-ai.service'; describe('AppController', () => { let appController: AppController; @@ -12,7 +12,7 @@ describe('AppController', () => { beforeEach(async () => { const app: TestingModule = await Test.createTestingModule({ controllers: [AppController], - providers: [AppService, TemplateSelectorService, PathwayService], + providers: [AppService, TemplateSelectorService, SpecialistAiService], imports: [HttpModule], }).compile(); diff --git a/src/app.controller.ts b/src/app.controller.ts index 9cd2f3c..32dc286 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -41,8 +41,8 @@ export class AppController { const response = await this.appService.postReferralQuestion(request); session[SessionKeys.REFERRAL_RESPONSE] = response; - // reset Pathway conversation history on new referral request - session[SessionKeys.PREVIOUS_PATHWAY_CONVERSATIONS] = []; + // reset Specialist AI conversation history on new referral request + session[SessionKeys.PREVIOUS_SPECIALIST_CONVERSATIONS] = []; return response; } @@ -64,7 +64,7 @@ export class AppController { return await this.appService.postReferralQuestionStreamed(request, session); } - @Post('/ask-pathway') + @Post('/ask-specialist') @ApiBody({ schema: { properties: { @@ -74,22 +74,22 @@ export class AppController { }) @ApiCreatedResponse({ description: - 'Successfully received Pathway AI response to a clarifying question.', + 'Successfully received Specialist AI response to a clarifying question.', type: SpecialistAIResponse, }) - async postPathwayQuestion( + async postSpecialistQuestion( @Session() session: Record, @Body('question') question: string, ): Promise { this.logger.debug('controller request', question); this.logger.debug('session', session); const response: SpecialistAIResponse = - await this.appService.postPathwayQuestion(question, session); - if (session[SessionKeys.PREVIOUS_PATHWAY_CONVERSATIONS] == null) { - session[SessionKeys.PREVIOUS_PATHWAY_CONVERSATIONS] = []; + await this.appService.postSpecialistQuestion(question, session); + if (session[SessionKeys.PREVIOUS_SPECIALIST_CONVERSATIONS] == null) { + session[SessionKeys.PREVIOUS_SPECIALIST_CONVERSATIONS] = []; } ( - session[SessionKeys.PREVIOUS_PATHWAY_CONVERSATIONS] as Record< + session[SessionKeys.PREVIOUS_SPECIALIST_CONVERSATIONS] as Record< string, SpecialistAIResponse >[] @@ -99,7 +99,7 @@ export class AppController { return response; } - @Post('/ask-pathway-streamed') + @Post('/ask-specialist-streamed') @Sse() @ApiBody({ schema: { @@ -110,19 +110,19 @@ export class AppController { }) @ApiOkResponse({ description: - 'Successfully received streamed Pathway AI response to a clarifying question.', + 'Successfully received streamed Specialist AI response to a clarifying question.', type: SpecialistAIResponse, }) - postPathwayQuestionStreamed( + postSpecialistQuestionStreamed( @Session() session: Record, @Body('question') question: string, ): Observable<{ data: SpecialistAIResponse }> { this.logger.debug('controller request', question); this.logger.debug('session', session); - if (session[SessionKeys.PREVIOUS_PATHWAY_CONVERSATIONS] == null) { - session[SessionKeys.PREVIOUS_PATHWAY_CONVERSATIONS] = []; + if (session[SessionKeys.PREVIOUS_SPECIALIST_CONVERSATIONS] == null) { + session[SessionKeys.PREVIOUS_SPECIALIST_CONVERSATIONS] = []; } - return this.appService.postPathwayQuestionStreamed(question, session); + return this.appService.postSpecialistQuestionStreamed(question, session); } @Get('/followup-questions') @@ -133,7 +133,7 @@ export class AppController { }) generateFollowupQuestions( @Session() session: Record, - ): Promise { + ): Promise<{ questions: string[] }> { this.logger.debug('controller request for followup questions generation'); this.logger.debug('session', session); return this.appService.generateFollowupQuestions(session); diff --git a/src/app.module.ts b/src/app.module.ts index df82c39..72f4b77 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -3,10 +3,16 @@ import { AppService } from './app.service'; import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { TemplateSelectorModule } from './template-selector/template-selector.module'; -import { PathwayModule } from './pathway/pathway.module'; +import { SpecialistAiModule } from './specialist-ai/specialist-ai.module'; +import { LlmSelectorModule } from './llm-selector/llm-selector.module'; @Module({ - imports: [ConfigModule.forRoot(), TemplateSelectorModule, PathwayModule], + imports: [ + ConfigModule.forRoot(), + TemplateSelectorModule, + SpecialistAiModule, + LlmSelectorModule, + ], controllers: [AppController], providers: [AppService], }) diff --git a/src/app.service.ts b/src/app.service.ts index 93b08eb..506aae5 100644 --- a/src/app.service.ts +++ b/src/app.service.ts @@ -4,19 +4,16 @@ import { FilePart, generateObject, ImagePart, - LanguageModelV1, streamObject, TextPart, UserContent, } from 'ai'; -import { anthropic } from '@ai-sdk/anthropic'; -import { google } from '@ai-sdk/google'; import { ReferralRequest } from './models/referralRequest'; import * as fs from 'node:fs'; import { join } from 'path'; import { ReferralResponse } from './models/referralResponse'; import { TemplateSelectorService } from './template-selector/template-selector.service'; -import { PathwayService } from './pathway/pathway.service'; +import { SpecialistAiService } from './specialist-ai/specialist-ai.service'; import { SpecialistAIResponse } from './models/specialistAIResponse'; import { map, Observable, tap } from 'rxjs'; import { fromReadableStreamLike } from 'rxjs/internal/observable/innerFrom'; @@ -25,12 +22,8 @@ import { z } from 'zod'; import { ChatRequest } from './models/chatRequest'; import { SessionKeys } from './const'; import { plainToInstance } from 'class-transformer'; - -enum AIProvider { - Claude = 'CLAUDE', - Gemini = 'GEMINI', - // TODO add further models -} +import { OpenAIResponsesProviderOptions } from '@ai-sdk/openai'; +import { LlmSelectorService } from './llm-selector/llm-selector.service'; const systemPromptFilePath: string = './resources/prompt.txt'; const systemPromptWithoutTemplatesFilePath: string = @@ -43,8 +36,9 @@ export class AppService { private readonly logger = new Logger(AppService.name); constructor( + private readonly llmSelectorService: LlmSelectorService, private readonly templateSelectorService: TemplateSelectorService, - private readonly pathwayService: PathwayService, + private readonly specialistAiService: SpecialistAiService, ) {} getRoot(): string { @@ -70,12 +64,10 @@ export class AppService { llmResponse = plainToInstance(ReferralResponse, llmResponse); llmResponse.populatedTemplate = llmResponse.postProcessedPopulatedTemplate(); - const pathwayResponse: SpecialistAIResponse = await this.queryPathway( - request, - llmResponse, - ); + const specialistAIResponse: SpecialistAIResponse = + await this.querySpecialistAi(request, llmResponse); const response: ReferralResponse = llmResponse as ReferralResponse; - response.specialistAIResponse = pathwayResponse; + response.specialistAIResponse = specialistAIResponse; this.logger.debug(JSON.stringify(response, null, 2)); @@ -110,8 +102,8 @@ export class AppService { accumulatedResponse = next; session[SessionKeys.REFERRAL_RESPONSE] = accumulatedResponse; - // reset Pathway conversation history on new referral request - session[SessionKeys.PREVIOUS_PATHWAY_CONVERSATIONS] = []; + // reset Specialist AI conversation history on new referral request + session[SessionKeys.PREVIOUS_SPECIALIST_CONVERSATIONS] = []; this.logger.debug('LLM partial response: ', JSON.stringify(next)); subscriber.next({ data: next }); @@ -121,7 +113,7 @@ export class AppService { subscriber.error(reason); }, complete: () => { - this.queryPathwayStreamed(request, accumulatedResponse) + this.querySpecialistAiStreamed(request, accumulatedResponse) .pipe( map((specialistAIResponse: SpecialistAIResponse) => { accumulatedResponse.specialistAIResponse = specialistAIResponse; @@ -144,22 +136,22 @@ export class AppService { }); } - async postPathwayQuestion( + async postSpecialistQuestion( request: string, session: Record, ): Promise { const chatRequest: ChatRequest = this.prepareChatRequest(session, request); - return this.pathwayService.retrieveChatAnswer(chatRequest); + return this.specialistAiService.retrieveChatAnswer(chatRequest); } - postPathwayQuestionStreamed( + postSpecialistQuestionStreamed( request: string, session: Record, ): Observable<{ data: SpecialistAIResponse }> { const chatRequest: ChatRequest = this.prepareChatRequest(session, request); let accumulatedResponse: SpecialistAIResponse; return new Observable((subscriber) => { - this.pathwayService + this.specialistAiService .retrieveChatAnswerStreamed(chatRequest) .pipe( tap((next) => { @@ -175,7 +167,7 @@ export class AppService { }, complete: () => { ( - session[SessionKeys.PREVIOUS_PATHWAY_CONVERSATIONS] as Record< + session[SessionKeys.PREVIOUS_SPECIALIST_CONVERSATIONS] as Record< string, SpecialistAIResponse >[] @@ -185,15 +177,21 @@ export class AppService { subscriber.complete(); }, error: (reason) => { - this.logger.error('error querying Pathway: ', reason, chatRequest); + this.logger.error( + 'error querying Specialist AI: ', + reason, + chatRequest, + ); subscriber.error(reason); }, }); }); } - generateFollowupQuestions(session: Record): Promise { - const systemPrompt = fs + generateFollowupQuestions( + session: Record, + ): Promise<{ questions: string[] }> { + const systemPrompt: string = fs .readFileSync(join(process.cwd(), systemPromptFollowupQuestionsFilePath)) .toString(); @@ -215,29 +213,34 @@ export class AppService { ); } - let lastPathwayResponse = ( + let lastSpecialistResponse = ( session[SessionKeys.REFERRAL_RESPONSE] as ReferralResponse ).specialistAIResponse?.summaryResponse; - if (session[SessionKeys.PREVIOUS_PATHWAY_CONVERSATIONS] != null) { - const lastPathwayConversation = ( - session[SessionKeys.PREVIOUS_PATHWAY_CONVERSATIONS] as Record< + if (session[SessionKeys.PREVIOUS_SPECIALIST_CONVERSATIONS] != null) { + const lastSpecialistConversation = ( + session[SessionKeys.PREVIOUS_SPECIALIST_CONVERSATIONS] as Record< string, SpecialistAIResponse >[] ).at(-1); - for (const question in lastPathwayConversation) { - lastPathwayResponse = lastPathwayConversation[question].summaryResponse; + for (const question in lastSpecialistConversation) { + lastSpecialistResponse = + lastSpecialistConversation[question].summaryResponse; } } const request = [ 'Clinical question: ' + (session[SessionKeys.REFERRAL_REQUEST] as ReferralRequest).question, - 'LLM-generated specialist response: ' + lastPathwayResponse, + 'LLM-generated specialist response: ' + lastSpecialistResponse, ]; - const responseSchema = z.string().array(); + const responseSchema = z.object({ questions: z.string().array() }); - return this.queryLLM(systemPrompt, request, responseSchema); + return this.queryLLM<{ questions: string[] }>( + systemPrompt, + request, + responseSchema, + ); } private selectSystemPrompt(bestTemplate: string) { @@ -290,7 +293,7 @@ export class AppService { responseSchema: z.Schema, ) { const input = { - model: this.selectModel(), + model: this.llmSelectorService.selectModel(), schema: responseSchema, system: systemPrompt, messages: [ @@ -299,6 +302,11 @@ export class AppService { content: [] as UserContent, } as CoreMessage, ], + providerOptions: { + openai: { + strictSchemas: true, + } satisfies OpenAIResponsesProviderOptions, + }, }; for (const message of messages) { @@ -314,48 +322,37 @@ export class AppService { return input; } - private async queryPathway( + private async querySpecialistAi( request: ReferralRequest, response: LLMResponse, ): Promise { - return await this.pathwayService.retrieveAnswer( + return await this.specialistAiService.retrieveAnswer( request.question, request.clinicalNotes, response.populatedTemplate, ); } - private queryPathwayStreamed( + private querySpecialistAiStreamed( request: ReferralRequest, response: LLMResponse, ): Observable { - return this.pathwayService.retrieveAnswerStreamed( + return this.specialistAiService.retrieveAnswerStreamed( request.question, request.clinicalNotes, response.populatedTemplate, ); } - private selectModel(): LanguageModelV1 { - switch (String(process.env.AI_PROVIDER).toUpperCase() as AIProvider) { - case AIProvider.Claude: - return anthropic('claude-3-opus-20240229'); - case AIProvider.Gemini: - return google('models/gemini-2.0-flash'); - default: - throw new Error('unknown AI_PROVIDER type selected'); - } - } - private prepareChatRequest(session: Record, request: string) { const originalReferralRequest = session[ - 'referralRequest' + SessionKeys.REFERRAL_REQUEST ] as ReferralRequest; const originalReferralResponse = session[ - 'referralResponse' + SessionKeys.REFERRAL_RESPONSE ] as ReferralResponse; let previousConversations: Record[] = session[ - 'previousPathwayConversations' + SessionKeys.PREVIOUS_SPECIALIST_CONVERSATIONS ] as Record[]; if (previousConversations == null) { previousConversations = []; diff --git a/src/const.ts b/src/const.ts index 5b9dc64..38852c7 100644 --- a/src/const.ts +++ b/src/const.ts @@ -1,5 +1,5 @@ export const SessionKeys = { REFERRAL_REQUEST: 'referralRequest', REFERRAL_RESPONSE: 'referralResponse', - PREVIOUS_PATHWAY_CONVERSATIONS: 'previousPathwayConversations', + PREVIOUS_SPECIALIST_CONVERSATIONS: 'previousSpecialistAiConversations', } as const; diff --git a/src/llm-selector/llm-selector.module.ts b/src/llm-selector/llm-selector.module.ts new file mode 100644 index 0000000..d1f6e8a --- /dev/null +++ b/src/llm-selector/llm-selector.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { LlmSelectorService } from './llm-selector.service'; + +@Module({ + providers: [LlmSelectorService], + exports: [LlmSelectorService], +}) +export class LlmSelectorModule {} diff --git a/src/llm-selector/llm-selector.service.spec.ts b/src/llm-selector/llm-selector.service.spec.ts new file mode 100644 index 0000000..95c3876 --- /dev/null +++ b/src/llm-selector/llm-selector.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { LlmSelectorService } from './llm-selector.service'; + +describe('LlmSelectorService', () => { + let service: LlmSelectorService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [LlmSelectorService], + }).compile(); + + service = module.get(LlmSelectorService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/llm-selector/llm-selector.service.ts b/src/llm-selector/llm-selector.service.ts new file mode 100644 index 0000000..bf919a7 --- /dev/null +++ b/src/llm-selector/llm-selector.service.ts @@ -0,0 +1,52 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { LanguageModelV1 } from 'ai'; +import { anthropic } from '@ai-sdk/anthropic'; +import { google } from '@ai-sdk/google'; +import { createAzure } from '@ai-sdk/azure'; + +enum AIProvider { + // NOTE: string value should be uppercase as we're comparing strings after uppercasing + Claude = 'CLAUDE', + Gemini = 'GEMINI', + SecureGPT_OpenAI = 'SECUREGPT_OPENAI', + // TODO add further models +} + +@Injectable() +export class LlmSelectorService { + private readonly logger: Logger = new Logger(LlmSelectorService.name); + private model: LanguageModelV1; + + selectModel(): LanguageModelV1 { + this.logger.log('selected AI provider: ', process.env.AI_PROVIDER); + if (this.model) { + return this.model; + } + + // this block is run only once at first request + switch (String(process.env.AI_PROVIDER).toUpperCase() as AIProvider) { + case AIProvider.Claude: + this.model = anthropic('claude-3-opus-20240229'); + break; + case AIProvider.Gemini: + this.model = google('models/gemini-2.0-flash'); + break; + case AIProvider.SecureGPT_OpenAI: + this.model = createAzure({ + baseURL: 'https://apim.stanfordhealthcare.org/openai-eastus2', + apiVersion: '2025-01-01-preview', + apiKey: process.env.SECUREGPT_API_KEY, + headers: { + 'Ocp-Apim-Subscription-Key': String(process.env.SECUREGPT_API_KEY), + }, + })('deployments/gpt-4.1', { + structuredOutputs: true, + }); + break; + default: + throw new Error('unknown AI_PROVIDER type selected'); + } + + return this.model; + } +} diff --git a/src/pathway/pathway.module.ts b/src/pathway/pathway.module.ts deleted file mode 100644 index 1a01922..0000000 --- a/src/pathway/pathway.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; -import { HttpModule } from '@nestjs/axios'; -import { PathwayService } from './pathway.service'; - -@Module({ - imports: [HttpModule], - providers: [PathwayService], - exports: [PathwayService], -}) -export class PathwayModule {} diff --git a/src/pathway/pathway.service.ts b/src/pathway/pathway.service.ts deleted file mode 100644 index 0d9fcd1..0000000 --- a/src/pathway/pathway.service.ts +++ /dev/null @@ -1,283 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { HttpService } from '@nestjs/axios'; -import { catchError, lastValueFrom, map, Observable, throwError } from 'rxjs'; -import { plainToInstance } from 'class-transformer'; -import { AxiosError } from 'axios'; -import { AnswersRequest } from './models/answersRequest'; -import { Message } from './models/message'; -import { AnswersResponse } from './models/answersResponse'; -import { Citation, SpecialistAIResponse } from '../models/specialistAIResponse'; -import EventSourceStream from '@server-sent-stream/web'; -import { ChatRequest } from '../models/chatRequest'; -import * as fs from 'node:fs'; -import { join } from 'path'; - -const promptFilePath: string = './resources/pathway_prompt.md'; - -@Injectable() -export class PathwayService { - private readonly logger = new Logger(PathwayService.name); - - constructor(private readonly httpService: HttpService) {} - - async retrieveAnswer( - clinicalQuestion: string, - clinicalNotes: string, - filledTemplate: object[], - ): Promise { - const request = this.prepareAnswersRequest( - clinicalQuestion, - clinicalNotes, - filledTemplate, - ); - - const data = await this.doSyncRequest(request); - return this.specialistResponseFrom(data); - } - - retrieveAnswerStreamed( - clinicalQuestion: string, - clinicalNotes: string, - filledTemplate: object[], - ): Observable { - const request = this.prepareAnswersRequest( - clinicalQuestion, - clinicalNotes, - filledTemplate, - true, - ); - return this.doAsyncRequest(request); - } - - // TODO try to merge with retrieveAnswer - async retrieveChatAnswer( - chatRequest: ChatRequest, - ): Promise { - // TODO should previousMessages contain information about whether the message was from 'user' or 'assistant'? - // Will Pathway accept messages from 'assistant' as context? - const request = this.toAnswersRequest(chatRequest); - const data = await this.doSyncRequest(request); - - return this.specialistResponseFrom(data); - } - - // TODO try to merge with retrieveAnswerStreamed - retrieveChatAnswerStreamed( - chatRequest: ChatRequest, - ): Observable { - // TODO should previousMessages contain information about whether the message was from 'user' or 'assistant'? - // Will Pathway accept messages from 'assistant' as context? - const request = this.toAnswersRequest( - chatRequest, - true, // shouldStream - ); - return this.doAsyncRequest(request); - } - - private doAsyncRequest( - request: AnswersRequest, - ): Observable { - return new Observable((subscriber) => { - const accumulatedResponse: SpecialistAIResponse = - new SpecialistAIResponse(); - accumulatedResponse.summaryResponse = ''; - accumulatedResponse.citations = []; - - this.httpService.axiosRef - .post(String(process.env.PATHWAY_API), request, { - headers: { - Authorization: 'Bearer ' + process.env.PATHWAY_AI_API_KEY, - Accept: 'text/event-stream', - }, - responseType: 'stream', - adapter: 'fetch', - }) - .then(async (response) => { - const stream = response.data; // <- should be a ReadableStream - - const decoder = new EventSourceStream(); - stream.pipeThrough(decoder); - - // Read from the EventSourceStream - const reader = decoder.readable.getReader(); - - while (true) { - const { value, done } = await reader.read(); - if (done) { - break; - } - - this.logger.debug('Pathway MessageEvent: ', value.data); - if (value.data == null) { - // TODO is this an error? - break; - } - const parsedResponse = plainToInstance( - AnswersResponse, - JSON.parse(value.data), - ); - - let receivedNewData = false; - let specialistResponseStr: string = ''; - for (const choice of parsedResponse.choices) { - if (choice.delta?.content) { - specialistResponseStr += choice.delta.content; - receivedNewData = true; - } - } - if (receivedNewData) { - accumulatedResponse.summaryResponse += specialistResponseStr; - } - if ( - parsedResponse.citationsDetailed && - parsedResponse.citationsDetailed.length > - accumulatedResponse.citations.length - ) { - receivedNewData = true; - for ( - let i = accumulatedResponse.citations.length; - i < parsedResponse.citationsDetailed.length; - i++ - ) { - accumulatedResponse.citations.push( - new Citation( - parsedResponse.citationsDetailed[i].name, - parsedResponse.citations[i], - ), - ); - } - } - - if (receivedNewData) { - subscriber.next(accumulatedResponse); - } - } - subscriber.complete(); - }) - .catch((error) => { - this.logger.error('error querying Pathway API: ', error); - subscriber.error(error); - }); - }); - } - - private toAnswersRequest( - chatRequest: ChatRequest, - shouldStream: boolean = false, - ): AnswersRequest { - const request: AnswersRequest = new AnswersRequest([], shouldStream); - - let message = - 'Given clinical notes and your previous responses to questions, answer the following question:\n' + - chatRequest.question + - '\n' + - 'INPUT:\n' + - 'Clinical notes: ' + - chatRequest.originalReferralRequestNotes + - '.\n' + - 'Your previous responses to questions: ' + - 'Question 1: ' + - chatRequest.originalReferralRequestQuestion + - '.\n' + - 'Your response 1: ' + - chatRequest.originalPathwayResponse + - '.\n'; - - let i = 1; - for (const previousConversation of chatRequest.previousConversations) { - for (const question in previousConversation) { - message += - 'Question ' + - i + - ': ' + - question + - '.\n' + - 'Your response ' + - i + - ': ' + - previousConversation[question].summaryResponse + - '.\n'; - i++; - } - } - - request.messages.push(new Message('user', message)); - - this.logger.debug('Pathway API request: ', request); - return request; - } - - private async doSyncRequest(request: AnswersRequest) { - const { data } = await lastValueFrom( - this.httpService - .post( - String(process.env.PATHWAY_API), - request, - { - headers: { - Authorization: 'Bearer ' + process.env.PATHWAY_AI_API_KEY, - }, - }, - ) - .pipe( - // looks like we need to manually map the data to the type we want via class-transform - map((response) => { - response.data = plainToInstance(AnswersResponse, response.data); - return response; - }), - ) - .pipe( - catchError((error: AxiosError) => { - // in case of an error just return the empty response - this.logger.error( - 'error querying Pathway API', - error.response?.data, - request.messages, - ); - return throwError( - () => new Error('Error querying Pathway API: ' + error.message), - ); - }), - ), - ); - this.logger.debug('data', data); - return data; - } - - private specialistResponseFrom(data: AnswersResponse): SpecialistAIResponse { - const specialistResponse: SpecialistAIResponse = new SpecialistAIResponse(); - let specialistResponseStr: string = ''; - for (const choice of data.choices) { - specialistResponseStr += choice.message.content; - } - specialistResponse.summaryResponse = specialistResponseStr; - specialistResponse.citations = []; - for (let i = 0; i < data.citationsDetailed.length; i++) { - specialistResponse.citations.push( - new Citation(data.citationsDetailed[i].name, data.citations[i]), - ); - } - return specialistResponse; - } - - private prepareAnswersRequest( - clinicalQuestion: string, - clinicalNotes: string, - filledTemplate: object[], - shouldStream: boolean = false, - ): AnswersRequest { - const request: AnswersRequest = new AnswersRequest([], shouldStream); - - const promptTemplate: string = fs - .readFileSync(join(process.cwd(), promptFilePath)) - .toString() - .replace('{{question}}', clinicalQuestion) - // TODO originally, this should be populated template, but since we're still not generating correct populated template, use clinical notes for now - .replace('{{notes}}', clinicalNotes); - - request.messages.push(new Message('user', promptTemplate)); - - this.logger.debug('Pathway API request: ', request); - return request; - } -} diff --git a/src/pathway/models/answersRequest.ts b/src/specialist-ai/models/answersRequest.ts similarity index 100% rename from src/pathway/models/answersRequest.ts rename to src/specialist-ai/models/answersRequest.ts diff --git a/src/pathway/models/answersResponse.ts b/src/specialist-ai/models/answersResponse.ts similarity index 100% rename from src/pathway/models/answersResponse.ts rename to src/specialist-ai/models/answersResponse.ts diff --git a/src/pathway/models/message.ts b/src/specialist-ai/models/message.ts similarity index 100% rename from src/pathway/models/message.ts rename to src/specialist-ai/models/message.ts diff --git a/src/specialist-ai/specialist-ai.module.ts b/src/specialist-ai/specialist-ai.module.ts new file mode 100644 index 0000000..dbfb1f6 --- /dev/null +++ b/src/specialist-ai/specialist-ai.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { SpecialistAiService } from './specialist-ai.service'; +import { LlmSelectorModule } from '../llm-selector/llm-selector.module'; + +@Module({ + imports: [LlmSelectorModule], + providers: [SpecialistAiService], + exports: [SpecialistAiService], +}) +export class SpecialistAiModule {} diff --git a/src/pathway/pathway.service.spec.ts b/src/specialist-ai/specialist-ai.service.spec.ts similarity index 58% rename from src/pathway/pathway.service.spec.ts rename to src/specialist-ai/specialist-ai.service.spec.ts index eb3a77d..f7d3ead 100644 --- a/src/pathway/pathway.service.spec.ts +++ b/src/specialist-ai/specialist-ai.service.spec.ts @@ -1,15 +1,15 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { PathwayService } from './pathway.service'; +import { SpecialistAiService } from './specialist-ai.service'; describe('PathwayService', () => { - let service: PathwayService; + let service: SpecialistAiService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [PathwayService], + providers: [SpecialistAiService], }).compile(); - service = module.get(PathwayService); + service = module.get(SpecialistAiService); }); it('should be defined', () => { diff --git a/src/specialist-ai/specialist-ai.service.ts b/src/specialist-ai/specialist-ai.service.ts new file mode 100644 index 0000000..6ee72a4 --- /dev/null +++ b/src/specialist-ai/specialist-ai.service.ts @@ -0,0 +1,193 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { distinct, map, Observable, scan } from 'rxjs'; +import { SpecialistAIResponse } from '../models/specialistAIResponse'; +import { ChatRequest } from '../models/chatRequest'; +import * as fs from 'node:fs'; +import { join } from 'path'; +import { CoreMessage, generateText, streamText } from 'ai'; +import { fromReadableStreamLike } from 'rxjs/internal/observable/innerFrom'; +import { LlmSelectorService } from '../llm-selector/llm-selector.service'; + +const promptFilePath: string = './resources/specialist_prompt.md'; + +@Injectable() +export class SpecialistAiService { + private readonly logger: Logger = new Logger(SpecialistAiService.name); + + constructor(private readonly llmSelectorService: LlmSelectorService) {} + + async retrieveAnswer( + clinicalQuestion: string, + clinicalNotes: string, + filledTemplate: object[], + ): Promise { + const request = this.prepareInput(clinicalQuestion, clinicalNotes); + + const { text } = await generateText(request); + this.logger.debug('Specialist AI response:', text); + + return this.specialistResponseFrom(text); + } + + retrieveAnswerStreamed( + clinicalQuestion: string, + clinicalNotes: string, + filledTemplate: object[], + ): Observable { + const request = this.prepareInput(clinicalQuestion, clinicalNotes); + + // @ts-expect-error("it is complaining that error has any type") + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (request as any).onError = ({ error }) => { + this.logger.error( + 'error during streaming Specialist AI response:', + error, + ); + }; + + const { textStream } = streamText(request); + + return fromReadableStreamLike(textStream as ReadableStream) + .pipe( + scan( + (accumulatedResponse: string, currentResponse: string): string => + accumulatedResponse + currentResponse, + '', + ), + ) + .pipe(distinct()) + .pipe( + map((accumulatedResponse: string): SpecialistAIResponse => { + return this.specialistResponseFrom(accumulatedResponse); + }), + ); + } + + // TODO try to merge with retrieveAnswer + async retrieveChatAnswer( + chatRequest: ChatRequest, + ): Promise { + // TODO should previousMessages contain information about whether the message was from 'user' or 'assistant'? + // Will Pathway accept messages from 'assistant' as context? + const request = this.prepareChatInput(chatRequest); + + const { text } = await generateText(request); + this.logger.debug('Specialist AI response:', text); + + return this.specialistResponseFrom(text); + } + + // TODO try to merge with retrieveAnswerStreamed + retrieveChatAnswerStreamed( + chatRequest: ChatRequest, + ): Observable { + // TODO should previousMessages contain information about whether the message was from 'user' or 'assistant'? + // Will Pathway accept messages from 'assistant' as context? + const request = this.prepareChatInput(chatRequest); + + // @ts-expect-error("it is complaining that error has any type") + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (request as any).onError = ({ error }) => { + this.logger.error( + 'error during streaming Specialist AI chat response:', + error, + ); + }; + + const { textStream } = streamText(request); + + return fromReadableStreamLike(textStream as ReadableStream) + .pipe( + scan( + (accumulatedResponse: string, currentResponse: string): string => + accumulatedResponse + currentResponse, + '', + ), + ) + .pipe(distinct()) + .pipe( + map((accumulatedResponse: string): SpecialistAIResponse => { + return this.specialistResponseFrom(accumulatedResponse); + }), + ); + } + + private prepareChatInput(chatRequest: ChatRequest) { + let message = + 'Given clinical notes and your previous responses to questions, answer the following question:\n' + + chatRequest.question + + '\n' + + 'INPUT:\n' + + 'Clinical notes: ' + + chatRequest.originalReferralRequestNotes + + '.\n' + + 'Your previous responses to questions: ' + + 'Question 1: ' + + chatRequest.originalReferralRequestQuestion + + '.\n' + + 'Your response 1: ' + + chatRequest.originalPathwayResponse + + '.\n'; + + let i = 1; + for (const previousConversation of chatRequest.previousConversations) { + for (const question in previousConversation) { + message += + 'Question ' + + i + + ': ' + + question + + '.\n' + + 'Your response ' + + i + + ': ' + + previousConversation[question].summaryResponse + + '.\n'; + i++; + } + } + + const input = { + model: this.llmSelectorService.selectModel(), + messages: [ + { + role: 'user', + // TODO add as a user/assistant messages array + content: message, + } as CoreMessage, + ], + }; + + this.logger.debug('Specialist AI chat request: ', input); + return input; + } + + private specialistResponseFrom(data: string): SpecialistAIResponse { + const specialistResponse: SpecialistAIResponse = new SpecialistAIResponse(); + specialistResponse.summaryResponse = data; + specialistResponse.citations = []; + return specialistResponse; + } + + private prepareInput(clinicalQuestion: string, clinicalNotes: string) { + const promptTemplate: string = fs + .readFileSync(join(process.cwd(), promptFilePath)) + .toString() + .replace('{{question}}', clinicalQuestion) + // TODO originally, this should be populated template, but since we're still not generating correct populated template, use clinical notes for now + .replace('{{notes}}', clinicalNotes); + + const input = { + model: this.llmSelectorService.selectModel(), + messages: [ + { + role: 'user', + content: promptTemplate, + } as CoreMessage, + ], + }; + + this.logger.debug('Specialist AI request: ', input); + return input; + } +}