mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-04-11 18:08:16 +00:00
feat: migrate AI to Vercel AI SDK, add OpenAI provider, fix worklist
- Replace raw @anthropic-ai/sdk with Vercel AI SDK (generateText, tool, generateObject) - Add provider abstraction (ai-provider.ts) — swap OpenAI/Anthropic via env var - AI chat controller: dynamic KB from platform (clinics, packages, insurance), zero hardcoding - AI enrichment service: use generateObject with Zod schema instead of manual JSON parsing - Worklist: resolve agent name from platform currentUser API instead of JWT decode - Worklist: fix GraphQL field names to match platform remapping (source, status, direction, etc.) - Config: add AI_PROVIDER, AI_MODEL, OPENAI_API_KEY env vars Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
723
package-lock.json
generated
723
package-lock.json
generated
@@ -9,13 +9,15 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.79.0",
|
"@ai-sdk/anthropic": "^3.0.58",
|
||||||
|
"@ai-sdk/openai": "^3.0.41",
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
"@nestjs/config": "^4.0.3",
|
"@nestjs/config": "^4.0.3",
|
||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
"@nestjs/platform-socket.io": "^11.1.17",
|
"@nestjs/platform-socket.io": "^11.1.17",
|
||||||
"@nestjs/websockets": "^11.1.17",
|
"@nestjs/websockets": "^11.1.17",
|
||||||
|
"ai": "^6.0.116",
|
||||||
"axios": "^1.13.6",
|
"axios": "^1.13.6",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
@@ -43,10 +45,89 @@
|
|||||||
"ts-loader": "^9.5.2",
|
"ts-loader": "^9.5.2",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"typescript-eslint": "^8.20.0"
|
"typescript-eslint": "^8.20.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@ai-sdk/anthropic": {
|
||||||
|
"version": "3.0.58",
|
||||||
|
"resolved": "http://localhost:4873/@ai-sdk/anthropic/-/anthropic-3.0.58.tgz",
|
||||||
|
"integrity": "sha512-/53SACgmVukO4bkms4dpxpRlYhW8Ct6QZRe6sj1Pi5H00hYhxIrqfiLbZBGxkdRvjsBQeP/4TVGsXgH5rQeb8Q==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@ai-sdk/provider": "3.0.8",
|
||||||
|
"@ai-sdk/provider-utils": "4.0.19"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"zod": "^3.25.76 || ^4.1.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ai-sdk/gateway": {
|
||||||
|
"version": "3.0.66",
|
||||||
|
"resolved": "http://localhost:4873/@ai-sdk/gateway/-/gateway-3.0.66.tgz",
|
||||||
|
"integrity": "sha512-SIQ0YY0iMuv+07HLsZ+bB990zUJ6S4ujORAh+Jv1V2KGNn73qQKnGO0JBk+w+Res8YqOFSycwDoWcFlQrVxS4A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@ai-sdk/provider": "3.0.8",
|
||||||
|
"@ai-sdk/provider-utils": "4.0.19",
|
||||||
|
"@vercel/oidc": "3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"zod": "^3.25.76 || ^4.1.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ai-sdk/openai": {
|
||||||
|
"version": "3.0.41",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-3.0.41.tgz",
|
||||||
|
"integrity": "sha512-IZ42A+FO+vuEQCVNqlnAPYQnnUpUfdJIwn1BEDOBywiEHa23fw7PahxVtlX9zm3/zMvTW4JKPzWyvAgDu+SQ2A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@ai-sdk/provider": "3.0.8",
|
||||||
|
"@ai-sdk/provider-utils": "4.0.19"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"zod": "^3.25.76 || ^4.1.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ai-sdk/provider": {
|
||||||
|
"version": "3.0.8",
|
||||||
|
"resolved": "http://localhost:4873/@ai-sdk/provider/-/provider-3.0.8.tgz",
|
||||||
|
"integrity": "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"json-schema": "^0.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ai-sdk/provider-utils": {
|
||||||
|
"version": "4.0.19",
|
||||||
|
"resolved": "http://localhost:4873/@ai-sdk/provider-utils/-/provider-utils-4.0.19.tgz",
|
||||||
|
"integrity": "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@ai-sdk/provider": "3.0.8",
|
||||||
|
"@standard-schema/spec": "^1.1.0",
|
||||||
|
"eventsource-parser": "^3.0.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"zod": "^3.25.76 || ^4.1.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@angular-devkit/core": {
|
"node_modules/@angular-devkit/core": {
|
||||||
"version": "19.2.19",
|
"version": "19.2.19",
|
||||||
"resolved": "http://localhost:4873/@angular-devkit/core/-/core-19.2.19.tgz",
|
"resolved": "http://localhost:4873/@angular-devkit/core/-/core-19.2.19.tgz",
|
||||||
@@ -191,26 +272,6 @@
|
|||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@anthropic-ai/sdk": {
|
|
||||||
"version": "0.79.0",
|
|
||||||
"resolved": "http://localhost:4873/@anthropic-ai/sdk/-/sdk-0.79.0.tgz",
|
|
||||||
"integrity": "sha512-ietmtM6glcnnrWq26H+BZm8J07iay9Cob6hRzDTr/A9QWF1m2T//TQhFO4MTKcZht2/7LS8bG9wUYEhcizKRnA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"json-schema-to-ts": "^3.1.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"anthropic-ai-sdk": "bin/cli"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"zod": "^3.25.0 || ^4.0.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"zod": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.29.0",
|
"version": "7.29.0",
|
||||||
"resolved": "http://localhost:4873/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
"resolved": "http://localhost:4873/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
||||||
@@ -672,15 +733,6 @@
|
|||||||
"@babel/core": "^7.0.0-0"
|
"@babel/core": "^7.0.0-0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/runtime": {
|
|
||||||
"version": "7.29.2",
|
|
||||||
"resolved": "http://localhost:4873/@babel/runtime/-/runtime-7.29.2.tgz",
|
|
||||||
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.9.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.28.6",
|
"version": "7.28.6",
|
||||||
"resolved": "http://localhost:4873/@babel/template/-/template-7.28.6.tgz",
|
"resolved": "http://localhost:4873/@babel/template/-/template-7.28.6.tgz",
|
||||||
@@ -815,6 +867,448 @@
|
|||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "http://localhost:4873/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"aix"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "http://localhost:4873/@esbuild/android-arm/-/android-arm-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "http://localhost:4873/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "http://localhost:4873/@esbuild/android-x64/-/android-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "http://localhost:4873/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "http://localhost:4873/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "http://localhost:4873/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "http://localhost:4873/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "http://localhost:4873/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "http://localhost:4873/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "http://localhost:4873/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "http://localhost:4873/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "http://localhost:4873/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "http://localhost:4873/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "http://localhost:4873/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "http://localhost:4873/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "http://localhost:4873/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "http://localhost:4873/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "http://localhost:4873/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "http://localhost:4873/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "http://localhost:4873/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openharmony-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "http://localhost:4873/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openharmony"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "http://localhost:4873/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "http://localhost:4873/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "http://localhost:4873/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "http://localhost:4873/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@eslint-community/eslint-utils": {
|
"node_modules/@eslint-community/eslint-utils": {
|
||||||
"version": "4.9.1",
|
"version": "4.9.1",
|
||||||
"resolved": "http://localhost:4873/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
|
"resolved": "http://localhost:4873/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
|
||||||
@@ -2610,6 +3104,15 @@
|
|||||||
"npm": ">=5.10.0"
|
"npm": ">=5.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@opentelemetry/api": {
|
||||||
|
"version": "1.9.0",
|
||||||
|
"resolved": "http://localhost:4873/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||||
|
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@paralleldrive/cuid2": {
|
"node_modules/@paralleldrive/cuid2": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "http://localhost:4873/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz",
|
"resolved": "http://localhost:4873/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz",
|
||||||
@@ -2677,6 +3180,12 @@
|
|||||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@standard-schema/spec": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "http://localhost:4873/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@tokenizer/inflate": {
|
"node_modules/@tokenizer/inflate": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "http://localhost:4873/@tokenizer/inflate/-/inflate-0.4.1.tgz",
|
"resolved": "http://localhost:4873/@tokenizer/inflate/-/inflate-0.4.1.tgz",
|
||||||
@@ -3593,6 +4102,15 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@vercel/oidc": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "http://localhost:4873/@vercel/oidc/-/oidc-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@webassemblyjs/ast": {
|
"node_modules/@webassemblyjs/ast": {
|
||||||
"version": "1.14.1",
|
"version": "1.14.1",
|
||||||
"resolved": "http://localhost:4873/@webassemblyjs/ast/-/ast-1.14.1.tgz",
|
"resolved": "http://localhost:4873/@webassemblyjs/ast/-/ast-1.14.1.tgz",
|
||||||
@@ -3830,6 +4348,24 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ai": {
|
||||||
|
"version": "6.0.116",
|
||||||
|
"resolved": "http://localhost:4873/ai/-/ai-6.0.116.tgz",
|
||||||
|
"integrity": "sha512-7yM+cTmyRLeNIXwt4Vj+mrrJgVQ9RMIW5WO0ydoLoYkewIvsMcvUmqS4j2RJTUXaF1HphwmSKUMQ/HypNRGOmA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@ai-sdk/gateway": "3.0.66",
|
||||||
|
"@ai-sdk/provider": "3.0.8",
|
||||||
|
"@ai-sdk/provider-utils": "4.0.19",
|
||||||
|
"@opentelemetry/api": "1.9.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"zod": "^3.25.76 || ^4.1.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.14.0",
|
"version": "6.14.0",
|
||||||
"resolved": "http://localhost:4873/ajv/-/ajv-6.14.0.tgz",
|
"resolved": "http://localhost:4873/ajv/-/ajv-6.14.0.tgz",
|
||||||
@@ -5209,6 +5745,48 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/esbuild": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "http://localhost:4873/esbuild/-/esbuild-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@esbuild/aix-ppc64": "0.27.4",
|
||||||
|
"@esbuild/android-arm": "0.27.4",
|
||||||
|
"@esbuild/android-arm64": "0.27.4",
|
||||||
|
"@esbuild/android-x64": "0.27.4",
|
||||||
|
"@esbuild/darwin-arm64": "0.27.4",
|
||||||
|
"@esbuild/darwin-x64": "0.27.4",
|
||||||
|
"@esbuild/freebsd-arm64": "0.27.4",
|
||||||
|
"@esbuild/freebsd-x64": "0.27.4",
|
||||||
|
"@esbuild/linux-arm": "0.27.4",
|
||||||
|
"@esbuild/linux-arm64": "0.27.4",
|
||||||
|
"@esbuild/linux-ia32": "0.27.4",
|
||||||
|
"@esbuild/linux-loong64": "0.27.4",
|
||||||
|
"@esbuild/linux-mips64el": "0.27.4",
|
||||||
|
"@esbuild/linux-ppc64": "0.27.4",
|
||||||
|
"@esbuild/linux-riscv64": "0.27.4",
|
||||||
|
"@esbuild/linux-s390x": "0.27.4",
|
||||||
|
"@esbuild/linux-x64": "0.27.4",
|
||||||
|
"@esbuild/netbsd-arm64": "0.27.4",
|
||||||
|
"@esbuild/netbsd-x64": "0.27.4",
|
||||||
|
"@esbuild/openbsd-arm64": "0.27.4",
|
||||||
|
"@esbuild/openbsd-x64": "0.27.4",
|
||||||
|
"@esbuild/openharmony-arm64": "0.27.4",
|
||||||
|
"@esbuild/sunos-x64": "0.27.4",
|
||||||
|
"@esbuild/win32-arm64": "0.27.4",
|
||||||
|
"@esbuild/win32-ia32": "0.27.4",
|
||||||
|
"@esbuild/win32-x64": "0.27.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/escalade": {
|
"node_modules/escalade": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "http://localhost:4873/escalade/-/escalade-3.2.0.tgz",
|
"resolved": "http://localhost:4873/escalade/-/escalade-3.2.0.tgz",
|
||||||
@@ -5472,6 +6050,15 @@
|
|||||||
"node": ">=0.8.x"
|
"node": ">=0.8.x"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eventsource-parser": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "http://localhost:4873/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/execa": {
|
"node_modules/execa": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "http://localhost:4873/execa/-/execa-5.1.1.tgz",
|
"resolved": "http://localhost:4873/execa/-/execa-5.1.1.tgz",
|
||||||
@@ -6027,6 +6614,19 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-tsconfig": {
|
||||||
|
"version": "4.13.6",
|
||||||
|
"resolved": "http://localhost:4873/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
|
||||||
|
"integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"resolve-pkg-maps": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/glob": {
|
"node_modules/glob": {
|
||||||
"version": "13.0.0",
|
"version": "13.0.0",
|
||||||
"resolved": "http://localhost:4873/glob/-/glob-13.0.0.tgz",
|
"resolved": "http://localhost:4873/glob/-/glob-13.0.0.tgz",
|
||||||
@@ -7409,18 +8009,11 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/json-schema-to-ts": {
|
"node_modules/json-schema": {
|
||||||
"version": "3.1.1",
|
"version": "0.4.0",
|
||||||
"resolved": "http://localhost:4873/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
|
"resolved": "http://localhost:4873/json-schema/-/json-schema-0.4.0.tgz",
|
||||||
"integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
|
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
|
||||||
"license": "MIT",
|
"license": "(AFL-2.1 OR BSD-3-Clause)"
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.18.3",
|
|
||||||
"ts-algebra": "^2.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/json-schema-traverse": {
|
"node_modules/json-schema-traverse": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
@@ -8619,6 +9212,16 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/resolve-pkg-maps": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "http://localhost:4873/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/restore-cursor": {
|
"node_modules/restore-cursor": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "http://localhost:4873/restore-cursor/-/restore-cursor-3.1.0.tgz",
|
"resolved": "http://localhost:4873/restore-cursor/-/restore-cursor-3.1.0.tgz",
|
||||||
@@ -9553,12 +10156,6 @@
|
|||||||
"url": "https://github.com/sponsors/Borewit"
|
"url": "https://github.com/sponsors/Borewit"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ts-algebra": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "http://localhost:4873/ts-algebra/-/ts-algebra-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/ts-api-utils": {
|
"node_modules/ts-api-utils": {
|
||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
"resolved": "http://localhost:4873/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
|
"resolved": "http://localhost:4873/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
|
||||||
@@ -9750,6 +10347,26 @@
|
|||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
|
"node_modules/tsx": {
|
||||||
|
"version": "4.21.0",
|
||||||
|
"resolved": "http://localhost:4873/tsx/-/tsx-4.21.0.tgz",
|
||||||
|
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"esbuild": "~0.27.0",
|
||||||
|
"get-tsconfig": "^4.7.5"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"tsx": "dist/cli.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "http://localhost:4873/type-check/-/type-check-0.4.0.tgz",
|
"resolved": "http://localhost:4873/type-check/-/type-check-0.4.0.tgz",
|
||||||
@@ -10444,6 +11061,16 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zod": {
|
||||||
|
"version": "4.3.6",
|
||||||
|
"resolved": "http://localhost:4873/zod/-/zod-4.3.6.tgz",
|
||||||
|
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,13 +20,15 @@
|
|||||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.79.0",
|
"@ai-sdk/anthropic": "^3.0.58",
|
||||||
|
"@ai-sdk/openai": "^3.0.41",
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
"@nestjs/config": "^4.0.3",
|
"@nestjs/config": "^4.0.3",
|
||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
"@nestjs/platform-socket.io": "^11.1.17",
|
"@nestjs/platform-socket.io": "^11.1.17",
|
||||||
"@nestjs/websockets": "^11.1.17",
|
"@nestjs/websockets": "^11.1.17",
|
||||||
|
"ai": "^6.0.116",
|
||||||
"axios": "^1.13.6",
|
"axios": "^1.13.6",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
@@ -54,6 +56,7 @@
|
|||||||
"ts-loader": "^9.5.2",
|
"ts-loader": "^9.5.2",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"typescript-eslint": "^8.20.0"
|
"typescript-eslint": "^8.20.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,267 +1,401 @@
|
|||||||
import { Controller, Post, Body, Headers, HttpException, Logger } from '@nestjs/common';
|
import { Controller, Post, Body, Headers, HttpException, Logger } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import Anthropic from '@anthropic-ai/sdk';
|
import { generateText, tool, stepCountIs } from 'ai';
|
||||||
|
import type { LanguageModel } from 'ai';
|
||||||
type ChatContext = {
|
import { z } from 'zod';
|
||||||
callerPhone?: string;
|
import { PlatformGraphqlService } from '../platform/platform-graphql.service';
|
||||||
leadId?: string;
|
import { createAiModel, isAiConfigured } from './ai-provider';
|
||||||
leadName?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ChatRequest = {
|
type ChatRequest = {
|
||||||
message: string;
|
message: string;
|
||||||
context?: ChatContext;
|
context?: { callerPhone?: string; leadId?: string; leadName?: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
type ChatResponse = {
|
|
||||||
reply: string;
|
|
||||||
sources: string[];
|
|
||||||
confidence: 'high' | 'medium' | 'low';
|
|
||||||
};
|
|
||||||
|
|
||||||
const HOSPITAL_KNOWLEDGE = `
|
|
||||||
## Global Hospital — Clinic Locations
|
|
||||||
|
|
||||||
- **Koramangala**: #45, 80 Feet Road, Koramangala 4th Block, Bengaluru 560034. Open Mon–Sat 8 AM–8 PM, Sun 9 AM–2 PM.
|
|
||||||
- **Whitefield**: Prestige Shantiniketan, ITPL Main Road, Whitefield, Bengaluru 560066. Open Mon–Sat 8 AM–8 PM, Sun closed.
|
|
||||||
- **Indiranagar**: #12, 100 Feet Road, Indiranagar, Bengaluru 560038. Open Mon–Sat 9 AM–7 PM, Sun 10 AM–1 PM.
|
|
||||||
|
|
||||||
## Departments & Doctors
|
|
||||||
|
|
||||||
| Doctor | Department | Clinics | Visiting Hours | Consultation Fee (New / Follow-up) |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| Dr. Sharma | Cardiology | Koramangala, Whitefield | Mon/Wed/Fri 10 AM–1 PM | ₹800 / ₹500 |
|
|
||||||
| Dr. Patel | Gynecology | Indiranagar, Koramangala | Tue/Thu/Sat 9 AM–12 PM | ₹700 / ₹400 |
|
|
||||||
| Dr. Kumar | Orthopedics | Whitefield | Mon–Fri 2 PM–5 PM | ₹600 / ₹400 |
|
|
||||||
| Dr. Reddy | General Medicine | Koramangala, Indiranagar, Whitefield | Mon–Sat 9 AM–6 PM | ₹500 / ₹300 |
|
|
||||||
| Dr. Singh | ENT | Indiranagar | Mon/Wed/Fri 11 AM–3 PM | ₹600 / ₹400 |
|
|
||||||
|
|
||||||
## Treatment Packages
|
|
||||||
|
|
||||||
- **Master Health Checkup**: ₹2,999 — includes blood work, ECG, X-ray, doctor consultation. Available at all clinics.
|
|
||||||
- **Cardiac Screening**: ₹4,999 — includes ECG, echo, TMT, lipid profile, cardiologist consultation.
|
|
||||||
- **Women's Wellness**: ₹3,499 — includes pap smear, mammography, thyroid, bone density, gynecology consult.
|
|
||||||
- **Orthopedic Assessment**: ₹1,999 — includes X-ray, bone density, physiotherapy assessment, orthopedic consult.
|
|
||||||
|
|
||||||
## Appointment Booking
|
|
||||||
|
|
||||||
- Appointments can be booked via phone, website, or walk-in.
|
|
||||||
- Cancellation policy: free cancellation up to 4 hours before appointment.
|
|
||||||
- Patients should arrive 15 minutes early for paperwork.
|
|
||||||
- First-time patients must bring a valid government ID and any previous medical records.
|
|
||||||
|
|
||||||
## Insurance & Payments
|
|
||||||
|
|
||||||
- Accepted insurance: Star Health, ICICI Lombard, Bajaj Allianz, HDFC Ergo, Max Bupa, New India Assurance.
|
|
||||||
- Payment methods: cash, credit/debit cards, UPI, net banking.
|
|
||||||
- EMI options available for procedures above ₹10,000.
|
|
||||||
`;
|
|
||||||
|
|
||||||
const SYSTEM_PROMPT = `You are an AI assistant for call center agents at Global Hospital, Bengaluru. Your job is to help agents quickly answer questions about doctors, clinics, appointments, treatment packages, and patient context during live calls.
|
|
||||||
|
|
||||||
${HOSPITAL_KNOWLEDGE}
|
|
||||||
|
|
||||||
## Guidelines
|
|
||||||
|
|
||||||
- Be concise and direct — agents are on a live call and need quick answers.
|
|
||||||
- Format responses for easy scanning: use bullet points for lists, bold key info.
|
|
||||||
- If a caller/lead context is provided, incorporate it naturally (e.g., "Since Priya is interested in cardiology...").
|
|
||||||
- Always mention consultation fees when discussing doctor availability.
|
|
||||||
- If asked about something outside the hospital knowledge base, say "I don't have that information — please check with the supervisor."
|
|
||||||
- Do NOT give medical advice, diagnosis, or treatment recommendations.
|
|
||||||
- Do NOT share sensitive internal hospital data (revenue, staff salaries, internal policies).
|
|
||||||
- Do NOT speculate about patient conditions or test results.
|
|
||||||
- Keep responses under 150 words unless the agent asks for detailed information.`;
|
|
||||||
|
|
||||||
@Controller('api/ai')
|
@Controller('api/ai')
|
||||||
export class AiChatController {
|
export class AiChatController {
|
||||||
private readonly logger = new Logger(AiChatController.name);
|
private readonly logger = new Logger(AiChatController.name);
|
||||||
private client: Anthropic | null = null;
|
private readonly aiModel: LanguageModel | null;
|
||||||
|
private knowledgeBase: string | null = null;
|
||||||
|
private kbLoadedAt = 0;
|
||||||
|
private readonly kbTtlMs = 5 * 60 * 1000;
|
||||||
|
|
||||||
constructor(private config: ConfigService) {
|
constructor(
|
||||||
const apiKey = config.get<string>('ai.anthropicApiKey');
|
private config: ConfigService,
|
||||||
if (apiKey) {
|
private platform: PlatformGraphqlService,
|
||||||
this.client = new Anthropic({ apiKey });
|
) {
|
||||||
|
this.aiModel = createAiModel(config);
|
||||||
|
if (!this.aiModel) {
|
||||||
|
this.logger.warn('AI not configured — chat uses fallback');
|
||||||
} else {
|
} else {
|
||||||
this.logger.warn('ANTHROPIC_API_KEY not set — AI chat will use keyword fallback');
|
const provider = config.get<string>('ai.provider') ?? 'openai';
|
||||||
|
const model = config.get<string>('ai.model') ?? 'gpt-4o-mini';
|
||||||
|
this.logger.log(`AI configured: ${provider}/${model}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('chat')
|
@Post('chat')
|
||||||
async chat(
|
async chat(@Body() body: ChatRequest, @Headers('authorization') auth: string) {
|
||||||
@Body() body: ChatRequest,
|
if (!auth) throw new HttpException('Authorization required', 401);
|
||||||
@Headers('authorization') authHeader: string,
|
if (!body.message?.trim()) throw new HttpException('message required', 400);
|
||||||
): Promise<ChatResponse> {
|
|
||||||
if (!authHeader) {
|
|
||||||
throw new HttpException('Authorization required', 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!body.message || body.message.trim().length === 0) {
|
const msg = body.message.trim();
|
||||||
throw new HttpException('message is required', 400);
|
const ctx = body.context;
|
||||||
}
|
let prefix = '';
|
||||||
|
if (ctx) {
|
||||||
const message = body.message.trim();
|
|
||||||
const context = body.context;
|
|
||||||
|
|
||||||
// Build context string for the user message
|
|
||||||
let contextPrefix = '';
|
|
||||||
if (context) {
|
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
if (context.leadName) parts.push(`Caller: ${context.leadName}`);
|
if (ctx.leadName) parts.push(`Caller: ${ctx.leadName}`);
|
||||||
if (context.callerPhone) parts.push(`Phone: ${context.callerPhone}`);
|
if (ctx.callerPhone) parts.push(`Phone: ${ctx.callerPhone}`);
|
||||||
if (context.leadId) parts.push(`Lead ID: ${context.leadId}`);
|
if (ctx.leadId) parts.push(`Lead ID: ${ctx.leadId}`);
|
||||||
if (parts.length > 0) {
|
if (parts.length) prefix = `[Active call: ${parts.join(', ')}]\n\n`;
|
||||||
contextPrefix = `[Call Context: ${parts.join(', ')}]\n\n`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no API key, use keyword fallback
|
if (!this.aiModel) {
|
||||||
if (!this.client) {
|
return { reply: await this.fallback(msg, auth), sources: ['fallback'], confidence: 'low' };
|
||||||
return this.keywordFallback(message, context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.client.messages.create({
|
return await this.chatWithTools(`${prefix}${msg}`, auth);
|
||||||
model: 'claude-haiku-4-5-20251001',
|
} catch (err) {
|
||||||
max_tokens: 300,
|
this.logger.error(`AI chat error: ${err}`);
|
||||||
system: SYSTEM_PROMPT,
|
return { reply: await this.fallback(msg, auth), sources: ['fallback'], confidence: 'low' };
|
||||||
messages: [
|
}
|
||||||
{
|
}
|
||||||
role: 'user',
|
|
||||||
content: `${contextPrefix}${message}`,
|
private async buildKnowledgeBase(auth: string): Promise<string> {
|
||||||
},
|
const now = Date.now();
|
||||||
],
|
if (this.knowledgeBase && now - this.kbLoadedAt < this.kbTtlMs) {
|
||||||
|
return this.knowledgeBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log('Building knowledge base from platform data...');
|
||||||
|
const sections: string[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const clinicData = await this.platform.queryWithAuth<any>(
|
||||||
|
`{ clinics(first: 20) { edges { node {
|
||||||
|
id name clinicName
|
||||||
|
addressCustom { addressStreet1 addressCity addressState addressPostcode }
|
||||||
|
weekdayHours saturdayHours sundayHours
|
||||||
|
status walkInAllowed onlineBooking
|
||||||
|
cancellationWindowHours arriveEarlyMin requiredDocuments
|
||||||
|
acceptsCash acceptsCard acceptsUpi
|
||||||
|
} } } }`,
|
||||||
|
undefined, auth,
|
||||||
|
);
|
||||||
|
const clinics = clinicData.clinics.edges.map((e: any) => e.node);
|
||||||
|
if (clinics.length) {
|
||||||
|
sections.push('## Clinics');
|
||||||
|
for (const c of clinics) {
|
||||||
|
const addr = c.addressCustom
|
||||||
|
? [c.addressCustom.addressStreet1, c.addressCustom.addressCity].filter(Boolean).join(', ')
|
||||||
|
: '';
|
||||||
|
const hours = [
|
||||||
|
c.weekdayHours ? `Mon–Fri ${c.weekdayHours}` : '',
|
||||||
|
c.saturdayHours ? `Sat ${c.saturdayHours}` : '',
|
||||||
|
c.sundayHours ? `Sun ${c.sundayHours}` : 'Sun closed',
|
||||||
|
].filter(Boolean).join(', ');
|
||||||
|
sections.push(`- ${c.clinicName ?? c.name}: ${addr}. ${hours}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rulesClinic = clinics[0];
|
||||||
|
const rules: string[] = [];
|
||||||
|
if (rulesClinic.cancellationWindowHours) rules.push(`Free cancellation up to ${rulesClinic.cancellationWindowHours}h before`);
|
||||||
|
if (rulesClinic.arriveEarlyMin) rules.push(`Arrive ${rulesClinic.arriveEarlyMin}min early`);
|
||||||
|
if (rulesClinic.requiredDocuments) rules.push(`First-time patients bring ${rulesClinic.requiredDocuments}`);
|
||||||
|
if (rulesClinic.walkInAllowed) rules.push('Walk-ins accepted');
|
||||||
|
if (rulesClinic.onlineBooking) rules.push('Online booking available');
|
||||||
|
if (rules.length) {
|
||||||
|
sections.push('\n### Booking Rules');
|
||||||
|
sections.push(rules.map(r => `- ${r}`).join('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const payments: string[] = [];
|
||||||
|
if (rulesClinic.acceptsCash === 'YES') payments.push('Cash');
|
||||||
|
if (rulesClinic.acceptsCard === 'YES') payments.push('Cards');
|
||||||
|
if (rulesClinic.acceptsUpi === 'YES') payments.push('UPI');
|
||||||
|
if (payments.length) {
|
||||||
|
sections.push('\n### Payments');
|
||||||
|
sections.push(`Accepted: ${payments.join(', ')}.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.warn(`Failed to fetch clinics: ${err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pkgData = await this.platform.queryWithAuth<any>(
|
||||||
|
`{ healthPackages(first: 30, filter: { active: { eq: true } }) { edges { node {
|
||||||
|
id name packageName description
|
||||||
|
price { amountMicros currencyCode }
|
||||||
|
discountedPrice { amountMicros currencyCode }
|
||||||
|
department inclusions durationMin eligibility
|
||||||
|
packageTests { edges { node { labTest { testName category } order } } }
|
||||||
|
} } } }`,
|
||||||
|
undefined, auth,
|
||||||
|
);
|
||||||
|
const packages = pkgData.healthPackages.edges.map((e: any) => e.node);
|
||||||
|
if (packages.length) {
|
||||||
|
sections.push('\n## Health Packages');
|
||||||
|
for (const p of packages) {
|
||||||
|
const price = p.price ? `₹${p.price.amountMicros / 1_000_000}` : '';
|
||||||
|
const disc = p.discountedPrice?.amountMicros ? ` (discounted: ₹${p.discountedPrice.amountMicros / 1_000_000})` : '';
|
||||||
|
const dept = p.department ? ` [${p.department}]` : '';
|
||||||
|
sections.push(`- ${p.packageName ?? p.name}: ${price}${disc}${dept}`);
|
||||||
|
const tests = p.packageTests?.edges
|
||||||
|
?.map((e: any) => e.node)
|
||||||
|
?.sort((a: any, b: any) => (a.order ?? 0) - (b.order ?? 0))
|
||||||
|
?.map((t: any) => t.labTest?.testName)
|
||||||
|
?.filter(Boolean);
|
||||||
|
if (tests?.length) {
|
||||||
|
sections.push(` Tests: ${tests.join(', ')}`);
|
||||||
|
} else if (p.inclusions) {
|
||||||
|
sections.push(` Includes: ${p.inclusions}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.warn(`Failed to fetch health packages: ${err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const insData = await this.platform.queryWithAuth<any>(
|
||||||
|
`{ insurancePartners(first: 30, filter: { empanelmentStatus: { eq: ACTIVE } }) { edges { node {
|
||||||
|
id name insurerName tpaName settlementType planTypesAccepted
|
||||||
|
} } } }`,
|
||||||
|
undefined, auth,
|
||||||
|
);
|
||||||
|
const insurers = insData.insurancePartners.edges.map((e: any) => e.node);
|
||||||
|
if (insurers.length) {
|
||||||
|
sections.push('\n## Insurance Partners');
|
||||||
|
const names = insurers.map((i: any) => {
|
||||||
|
const settlement = i.settlementType ? ` (${i.settlementType.toLowerCase()})` : '';
|
||||||
|
return `${i.insurerName ?? i.name}${settlement}`;
|
||||||
|
});
|
||||||
|
sections.push(names.join(', '));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.warn(`Failed to fetch insurance partners: ${err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.knowledgeBase = sections.join('\n') || 'No hospital information available yet.';
|
||||||
|
this.kbLoadedAt = now;
|
||||||
|
this.logger.log(`Knowledge base built (${this.knowledgeBase.length} chars)`);
|
||||||
|
return this.knowledgeBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildSystemPrompt(kb: string): string {
|
||||||
|
return `You are an AI assistant for call center agents at a hospital.
|
||||||
|
You help agents answer questions about patients, doctors, appointments, and hospital services during live calls.
|
||||||
|
|
||||||
|
RULES:
|
||||||
|
1. For patient-specific questions, you MUST use lookup tools. NEVER guess patient data.
|
||||||
|
2. For doctor schedule/fees, use the lookup_doctor tool to get real data from the system.
|
||||||
|
3. For hospital info (clinics, packages, insurance), use the knowledge base below.
|
||||||
|
4. If a tool returns no data, say "I couldn't find that in our system."
|
||||||
|
5. Be concise — agents are on live calls. Under 100 words unless asked for detail.
|
||||||
|
6. NEVER give medical advice, diagnosis, or treatment recommendations.
|
||||||
|
7. NEVER share sensitive hospital data (revenue, salaries, internal policies).
|
||||||
|
8. Format with bullet points for easy scanning.
|
||||||
|
|
||||||
|
${kb}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async chatWithTools(userMessage: string, auth: string) {
|
||||||
|
const kb = await this.buildKnowledgeBase(auth);
|
||||||
|
const systemPrompt = this.buildSystemPrompt(kb);
|
||||||
|
const platformService = this.platform;
|
||||||
|
|
||||||
|
const { text, steps } = await generateText({
|
||||||
|
model: this.aiModel!,
|
||||||
|
system: systemPrompt,
|
||||||
|
prompt: userMessage,
|
||||||
|
stopWhen: stepCountIs(5),
|
||||||
|
tools: {
|
||||||
|
lookup_patient: tool({
|
||||||
|
description: 'Search for a patient/lead by phone number or name. Returns their profile, lead status, AI summary, and linked patient/campaign IDs.',
|
||||||
|
inputSchema: z.object({
|
||||||
|
phone: z.string().optional().describe('Phone number to search'),
|
||||||
|
name: z.string().optional().describe('Patient/lead name to search'),
|
||||||
|
}),
|
||||||
|
execute: async ({ phone, name }) => {
|
||||||
|
const data = await platformService.queryWithAuth<any>(
|
||||||
|
`{ leads(first: 50) { edges { node {
|
||||||
|
id name contactName { firstName lastName }
|
||||||
|
contactPhone { primaryPhoneNumber }
|
||||||
|
contactEmail { primaryEmail }
|
||||||
|
source status interestedService assignedAgent
|
||||||
|
leadScore contactAttempts firstContacted lastContacted
|
||||||
|
aiSummary aiSuggestedAction patientId campaignId
|
||||||
|
} } } }`,
|
||||||
|
undefined, auth,
|
||||||
|
);
|
||||||
|
const leads = data.leads.edges.map((e: any) => e.node);
|
||||||
|
const phoneClean = (phone ?? '').replace(/\D/g, '');
|
||||||
|
const nameClean = (name ?? '').toLowerCase();
|
||||||
|
|
||||||
|
const matched = leads.filter((l: any) => {
|
||||||
|
if (phoneClean) {
|
||||||
|
const lp = (l.contactPhone?.primaryPhoneNumber ?? '').replace(/\D/g, '');
|
||||||
|
if (lp.endsWith(phoneClean) || phoneClean.endsWith(lp)) return true;
|
||||||
|
}
|
||||||
|
if (nameClean) {
|
||||||
|
const fn = `${l.contactName?.firstName ?? ''} ${l.contactName?.lastName ?? ''}`.toLowerCase();
|
||||||
|
if (fn.includes(nameClean)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
const text = response.content[0].type === 'text' ? response.content[0].text : '';
|
if (!matched.length) return { found: false, message: 'No patient/lead found.' };
|
||||||
|
return { found: true, count: matched.length, leads: matched };
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
this.logger.log(`AI chat responded (${text.length} chars) for: "${message.substring(0, 50)}..."`);
|
lookup_appointments: tool({
|
||||||
|
description: 'Get all appointments (past and upcoming) for a patient. Returns doctor, department, date, status, and reason.',
|
||||||
|
inputSchema: z.object({
|
||||||
|
patientId: z.string().describe('Patient ID'),
|
||||||
|
}),
|
||||||
|
execute: async ({ patientId }) => {
|
||||||
|
const data = await platformService.queryWithAuth<any>(
|
||||||
|
`{ appointments(first: 20, filter: { patientId: { eq: "${patientId}" } }, orderBy: [{ scheduledAt: DescNullsLast }]) { edges { node {
|
||||||
|
id name scheduledAt durationMin appointmentType status
|
||||||
|
doctorName department reasonForVisit doctorId
|
||||||
|
} } } }`,
|
||||||
|
undefined, auth,
|
||||||
|
);
|
||||||
|
return { appointments: data.appointments.edges.map((e: any) => e.node) };
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
lookup_call_history: tool({
|
||||||
|
description: 'Get call log for a lead — all inbound/outbound calls with dispositions and durations.',
|
||||||
|
inputSchema: z.object({
|
||||||
|
leadId: z.string().describe('Lead ID'),
|
||||||
|
}),
|
||||||
|
execute: async ({ leadId }) => {
|
||||||
|
const data = await platformService.queryWithAuth<any>(
|
||||||
|
`{ calls(first: 20, filter: { leadId: { eq: "${leadId}" } }, orderBy: [{ startedAt: DescNullsLast }]) { edges { node {
|
||||||
|
id name direction callStatus agentName startedAt durationSec disposition
|
||||||
|
} } } }`,
|
||||||
|
undefined, auth,
|
||||||
|
);
|
||||||
|
return { calls: data.calls.edges.map((e: any) => e.node) };
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
lookup_lead_activities: tool({
|
||||||
|
description: 'Get the full interaction timeline — status changes, calls, WhatsApp, notes, appointments.',
|
||||||
|
inputSchema: z.object({
|
||||||
|
leadId: z.string().describe('Lead ID'),
|
||||||
|
}),
|
||||||
|
execute: async ({ leadId }) => {
|
||||||
|
const data = await platformService.queryWithAuth<any>(
|
||||||
|
`{ leadActivities(first: 30, filter: { leadId: { eq: "${leadId}" } }, orderBy: [{ occurredAt: DescNullsLast }]) { edges { node {
|
||||||
|
id activityType summary occurredAt performedBy channel
|
||||||
|
} } } }`,
|
||||||
|
undefined, auth,
|
||||||
|
);
|
||||||
|
return { activities: data.leadActivities.edges.map((e: any) => e.node) };
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
lookup_doctor: tool({
|
||||||
|
description: 'Get doctor details — schedule, clinic, fees, qualifications, specialty. Search by name.',
|
||||||
|
inputSchema: z.object({
|
||||||
|
doctorName: z.string().describe('Doctor name (e.g. "Patel", "Sharma")'),
|
||||||
|
}),
|
||||||
|
execute: async ({ doctorName }) => {
|
||||||
|
const data = await platformService.queryWithAuth<any>(
|
||||||
|
`{ doctors(first: 10) { edges { node {
|
||||||
|
id name fullName { firstName lastName }
|
||||||
|
department specialty qualifications yearsOfExperience
|
||||||
|
visitingHours
|
||||||
|
consultationFeeNew { amountMicros currencyCode }
|
||||||
|
consultationFeeFollowUp { amountMicros currencyCode }
|
||||||
|
active registrationNumber
|
||||||
|
clinic { id name clinicName }
|
||||||
|
} } } }`,
|
||||||
|
undefined, auth,
|
||||||
|
);
|
||||||
|
|
||||||
|
const doctors = data.doctors.edges.map((e: any) => e.node);
|
||||||
|
const search = doctorName.toLowerCase();
|
||||||
|
const matched = doctors.filter((d: any) => {
|
||||||
|
const full = `${d.fullName?.firstName ?? ''} ${d.fullName?.lastName ?? ''} ${d.name ?? ''}`.toLowerCase();
|
||||||
|
return full.includes(search);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!matched.length) return { found: false, message: `No doctor matching "${doctorName}"` };
|
||||||
|
|
||||||
|
return {
|
||||||
|
found: true,
|
||||||
|
doctors: matched.map((d: any) => ({
|
||||||
|
...d,
|
||||||
|
clinicName: d.clinic?.clinicName ?? d.clinic?.name ?? 'N/A',
|
||||||
|
feeNewFormatted: d.consultationFeeNew ? `₹${d.consultationFeeNew.amountMicros / 1_000_000}` : 'N/A',
|
||||||
|
feeFollowUpFormatted: d.consultationFeeFollowUp ? `₹${d.consultationFeeFollowUp.amountMicros / 1_000_000}` : 'N/A',
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const toolCallCount = steps.filter(s => s.toolCalls?.length).length;
|
||||||
|
this.logger.log(`Response (${text.length} chars, ${toolCallCount} tool steps)`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
reply: text,
|
reply: text,
|
||||||
sources: ['hospital_db'],
|
sources: toolCallCount > 0 ? ['platform_db', 'hospital_kb'] : ['hospital_kb'],
|
||||||
confidence: 'high',
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`AI chat failed: ${error}`);
|
|
||||||
return this.keywordFallback(message, context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private keywordFallback(message: string, context?: ChatContext): ChatResponse {
|
|
||||||
const lower = message.toLowerCase();
|
|
||||||
|
|
||||||
// Doctor-specific responses
|
|
||||||
if (lower.includes('sharma')) {
|
|
||||||
return {
|
|
||||||
reply: 'Dr. Sharma (Cardiology) is available at Koramangala and Whitefield clinics on Mon/Wed/Fri from 10 AM to 1 PM. Consultation fee: ₹800 for new patients, ₹500 for follow-ups.',
|
|
||||||
sources: ['hospital_db'],
|
|
||||||
confidence: 'high',
|
confidence: 'high',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lower.includes('patel')) {
|
private async fallback(msg: string, auth: string): Promise<string> {
|
||||||
return {
|
try {
|
||||||
reply: 'Dr. Patel (Gynecology) is available at Indiranagar and Koramangala clinics on Tue/Thu/Sat from 9 AM to 12 PM. Consultation fee: ₹700 for new patients, ₹400 for follow-ups.',
|
const doctors = await this.platform.queryWithAuth<any>(
|
||||||
sources: ['hospital_db'],
|
`{ doctors(first: 10) { edges { node {
|
||||||
confidence: 'high',
|
name fullName { firstName lastName } department specialty visitingHours
|
||||||
};
|
consultationFeeNew { amountMicros currencyCode }
|
||||||
|
clinic { name clinicName }
|
||||||
|
} } } }`,
|
||||||
|
undefined, auth,
|
||||||
|
);
|
||||||
|
const docs = doctors.doctors.edges.map((e: any) => e.node);
|
||||||
|
const l = msg.toLowerCase();
|
||||||
|
|
||||||
|
const matchedDoc = docs.find((d: any) => {
|
||||||
|
const full = `${d.fullName?.firstName ?? ''} ${d.fullName?.lastName ?? ''} ${d.name ?? ''}`.toLowerCase();
|
||||||
|
return l.split(/\s+/).some((w: string) => w.length > 2 && full.includes(w));
|
||||||
|
});
|
||||||
|
if (matchedDoc) {
|
||||||
|
const fee = matchedDoc.consultationFeeNew ? `₹${matchedDoc.consultationFeeNew.amountMicros / 1_000_000}` : '';
|
||||||
|
const clinic = matchedDoc.clinic?.clinicName ?? '';
|
||||||
|
return `Dr. ${matchedDoc.fullName?.lastName ?? matchedDoc.name} (${matchedDoc.department ?? matchedDoc.specialty}): ${matchedDoc.visitingHours ?? 'hours not set'}${clinic ? ` at ${clinic}` : ''}${fee ? `. Fee: ${fee}` : ''}.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lower.includes('kumar')) {
|
if (l.includes('doctor') || l.includes('available')) {
|
||||||
return {
|
return 'Doctors: ' + docs.map((d: any) =>
|
||||||
reply: 'Dr. Kumar (Orthopedics) is available at Whitefield clinic Mon-Fri from 2 PM to 5 PM. Consultation fee: ₹600 for new patients, ₹400 for follow-ups.',
|
`${d.fullName?.lastName ?? d.name} (${d.department ?? d.specialty})`
|
||||||
sources: ['hospital_db'],
|
).join(', ') + '.';
|
||||||
confidence: 'high',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lower.includes('reddy')) {
|
if (l.includes('package') || l.includes('checkup') || l.includes('screening')) {
|
||||||
return {
|
const pkgs = await this.platform.queryWithAuth<any>(
|
||||||
reply: 'Dr. Reddy (General Medicine) is available at all three clinics (Koramangala, Indiranagar, Whitefield) Mon-Sat from 9 AM to 6 PM. Consultation fee: ₹500 for new patients, ₹300 for follow-ups.',
|
`{ healthPackages(first: 20) { edges { node { packageName price { amountMicros } } } } }`,
|
||||||
sources: ['hospital_db'],
|
undefined, auth,
|
||||||
confidence: 'high',
|
);
|
||||||
};
|
const packages = pkgs.healthPackages.edges.map((e: any) => e.node);
|
||||||
|
if (packages.length) {
|
||||||
|
return 'Packages: ' + packages.map((p: any) =>
|
||||||
|
`${p.packageName} ₹${p.price?.amountMicros ? p.price.amountMicros / 1_000_000 : 'N/A'}`
|
||||||
|
).join(' | ') + '.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// platform unreachable
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lower.includes('singh')) {
|
return 'I can help with: doctor schedules, patient lookup, appointments, packages, insurance. What do you need?';
|
||||||
return {
|
|
||||||
reply: 'Dr. Singh (ENT) is available at Indiranagar clinic on Mon/Wed/Fri from 11 AM to 3 PM. Consultation fee: ₹600 for new patients, ₹400 for follow-ups.',
|
|
||||||
sources: ['hospital_db'],
|
|
||||||
confidence: 'high',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Topic-based responses
|
|
||||||
if (lower.includes('availability') || lower.includes('visiting') || lower.includes('hours') || lower.includes('timing')) {
|
|
||||||
return {
|
|
||||||
reply: 'Our doctors\' visiting hours:\n- Dr. Sharma (Cardiology): Mon/Wed/Fri 10 AM–1 PM\n- Dr. Patel (Gynecology): Tue/Thu/Sat 9 AM–12 PM\n- Dr. Kumar (Orthopedics): Mon–Fri 2 PM–5 PM\n- Dr. Reddy (General Medicine): Mon–Sat 9 AM–6 PM\n- Dr. Singh (ENT): Mon/Wed/Fri 11 AM–3 PM\n\nWhich doctor would the patient like to see?',
|
|
||||||
sources: ['hospital_db'],
|
|
||||||
confidence: 'high',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lower.includes('clinic') || lower.includes('location') || lower.includes('address') || lower.includes('branch')) {
|
|
||||||
return {
|
|
||||||
reply: 'Global Hospital has 3 clinics in Bengaluru:\n- **Koramangala**: 80 Feet Road, Mon–Sat 8 AM–8 PM, Sun 9 AM–2 PM\n- **Whitefield**: Prestige Shantiniketan, Mon–Sat 8 AM–8 PM, Sun closed\n- **Indiranagar**: 100 Feet Road, Mon–Sat 9 AM–7 PM, Sun 10 AM–1 PM',
|
|
||||||
sources: ['hospital_db'],
|
|
||||||
confidence: 'high',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lower.includes('package') || lower.includes('checkup') || lower.includes('screening') || lower.includes('wellness')) {
|
|
||||||
return {
|
|
||||||
reply: 'Our treatment packages:\n- **Master Health Checkup**: ₹2,999 (blood work, ECG, X-ray, consultation)\n- **Cardiac Screening**: ₹4,999 (ECG, echo, TMT, lipid profile)\n- **Women\'s Wellness**: ₹3,499 (pap smear, mammography, thyroid)\n- **Orthopedic Assessment**: ₹1,999 (X-ray, bone density, physio)\n\nAll packages available at all clinics.',
|
|
||||||
sources: ['hospital_db'],
|
|
||||||
confidence: 'high',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lower.includes('insurance') || lower.includes('payment') || lower.includes('emi')) {
|
|
||||||
return {
|
|
||||||
reply: 'We accept: Star Health, ICICI Lombard, Bajaj Allianz, HDFC Ergo, Max Bupa, New India Assurance. Payment via cash, cards, UPI, or net banking. EMI available for procedures above ₹10,000.',
|
|
||||||
sources: ['hospital_db'],
|
|
||||||
confidence: 'high',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lower.includes('appointment') || lower.includes('book') || lower.includes('cancel')) {
|
|
||||||
return {
|
|
||||||
reply: 'Appointments can be booked via phone, website, or walk-in. Free cancellation up to 4 hours before the appointment. Patients should arrive 15 minutes early. First-time patients need a valid government ID and previous medical records.',
|
|
||||||
sources: ['hospital_db'],
|
|
||||||
confidence: 'high',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lower.includes('fee') || lower.includes('cost') || lower.includes('price') || lower.includes('charge')) {
|
|
||||||
return {
|
|
||||||
reply: 'Consultation fees (New / Follow-up):\n- Dr. Sharma (Cardiology): ₹800 / ₹500\n- Dr. Patel (Gynecology): ₹700 / ₹400\n- Dr. Kumar (Orthopedics): ₹600 / ₹400\n- Dr. Reddy (General Medicine): ₹500 / ₹300\n- Dr. Singh (ENT): ₹600 / ₹400',
|
|
||||||
sources: ['hospital_db'],
|
|
||||||
confidence: 'high',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Patient context response
|
|
||||||
if (lower.includes('patient') || lower.includes('history') || lower.includes('lead')) {
|
|
||||||
if (context?.leadName) {
|
|
||||||
return {
|
|
||||||
reply: `For ${context.leadName} (${context.callerPhone ?? 'no phone on file'}): I can see this is an existing lead in the system. Please check the lead card on your screen for their full history, AI insights, and recent activities.`,
|
|
||||||
sources: ['hospital_db'],
|
|
||||||
confidence: 'medium',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
reply: 'To look up a patient\'s history, I need their name or phone number. Please provide the caller\'s details or identify them from the incoming call card.',
|
|
||||||
sources: ['hospital_db'],
|
|
||||||
confidence: 'medium',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default fallback
|
|
||||||
return {
|
|
||||||
reply: 'I don\'t have specific information about that. Here\'s what I can help with:\n- Doctor availability and visiting hours\n- Clinic locations and timings\n- Treatment packages and pricing\n- Insurance and payment options\n- Appointment booking process\n\nPlease ask about any of these topics!',
|
|
||||||
sources: ['hospital_db'],
|
|
||||||
confidence: 'low',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import Anthropic from '@anthropic-ai/sdk';
|
import { generateObject } from 'ai';
|
||||||
|
import type { LanguageModel } from 'ai';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { createAiModel } from './ai-provider';
|
||||||
|
|
||||||
type LeadContext = {
|
type LeadContext = {
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
@@ -19,23 +22,25 @@ type EnrichmentResult = {
|
|||||||
aiSuggestedAction: string;
|
aiSuggestedAction: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const enrichmentSchema = z.object({
|
||||||
|
aiSummary: z.string().describe('1-2 sentence summary of who this lead is and their history'),
|
||||||
|
aiSuggestedAction: z.string().describe('5-10 word suggested action for the agent'),
|
||||||
|
});
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AiEnrichmentService {
|
export class AiEnrichmentService {
|
||||||
private readonly logger = new Logger(AiEnrichmentService.name);
|
private readonly logger = new Logger(AiEnrichmentService.name);
|
||||||
private client: Anthropic | null = null;
|
private readonly aiModel: LanguageModel | null;
|
||||||
|
|
||||||
constructor(private config: ConfigService) {
|
constructor(private config: ConfigService) {
|
||||||
const apiKey = config.get<string>('ai.anthropicApiKey');
|
this.aiModel = createAiModel(config);
|
||||||
if (apiKey) {
|
if (!this.aiModel) {
|
||||||
this.client = new Anthropic({ apiKey });
|
this.logger.warn('AI not configured — enrichment uses fallback');
|
||||||
} else {
|
|
||||||
this.logger.warn('ANTHROPIC_API_KEY not set — AI enrichment disabled, using fallback');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async enrichLead(lead: LeadContext): Promise<EnrichmentResult> {
|
async enrichLead(lead: LeadContext): Promise<EnrichmentResult> {
|
||||||
// Fallback if no API key configured
|
if (!this.aiModel) {
|
||||||
if (!this.client) {
|
|
||||||
return this.fallbackEnrichment(lead);
|
return this.fallbackEnrichment(lead);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +53,10 @@ export class AiEnrichmentService {
|
|||||||
? lead.activities.map(a => `- ${a.activityType}: ${a.summary}`).join('\n')
|
? lead.activities.map(a => `- ${a.activityType}: ${a.summary}`).join('\n')
|
||||||
: 'No previous interactions';
|
: 'No previous interactions';
|
||||||
|
|
||||||
const prompt = `You are an AI assistant for a hospital call center at Global Hospital.
|
const { object } = await generateObject({
|
||||||
|
model: this.aiModel!,
|
||||||
|
schema: enrichmentSchema,
|
||||||
|
prompt: `You are an AI assistant for a hospital call center.
|
||||||
An inbound call is coming in from a lead. Summarize their history and suggest what the call center agent should do.
|
An inbound call is coming in from a lead. Summarize their history and suggest what the call center agent should do.
|
||||||
|
|
||||||
Lead details:
|
Lead details:
|
||||||
@@ -60,25 +68,11 @@ Lead details:
|
|||||||
- Contact attempts: ${lead.contactAttempts ?? 0}
|
- Contact attempts: ${lead.contactAttempts ?? 0}
|
||||||
|
|
||||||
Recent activity:
|
Recent activity:
|
||||||
${activitiesText}
|
${activitiesText}`,
|
||||||
|
|
||||||
Respond ONLY with valid JSON (no markdown, no code blocks):
|
|
||||||
{"aiSummary": "1-2 sentence summary of who this lead is and their history", "aiSuggestedAction": "5-10 word suggested action for the agent"}`;
|
|
||||||
|
|
||||||
const response = await this.client.messages.create({
|
|
||||||
model: 'claude-haiku-4-5-20251001',
|
|
||||||
max_tokens: 200,
|
|
||||||
messages: [{ role: 'user', content: prompt }],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const text = response.content[0].type === 'text' ? response.content[0].text : '';
|
|
||||||
const parsed = JSON.parse(text.trim());
|
|
||||||
|
|
||||||
this.logger.log(`AI enrichment generated for lead ${lead.firstName} ${lead.lastName}`);
|
this.logger.log(`AI enrichment generated for lead ${lead.firstName} ${lead.lastName}`);
|
||||||
return {
|
return object;
|
||||||
aiSummary: parsed.aiSummary,
|
|
||||||
aiSuggestedAction: parsed.aiSuggestedAction,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`AI enrichment failed: ${error}`);
|
this.logger.error(`AI enrichment failed: ${error}`);
|
||||||
return this.fallbackEnrichment(lead);
|
return this.fallbackEnrichment(lead);
|
||||||
|
|||||||
26
src/ai/ai-provider.ts
Normal file
26
src/ai/ai-provider.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { anthropic } from '@ai-sdk/anthropic';
|
||||||
|
import { openai } from '@ai-sdk/openai';
|
||||||
|
import type { LanguageModel } from 'ai';
|
||||||
|
|
||||||
|
export function createAiModel(config: ConfigService): LanguageModel | null {
|
||||||
|
const provider = config.get<string>('ai.provider') ?? 'openai';
|
||||||
|
const model = config.get<string>('ai.model') ?? 'gpt-4o-mini';
|
||||||
|
|
||||||
|
if (provider === 'anthropic') {
|
||||||
|
const apiKey = config.get<string>('ai.anthropicApiKey');
|
||||||
|
if (!apiKey) return null;
|
||||||
|
return anthropic(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to openai
|
||||||
|
const apiKey = config.get<string>('ai.openaiApiKey');
|
||||||
|
if (!apiKey) return null;
|
||||||
|
return openai(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAiConfigured(config: ConfigService): boolean {
|
||||||
|
const provider = config.get<string>('ai.provider') ?? 'openai';
|
||||||
|
if (provider === 'anthropic') return !!config.get<string>('ai.anthropicApiKey');
|
||||||
|
return !!config.get<string>('ai.openaiApiKey');
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { PlatformModule } from '../platform/platform.module';
|
||||||
import { AiEnrichmentService } from './ai-enrichment.service';
|
import { AiEnrichmentService } from './ai-enrichment.service';
|
||||||
import { AiChatController } from './ai-chat.controller';
|
import { AiChatController } from './ai-chat.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [PlatformModule],
|
||||||
controllers: [AiChatController],
|
controllers: [AiChatController],
|
||||||
providers: [AiEnrichmentService],
|
providers: [AiEnrichmentService],
|
||||||
exports: [AiEnrichmentService],
|
exports: [AiEnrichmentService],
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ export default () => ({
|
|||||||
webhookSecret: process.env.EXOTEL_WEBHOOK_SECRET ?? '',
|
webhookSecret: process.env.EXOTEL_WEBHOOK_SECRET ?? '',
|
||||||
},
|
},
|
||||||
ai: {
|
ai: {
|
||||||
|
provider: process.env.AI_PROVIDER ?? 'openai',
|
||||||
anthropicApiKey: process.env.ANTHROPIC_API_KEY ?? '',
|
anthropicApiKey: process.env.ANTHROPIC_API_KEY ?? '',
|
||||||
|
openaiApiKey: process.env.OPENAI_API_KEY ?? '',
|
||||||
|
model: process.env.AI_MODEL ?? 'gpt-4o-mini',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { Controller, Get, Headers, HttpException, Logger } from '@nestjs/common';
|
import { Controller, Get, Headers, HttpException, Logger } from '@nestjs/common';
|
||||||
|
import { PlatformGraphqlService } from '../platform/platform-graphql.service';
|
||||||
import { WorklistService } from './worklist.service';
|
import { WorklistService } from './worklist.service';
|
||||||
|
|
||||||
@Controller('api/worklist')
|
@Controller('api/worklist')
|
||||||
export class WorklistController {
|
export class WorklistController {
|
||||||
private readonly logger = new Logger(WorklistController.name);
|
private readonly logger = new Logger(WorklistController.name);
|
||||||
|
|
||||||
constructor(private readonly worklist: WorklistService) {}
|
constructor(
|
||||||
|
private readonly worklist: WorklistService,
|
||||||
|
private readonly platform: PlatformGraphqlService,
|
||||||
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
async getWorklist(@Headers('authorization') authHeader: string) {
|
async getWorklist(@Headers('authorization') authHeader: string) {
|
||||||
@@ -13,33 +17,25 @@ export class WorklistController {
|
|||||||
throw new HttpException('Authorization required', 401);
|
throw new HttpException('Authorization required', 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode the JWT to extract the agent name
|
const agentName = await this.resolveAgentName(authHeader);
|
||||||
// The platform JWT payload contains user info — we extract the name
|
|
||||||
const agentName = this.extractAgentName(authHeader);
|
|
||||||
if (!agentName) {
|
|
||||||
throw new HttpException('Could not determine agent identity from token', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log(`Fetching worklist for agent: ${agentName}`);
|
this.logger.log(`Fetching worklist for agent: ${agentName}`);
|
||||||
|
|
||||||
return this.worklist.getWorklist(agentName, authHeader);
|
return this.worklist.getWorklist(agentName, authHeader);
|
||||||
}
|
}
|
||||||
|
|
||||||
private extractAgentName(authHeader: string): string | null {
|
private async resolveAgentName(authHeader: string): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const token = authHeader.replace(/^Bearer\s+/i, '');
|
const data = await this.platform.queryWithAuth<any>(
|
||||||
// JWT payload is the second segment, base64url-encoded
|
`{ currentUser { workspaceMember { name { firstName lastName } } } }`,
|
||||||
const payload = JSON.parse(
|
undefined,
|
||||||
Buffer.from(token.split('.')[1], 'base64url').toString('utf8'),
|
authHeader,
|
||||||
);
|
);
|
||||||
// The platform JWT includes sub (userId) and workspace info
|
const name = data.currentUser?.workspaceMember?.name;
|
||||||
// The agent name comes from firstName + lastName in the token
|
const full = `${name?.firstName ?? ''} ${name?.lastName ?? ''}`.trim();
|
||||||
const firstName = payload.firstName ?? payload.given_name ?? '';
|
if (full) return full;
|
||||||
const lastName = payload.lastName ?? payload.family_name ?? '';
|
} catch (err) {
|
||||||
const fullName = `${firstName} ${lastName}`.trim();
|
this.logger.warn(`Failed to resolve agent name: ${err}`);
|
||||||
return fullName || payload.email || payload.sub || null;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
throw new HttpException('Could not determine agent identity', 400);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ export class WorklistService {
|
|||||||
|
|
||||||
async getWorklist(agentName: string, authHeader: string): Promise<WorklistResponse> {
|
async getWorklist(agentName: string, authHeader: string): Promise<WorklistResponse> {
|
||||||
const [missedCalls, followUps, marketingLeads] = await Promise.all([
|
const [missedCalls, followUps, marketingLeads] = await Promise.all([
|
||||||
this.getMissedCallsWithToken(agentName, authHeader),
|
this.getMissedCalls(agentName, authHeader),
|
||||||
this.getPendingFollowUpsWithToken(agentName, authHeader),
|
this.getPendingFollowUps(agentName, authHeader),
|
||||||
this.getAssignedLeadsWithToken(agentName, authHeader),
|
this.getAssignedLeads(agentName, authHeader),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -29,108 +29,64 @@ export class WorklistService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAssignedLeadsWithToken(agentName: string, authHeader: string): Promise<any[]> {
|
private async getAssignedLeads(agentName: string, authHeader: string): Promise<any[]> {
|
||||||
try {
|
try {
|
||||||
const data = await this.platform.queryWithAuth<{
|
const data = await this.platform.queryWithAuth<any>(
|
||||||
leads: { edges: { node: any }[] };
|
`{ leads(first: 20, filter: { assignedAgent: { eq: "${agentName}" } }, orderBy: [{ createdAt: AscNullsLast }]) { edges { node {
|
||||||
}>(
|
|
||||||
`query GetAssignedLeads($filter: LeadFilterInput, $first: Int, $orderBy: [LeadOrderByInput]) {
|
|
||||||
leads(filter: $filter, first: $first, orderBy: $orderBy) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
id createdAt
|
id createdAt
|
||||||
contactName { firstName lastName }
|
contactName { firstName lastName }
|
||||||
contactPhone { number callingCode }
|
contactPhone { primaryPhoneNumber }
|
||||||
contactEmail { address }
|
contactEmail { primaryEmail }
|
||||||
leadSource leadStatus interestedService
|
source status interestedService
|
||||||
assignedAgent campaignId adId
|
assignedAgent campaignId
|
||||||
contactAttempts spamScore isSpam
|
contactAttempts spamScore isSpam
|
||||||
aiSummary aiSuggestedAction
|
aiSummary aiSuggestedAction
|
||||||
}
|
} } } }`,
|
||||||
}
|
undefined,
|
||||||
}
|
|
||||||
}`,
|
|
||||||
{
|
|
||||||
filter: { assignedAgent: { eq: agentName } },
|
|
||||||
first: 20,
|
|
||||||
orderBy: [{ createdAt: 'AscNullsLast' }],
|
|
||||||
},
|
|
||||||
authHeader,
|
authHeader,
|
||||||
);
|
);
|
||||||
|
return data.leads.edges.map((e: any) => e.node);
|
||||||
return data.leads.edges.map((e) => e.node);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.warn(`Failed to fetch assigned leads: ${err}`);
|
this.logger.warn(`Failed to fetch assigned leads: ${err}`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getPendingFollowUpsWithToken(agentName: string, authHeader: string): Promise<any[]> {
|
private async getPendingFollowUps(agentName: string, authHeader: string): Promise<any[]> {
|
||||||
try {
|
try {
|
||||||
const data = await this.platform.queryWithAuth<{
|
const data = await this.platform.queryWithAuth<any>(
|
||||||
followUps: { edges: { node: any }[] };
|
`{ followUps(first: 20, filter: { assignedAgent: { eq: "${agentName}" } }) { edges { node {
|
||||||
}>(
|
id name createdAt
|
||||||
`query GetPendingFollowUps($filter: FollowUpFilterInput, $first: Int) {
|
typeCustom status scheduledAt completedAt
|
||||||
followUps(filter: $filter, first: $first) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
id createdAt
|
|
||||||
followUpType followUpStatus
|
|
||||||
scheduledAt completedAt
|
|
||||||
priority assignedAgent
|
priority assignedAgent
|
||||||
patientId callId
|
patientId callId
|
||||||
}
|
} } } }`,
|
||||||
}
|
undefined,
|
||||||
}
|
|
||||||
}`,
|
|
||||||
{
|
|
||||||
filter: {
|
|
||||||
assignedAgent: { eq: agentName },
|
|
||||||
followUpStatus: { in: ['PENDING', 'OVERDUE'] },
|
|
||||||
},
|
|
||||||
first: 20,
|
|
||||||
},
|
|
||||||
authHeader,
|
authHeader,
|
||||||
);
|
);
|
||||||
|
// Filter to PENDING/OVERDUE client-side since platform may not support in-filter on remapped fields
|
||||||
return data.followUps.edges.map((e) => e.node);
|
return data.followUps.edges
|
||||||
|
.map((e: any) => e.node)
|
||||||
|
.filter((f: any) => f.status === 'PENDING' || f.status === 'OVERDUE');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.warn(`Failed to fetch follow-ups: ${err}`);
|
this.logger.warn(`Failed to fetch follow-ups: ${err}`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getMissedCallsWithToken(agentName: string, authHeader: string): Promise<any[]> {
|
private async getMissedCalls(agentName: string, authHeader: string): Promise<any[]> {
|
||||||
try {
|
try {
|
||||||
const data = await this.platform.queryWithAuth<{
|
const data = await this.platform.queryWithAuth<any>(
|
||||||
calls: { edges: { node: any }[] };
|
`{ calls(first: 20, filter: { agentName: { eq: "${agentName}" }, callStatus: { eq: "MISSED" } }, orderBy: [{ startedAt: DescNullsLast }]) { edges { node {
|
||||||
}>(
|
id name createdAt
|
||||||
`query GetMissedCalls($filter: CallFilterInput, $first: Int, $orderBy: [CallOrderByInput]) {
|
direction callStatus callerNumber agentName
|
||||||
calls(filter: $filter, first: $first, orderBy: $orderBy) {
|
startedAt endedAt durationSec
|
||||||
edges {
|
disposition leadId
|
||||||
node {
|
} } } }`,
|
||||||
id createdAt
|
undefined,
|
||||||
callDirection callStatus
|
|
||||||
callerNumber { number callingCode }
|
|
||||||
agentName startedAt endedAt
|
|
||||||
durationSeconds disposition
|
|
||||||
callNotes leadId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
{
|
|
||||||
filter: {
|
|
||||||
callStatus: { eq: 'MISSED' },
|
|
||||||
agentName: { eq: agentName },
|
|
||||||
},
|
|
||||||
first: 20,
|
|
||||||
orderBy: [{ createdAt: 'AscNullsLast' }],
|
|
||||||
},
|
|
||||||
authHeader,
|
authHeader,
|
||||||
);
|
);
|
||||||
|
return data.calls.edges.map((e: any) => e.node);
|
||||||
return data.calls.edges.map((e) => e.node);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.warn(`Failed to fetch missed calls: ${err}`);
|
this.logger.warn(`Failed to fetch missed calls: ${err}`);
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
Reference in New Issue
Block a user