aliroohan179 commited on
Commit
cd5e33d
·
1 Parent(s): f1b26cb
.dockerignore ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependencies
2
+ node_modules/
3
+ npm-debug.log*
4
+
5
+ # Environment files
6
+ .env
7
+ .env.local
8
+ .env.*.local
9
+
10
+ # Vector database (will be created in container)
11
+ chroma_db/
12
+
13
+ # Git
14
+ .git/
15
+ .gitignore
16
+
17
+ # IDE
18
+ .idea/
19
+ .vscode/
20
+ *.swp
21
+ *.swo
22
+
23
+ # OS files
24
+ .DS_Store
25
+ Thumbs.db
26
+
27
+ # Documentation
28
+ README.md
29
+
30
+ # Build artifacts
31
+ dist/
32
+ build/
33
+
.gitignore ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependencies
2
+ node_modules/
3
+
4
+ # Environment variables
5
+ .env
6
+ .env.local
7
+ .env.*.local
8
+
9
+ # Vector database
10
+ chroma_db/
11
+
12
+ # Logs
13
+ logs/
14
+ *.log
15
+ npm-debug.log*
16
+
17
+ # OS files
18
+ .DS_Store
19
+ Thumbs.db
20
+
21
+ # IDE
22
+ .idea/
23
+ .vscode/
24
+ *.swp
25
+ *.swo
26
+
27
+ # Build
28
+ dist/
29
+ build/
30
+
Dockerfile ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Node.js LTS version
2
+ FROM node:20-alpine
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Copy package files
8
+ COPY package*.json ./
9
+
10
+ # Install dependencies
11
+ RUN npm install --production
12
+
13
+ # Copy source code
14
+ COPY . .
15
+
16
+ # Create directory for ChromaDB data
17
+ RUN mkdir -p /app/chroma_db
18
+
19
+ # Expose port
20
+ EXPOSE 7860
21
+
22
+ # Set environment variables
23
+ ENV NODE_ENV=production
24
+ ENV PORT=7860
25
+
26
+ # Health check
27
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
28
+ CMD node -e "require('http').get('http://localhost:7860/', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"
29
+
30
+ # Start the server
31
+ CMD ["node", "app.js"]
32
+
app.js ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express';
2
+ import cors from 'cors';
3
+ import dotenv from 'dotenv';
4
+ import { connectDB, ensureIndexes } from './db/mongo.js';
5
+ import authRoutes from './routes/auth.js';
6
+ import patientRoutes from './routes/patients.js';
7
+ import appointmentRoutes from './routes/appointments.js';
8
+ import warningRoutes from './routes/warnings.js';
9
+
10
+ dotenv.config();
11
+
12
+ const app = express();
13
+ const PORT = process.env.PORT || 7860;
14
+
15
+ // Middleware
16
+ app.use(express.json());
17
+ app.use(cors({
18
+ origin: [
19
+ 'http://localhost:3000',
20
+ 'http://localhost:5173',
21
+ 'http://127.0.0.1:3000',
22
+ 'http://127.0.0.1:5173'
23
+ ],
24
+ credentials: true,
25
+ methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
26
+ allowedHeaders: ['Content-Type', 'Authorization']
27
+ }));
28
+
29
+ // Routes
30
+ app.use('/api/auth', authRoutes);
31
+ app.use('/api', patientRoutes);
32
+ app.use('/api', appointmentRoutes);
33
+ app.use('/api', warningRoutes);
34
+
35
+ // Root endpoint
36
+ app.get('/', (req, res) => {
37
+ res.json({ message: 'Welcome to the Personalized Patient Information System!' });
38
+ });
39
+
40
+ // Error handling middleware
41
+ app.use((err, req, res, next) => {
42
+ console.error(err.stack);
43
+ res.status(500).json({ detail: err.message || 'Internal Server Error' });
44
+ });
45
+
46
+ // Start server
47
+ const startServer = async () => {
48
+ try {
49
+ await connectDB();
50
+ await ensureIndexes();
51
+
52
+ app.listen(PORT, () => {
53
+ console.log(`✓ Server running on port ${PORT}`);
54
+ });
55
+ } catch (error) {
56
+ console.error('Failed to start server:', error);
57
+ process.exit(1);
58
+ }
59
+ };
60
+
61
+ startServer();
62
+
bun.lock ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "lockfileVersion": 1,
3
+ "configVersion": 1,
4
+ "workspaces": {
5
+ "": {
6
+ "name": "patients-backend-express",
7
+ "dependencies": {
8
+ "@google/generative-ai": "^0.21.0",
9
+ "bcryptjs": "^2.4.3",
10
+ "chromadb": "^1.9.2",
11
+ "cors": "^2.8.5",
12
+ "dotenv": "^16.3.1",
13
+ "express": "^4.18.2",
14
+ "jsonwebtoken": "^9.0.2",
15
+ "mongodb": "^6.3.0",
16
+ "openai": "^4.24.0",
17
+ },
18
+ },
19
+ },
20
+ "packages": {
21
+ "@google/generative-ai": ["@google/generative-ai@0.21.0", "", {}, "sha512-7XhUbtnlkSEZK15kN3t+tzIMxsbKm/dSkKBFalj+20NvPKe1kBY7mR2P7vuijEn+f06z5+A8bVGKO0v39cr6Wg=="],
22
+
23
+ "@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.4.0", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-ZHzx7Z3rdlWL1mECydvpryWN/ETXJiCxdgQKTAH+djzIPe77HdnSizKBDi1TVDXZjXyOj2IqEG/vPw71ULF06w=="],
24
+
25
+ "@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
26
+
27
+ "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="],
28
+
29
+ "@types/webidl-conversions": ["@types/webidl-conversions@7.0.3", "", {}, "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="],
30
+
31
+ "@types/whatwg-url": ["@types/whatwg-url@11.0.5", "", { "dependencies": { "@types/webidl-conversions": "*" } }, "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ=="],
32
+
33
+ "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
34
+
35
+ "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
36
+
37
+ "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
38
+
39
+ "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
40
+
41
+ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
42
+
43
+ "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="],
44
+
45
+ "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
46
+
47
+ "bcryptjs": ["bcryptjs@2.4.3", "", {}, "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ=="],
48
+
49
+ "body-parser": ["body-parser@1.20.4", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.14.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA=="],
50
+
51
+ "bson": ["bson@6.10.4", "", {}, "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng=="],
52
+
53
+ "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
54
+
55
+ "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
56
+
57
+ "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
58
+
59
+ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
60
+
61
+ "chromadb": ["chromadb@1.10.5", "", { "dependencies": { "cliui": "^8.0.1", "isomorphic-fetch": "^3.0.0" }, "peerDependencies": { "@google/generative-ai": "^0.1.1", "cohere-ai": "^5.0.0 || ^6.0.0 || ^7.0.0", "ollama": "^0.5.0", "openai": "^3.0.0 || ^4.0.0", "voyageai": "^0.0.3-1" }, "optionalPeers": ["@google/generative-ai", "cohere-ai", "ollama", "openai", "voyageai"] }, "sha512-+IeTjjf44pKUY3vp1BacwO2tFAPcWCd64zxPZZm98dVj/kbSBeaHKB2D6eX7iRLHS1PTVASuqoR6mAJ+nrsTBg=="],
62
+
63
+ "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
64
+
65
+ "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
66
+
67
+ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
68
+
69
+ "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
70
+
71
+ "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="],
72
+
73
+ "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
74
+
75
+ "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
76
+
77
+ "cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="],
78
+
79
+ "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
80
+
81
+ "debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
82
+
83
+ "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
84
+
85
+ "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
86
+
87
+ "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="],
88
+
89
+ "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
90
+
91
+ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
92
+
93
+ "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
94
+
95
+ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
96
+
97
+ "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
98
+
99
+ "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
100
+
101
+ "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
102
+
103
+ "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
104
+
105
+ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
106
+
107
+ "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
108
+
109
+ "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
110
+
111
+ "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
112
+
113
+ "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
114
+
115
+ "express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="],
116
+
117
+ "finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="],
118
+
119
+ "form-data": ["form-data@4.0.5", "", { "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" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
120
+
121
+ "form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="],
122
+
123
+ "formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="],
124
+
125
+ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
126
+
127
+ "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
128
+
129
+ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
130
+
131
+ "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
132
+
133
+ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
134
+
135
+ "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
136
+
137
+ "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
138
+
139
+ "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
140
+
141
+ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
142
+
143
+ "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
144
+
145
+ "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="],
146
+
147
+ "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
148
+
149
+ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
150
+
151
+ "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
152
+
153
+ "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
154
+
155
+ "isomorphic-fetch": ["isomorphic-fetch@3.0.0", "", { "dependencies": { "node-fetch": "^2.6.1", "whatwg-fetch": "^3.4.1" } }, "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA=="],
156
+
157
+ "jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="],
158
+
159
+ "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
160
+
161
+ "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
162
+
163
+ "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
164
+
165
+ "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="],
166
+
167
+ "lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="],
168
+
169
+ "lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="],
170
+
171
+ "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="],
172
+
173
+ "lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="],
174
+
175
+ "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="],
176
+
177
+ "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
178
+
179
+ "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
180
+
181
+ "memory-pager": ["memory-pager@1.5.0", "", {}, "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="],
182
+
183
+ "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="],
184
+
185
+ "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="],
186
+
187
+ "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
188
+
189
+ "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
190
+
191
+ "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
192
+
193
+ "mongodb": ["mongodb@6.21.0", "", { "dependencies": { "@mongodb-js/saslprep": "^1.3.0", "bson": "^6.10.4", "mongodb-connection-string-url": "^3.0.2" }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.188.0", "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", "mongodb-client-encryption": ">=6.0.0 <7", "snappy": "^7.3.2", "socks": "^2.7.1" }, "optionalPeers": ["@aws-sdk/credential-providers", "@mongodb-js/zstd", "gcp-metadata", "kerberos", "mongodb-client-encryption", "snappy", "socks"] }, "sha512-URyb/VXMjJ4da46OeSXg+puO39XH9DeQpWCslifrRn9JWugy0D+DvvBvkm2WxmHe61O/H19JM66p1z7RHVkZ6A=="],
194
+
195
+ "mongodb-connection-string-url": ["mongodb-connection-string-url@3.0.2", "", { "dependencies": { "@types/whatwg-url": "^11.0.2", "whatwg-url": "^14.1.0 || ^13.0.0" } }, "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA=="],
196
+
197
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
198
+
199
+ "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
200
+
201
+ "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
202
+
203
+ "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
204
+
205
+ "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
206
+
207
+ "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
208
+
209
+ "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
210
+
211
+ "openai": ["openai@4.104.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" }, "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA=="],
212
+
213
+ "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
214
+
215
+ "path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="],
216
+
217
+ "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
218
+
219
+ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
220
+
221
+ "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
222
+
223
+ "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
224
+
225
+ "raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="],
226
+
227
+ "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
228
+
229
+ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
230
+
231
+ "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
232
+
233
+ "send": ["send@0.19.1", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg=="],
234
+
235
+ "serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="],
236
+
237
+ "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
238
+
239
+ "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
240
+
241
+ "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
242
+
243
+ "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
244
+
245
+ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
246
+
247
+ "sparse-bitfield": ["sparse-bitfield@3.0.3", "", { "dependencies": { "memory-pager": "^1.0.2" } }, "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ=="],
248
+
249
+ "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
250
+
251
+ "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
252
+
253
+ "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
254
+
255
+ "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
256
+
257
+ "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="],
258
+
259
+ "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
260
+
261
+ "undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
262
+
263
+ "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
264
+
265
+ "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
266
+
267
+ "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
268
+
269
+ "web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="],
270
+
271
+ "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="],
272
+
273
+ "whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="],
274
+
275
+ "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="],
276
+
277
+ "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
278
+
279
+ "debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
280
+
281
+ "node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
282
+
283
+ "send/http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
284
+
285
+ "send/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
286
+
287
+ "serve-static/send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="],
288
+
289
+ "node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
290
+
291
+ "node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
292
+
293
+ "serve-static/send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
294
+
295
+ "serve-static/send/http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
296
+
297
+ "serve-static/send/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
298
+ }
299
+ }
db/mongo.js ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { MongoClient, ObjectId } from 'mongodb';
2
+ import dotenv from 'dotenv';
3
+
4
+ dotenv.config();
5
+
6
+ const MONGO_URI = process.env.MONGO_URI;
7
+
8
+ let client;
9
+ let db;
10
+
11
+ // Collections
12
+ let patientsCollection;
13
+ let usersCollection;
14
+ let appointmentsCollection;
15
+ let warningsCollection;
16
+
17
+ export const connectDB = async () => {
18
+ try {
19
+ client = new MongoClient(MONGO_URI, {
20
+ serverSelectionTimeoutMS: 5000
21
+ });
22
+
23
+ await client.connect();
24
+ await client.db('admin').command({ ping: 1 });
25
+ console.log('✓ Connected to MongoDB Atlas successfully!');
26
+
27
+ db = client.db('patient_info');
28
+
29
+ // Initialize collections
30
+ patientsCollection = db.collection('patients');
31
+ usersCollection = db.collection('users');
32
+ appointmentsCollection = db.collection('appointments');
33
+ warningsCollection = db.collection('clinical_warnings');
34
+
35
+ return db;
36
+ } catch (error) {
37
+ console.log(`⚠️ MongoDB connection warning: ${error.message}`);
38
+ console.log('The server will start, but database operations may fail.');
39
+ console.log('Please check:');
40
+ console.log(' 1. Your MongoDB Atlas IP whitelist (add your current IP)');
41
+ console.log(' 2. Your internet connection');
42
+ console.log(' 3. If your Atlas cluster is paused');
43
+
44
+ // Create client anyway for later retry
45
+ client = new MongoClient(MONGO_URI);
46
+ db = client.db('patient_info');
47
+
48
+ patientsCollection = db.collection('patients');
49
+ usersCollection = db.collection('users');
50
+ appointmentsCollection = db.collection('appointments');
51
+ warningsCollection = db.collection('clinical_warnings');
52
+
53
+ return db;
54
+ }
55
+ };
56
+
57
+ export const ensureIndexes = async () => {
58
+ try {
59
+ await usersCollection.createIndex({ email: 1 }, { unique: true });
60
+ console.log('✓ Database indexes created');
61
+ } catch (error) {
62
+ console.log(`⚠️ Could not create indexes: ${error.message}`);
63
+ }
64
+ };
65
+
66
+ export const getDB = () => db;
67
+ export const getPatientsCollection = () => patientsCollection;
68
+ export const getUsersCollection = () => usersCollection;
69
+ export const getAppointmentsCollection = () => appointmentsCollection;
70
+ export const getWarningsCollection = () => warningsCollection;
71
+
72
+ export { ObjectId };
73
+
docker-compose.yml ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ backend:
5
+ build: .
6
+ container_name: patients-express-backend
7
+ ports:
8
+ - "8000:8000"
9
+ environment:
10
+ - NODE_ENV=production
11
+ - PORT=8000
12
+ - MONGO_URI=${MONGO_URI}
13
+ - JWT_SECRET_KEY=${JWT_SECRET_KEY}
14
+ - HF_TOKEN=${HF_TOKEN}
15
+ volumes:
16
+ - chroma_data:/app/chroma_db
17
+ restart: unless-stopped
18
+ healthcheck:
19
+ test: ["CMD", "node", "-e", "require('http').get('http://localhost:8000/', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"]
20
+ interval: 30s
21
+ timeout: 10s
22
+ retries: 3
23
+ start_period: 10s
24
+
25
+ volumes:
26
+ chroma_data:
27
+ driver: local
28
+
middleware/auth.js ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { authService } from '../services/authService.js';
2
+
3
+ // User roles enum
4
+ export const UserRole = {
5
+ DOCTOR: 'doctor',
6
+ PATIENT: 'patient'
7
+ };
8
+
9
+ /**
10
+ * Middleware to get the current authenticated user from JWT token
11
+ */
12
+ export const getCurrentUser = async (req, res, next) => {
13
+ try {
14
+ const authHeader = req.headers.authorization;
15
+
16
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
17
+ return res.status(401).json({
18
+ detail: 'Could not validate credentials',
19
+ headers: { 'WWW-Authenticate': 'Bearer' }
20
+ });
21
+ }
22
+
23
+ const token = authHeader.split(' ')[1];
24
+ const tokenData = authService.decodeToken(token);
25
+
26
+ if (!tokenData) {
27
+ return res.status(401).json({
28
+ detail: 'Could not validate credentials',
29
+ headers: { 'WWW-Authenticate': 'Bearer' }
30
+ });
31
+ }
32
+
33
+ const user = await authService.getUserById(tokenData.userId);
34
+
35
+ if (!user) {
36
+ return res.status(401).json({
37
+ detail: 'Could not validate credentials',
38
+ headers: { 'WWW-Authenticate': 'Bearer' }
39
+ });
40
+ }
41
+
42
+ if (!user.is_active) {
43
+ return res.status(403).json({
44
+ detail: 'User account is disabled'
45
+ });
46
+ }
47
+
48
+ req.user = user;
49
+ next();
50
+ } catch (error) {
51
+ return res.status(401).json({
52
+ detail: 'Could not validate credentials'
53
+ });
54
+ }
55
+ };
56
+
57
+ /**
58
+ * Middleware to ensure the current user is active
59
+ */
60
+ export const getCurrentActiveUser = async (req, res, next) => {
61
+ await getCurrentUser(req, res, () => {
62
+ if (!req.user.is_active) {
63
+ return res.status(403).json({
64
+ detail: 'Inactive user'
65
+ });
66
+ }
67
+ next();
68
+ });
69
+ };
70
+
71
+ /**
72
+ * Factory function to require specific roles
73
+ */
74
+ export const requireRoles = (allowedRoles) => {
75
+ return async (req, res, next) => {
76
+ await getCurrentUser(req, res, () => {
77
+ const userRole = req.user.role || 'patient';
78
+
79
+ if (!allowedRoles.includes(userRole)) {
80
+ return res.status(403).json({
81
+ detail: `Access denied. Required roles: ${allowedRoles.join(', ')}`
82
+ });
83
+ }
84
+ next();
85
+ });
86
+ };
87
+ };
88
+
89
+ // Pre-built role middlewares for common use cases
90
+ export const requireDoctor = requireRoles([UserRole.DOCTOR]);
91
+ export const requireStaff = requireRoles([UserRole.DOCTOR]);
92
+
93
+ /**
94
+ * Optional authentication - doesn't fail if no token provided
95
+ */
96
+ export const optionalAuth = async (req, res, next) => {
97
+ try {
98
+ const authHeader = req.headers.authorization;
99
+
100
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
101
+ req.user = null;
102
+ return next();
103
+ }
104
+
105
+ const token = authHeader.split(' ')[1];
106
+ const tokenData = authService.decodeToken(token);
107
+
108
+ if (!tokenData) {
109
+ req.user = null;
110
+ return next();
111
+ }
112
+
113
+ const user = await authService.getUserById(tokenData.userId);
114
+ req.user = user || null;
115
+ next();
116
+ } catch (error) {
117
+ req.user = null;
118
+ next();
119
+ }
120
+ };
121
+
package.json ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "patients-backend-express",
3
+ "version": "1.0.0",
4
+ "description": "Personalized Patient Information System - Express.js Backend",
5
+ "main": "app.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "start": "node app.js",
9
+ "dev": "node --watch app.js"
10
+ },
11
+ "keywords": [
12
+ "healthcare",
13
+ "patient",
14
+ "medical",
15
+ "express",
16
+ "mongodb"
17
+ ],
18
+ "author": "",
19
+ "license": "ISC",
20
+ "dependencies": {
21
+ "express": "^4.18.2",
22
+ "cors": "^2.8.5",
23
+ "dotenv": "^16.3.1",
24
+ "mongodb": "^6.3.0",
25
+ "jsonwebtoken": "^9.0.2",
26
+ "bcryptjs": "^2.4.3",
27
+ "openai": "^4.24.0",
28
+ "chromadb": "^1.9.2"
29
+ }
30
+ }
31
+
routes/appointments.js ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express';
2
+ import { ObjectId } from '../db/mongo.js';
3
+ import { appointmentService } from '../services/appointmentService.js';
4
+ import { getCurrentUser, requireStaff } from '../middleware/auth.js';
5
+
6
+ const router = express.Router();
7
+
8
+ /**
9
+ * Validate ObjectId format
10
+ */
11
+ const isValidObjectId = (id) => {
12
+ try {
13
+ return ObjectId.isValid(id) && new ObjectId(id).toString() === id;
14
+ } catch {
15
+ return false;
16
+ }
17
+ };
18
+
19
+ /**
20
+ * POST /api/appointments
21
+ * Create a new appointment
22
+ */
23
+ router.post('/appointments', requireStaff, async (req, res) => {
24
+ try {
25
+ const userId = req.user._id.toString();
26
+ const appointmentData = req.body;
27
+
28
+ // If no doctor_id specified, use current user as doctor
29
+ if (!appointmentData.doctor_id) {
30
+ appointmentData.doctor_id = userId;
31
+ }
32
+
33
+ // Check for conflicts if doctor is assigned
34
+ if (appointmentData.doctor_id) {
35
+ const conflicts = await appointmentService.checkConflicts(
36
+ appointmentData.doctor_id,
37
+ appointmentData.date_time,
38
+ appointmentData.duration_minutes || 30
39
+ );
40
+
41
+ if (conflicts.length > 0) {
42
+ return res.status(409).json({
43
+ detail: {
44
+ message: 'Scheduling conflict detected',
45
+ conflicts
46
+ }
47
+ });
48
+ }
49
+ }
50
+
51
+ const appointment = await appointmentService.createAppointment(appointmentData, userId);
52
+ res.status(201).json(appointment);
53
+ } catch (error) {
54
+ res.status(500).json({ detail: `Error creating appointment: ${error.message}` });
55
+ }
56
+ });
57
+
58
+ /**
59
+ * GET /api/appointments
60
+ * List appointments with optional filters
61
+ */
62
+ router.get('/appointments', getCurrentUser, async (req, res) => {
63
+ try {
64
+ const { patient_id, doctor_id, status, from_date, to_date, limit = 50 } = req.query;
65
+
66
+ const appointments = await appointmentService.listAppointments({
67
+ patientId: patient_id,
68
+ doctorId: doctor_id,
69
+ status,
70
+ fromDate: from_date,
71
+ toDate: to_date,
72
+ limit: Math.min(parseInt(limit), 100)
73
+ });
74
+
75
+ res.json(appointments);
76
+ } catch (error) {
77
+ res.status(500).json({ detail: `Error fetching appointments: ${error.message}` });
78
+ }
79
+ });
80
+
81
+ /**
82
+ * GET /api/appointments/upcoming
83
+ * Get upcoming appointments
84
+ */
85
+ router.get('/appointments/upcoming', getCurrentUser, async (req, res) => {
86
+ try {
87
+ const { limit = 20 } = req.query;
88
+ const appointments = await appointmentService.getUpcomingAppointments(
89
+ Math.min(parseInt(limit), 100)
90
+ );
91
+ res.json(appointments);
92
+ } catch (error) {
93
+ res.status(500).json({ detail: `Error fetching appointments: ${error.message}` });
94
+ }
95
+ });
96
+
97
+ /**
98
+ * GET /api/appointments/stats
99
+ * Get appointment statistics
100
+ */
101
+ router.get('/appointments/stats', getCurrentUser, async (req, res) => {
102
+ try {
103
+ const stats = await appointmentService.getAppointmentStats();
104
+ res.json(stats);
105
+ } catch (error) {
106
+ res.status(500).json({ detail: `Error fetching stats: ${error.message}` });
107
+ }
108
+ });
109
+
110
+ /**
111
+ * GET /api/appointments/:appointmentId
112
+ * Get a specific appointment
113
+ */
114
+ router.get('/appointments/:appointmentId', getCurrentUser, async (req, res) => {
115
+ try {
116
+ const { appointmentId } = req.params;
117
+
118
+ if (!isValidObjectId(appointmentId)) {
119
+ return res.status(400).json({ detail: 'Invalid appointment ID format' });
120
+ }
121
+
122
+ const appointment = await appointmentService.getAppointment(appointmentId);
123
+
124
+ if (!appointment) {
125
+ return res.status(404).json({ detail: 'Appointment not found' });
126
+ }
127
+
128
+ res.json(appointment);
129
+ } catch (error) {
130
+ res.status(500).json({ detail: `Error fetching appointment: ${error.message}` });
131
+ }
132
+ });
133
+
134
+ /**
135
+ * PUT /api/appointments/:appointmentId
136
+ * Update an appointment
137
+ */
138
+ router.put('/appointments/:appointmentId', requireStaff, async (req, res) => {
139
+ try {
140
+ const { appointmentId } = req.params;
141
+ const updateData = req.body;
142
+
143
+ if (!isValidObjectId(appointmentId)) {
144
+ return res.status(400).json({ detail: 'Invalid appointment ID format' });
145
+ }
146
+
147
+ // Check for conflicts if time is being updated
148
+ const existing = await appointmentService.getAppointment(appointmentId);
149
+ if (!existing) {
150
+ return res.status(404).json({ detail: 'Appointment not found' });
151
+ }
152
+
153
+ if (updateData.date_time && existing.doctor_id) {
154
+ const conflicts = await appointmentService.checkConflicts(
155
+ existing.doctor_id,
156
+ updateData.date_time,
157
+ updateData.duration_minutes || existing.duration_minutes || 30,
158
+ appointmentId
159
+ );
160
+
161
+ if (conflicts.length > 0) {
162
+ return res.status(409).json({
163
+ detail: {
164
+ message: 'Scheduling conflict detected',
165
+ conflicts
166
+ }
167
+ });
168
+ }
169
+ }
170
+
171
+ const appointment = await appointmentService.updateAppointment(appointmentId, updateData);
172
+
173
+ if (!appointment) {
174
+ return res.status(404).json({ detail: 'Appointment not found' });
175
+ }
176
+
177
+ res.json(appointment);
178
+ } catch (error) {
179
+ res.status(500).json({ detail: `Error updating appointment: ${error.message}` });
180
+ }
181
+ });
182
+
183
+ /**
184
+ * POST /api/appointments/:appointmentId/cancel
185
+ * Cancel an appointment
186
+ */
187
+ router.post('/appointments/:appointmentId/cancel', requireStaff, async (req, res) => {
188
+ try {
189
+ const { appointmentId } = req.params;
190
+ const { reason } = req.body || {};
191
+
192
+ if (!isValidObjectId(appointmentId)) {
193
+ return res.status(400).json({ detail: 'Invalid appointment ID format' });
194
+ }
195
+
196
+ const appointment = await appointmentService.cancelAppointment(appointmentId, reason);
197
+
198
+ if (!appointment) {
199
+ return res.status(404).json({ detail: 'Appointment not found' });
200
+ }
201
+
202
+ res.json(appointment);
203
+ } catch (error) {
204
+ res.status(500).json({ detail: `Error cancelling appointment: ${error.message}` });
205
+ }
206
+ });
207
+
208
+ /**
209
+ * POST /api/appointments/:appointmentId/complete
210
+ * Mark an appointment as completed
211
+ */
212
+ router.post('/appointments/:appointmentId/complete', requireStaff, async (req, res) => {
213
+ try {
214
+ const { appointmentId } = req.params;
215
+ const { notes } = req.body || {};
216
+
217
+ if (!isValidObjectId(appointmentId)) {
218
+ return res.status(400).json({ detail: 'Invalid appointment ID format' });
219
+ }
220
+
221
+ const appointment = await appointmentService.completeAppointment(appointmentId, notes);
222
+
223
+ if (!appointment) {
224
+ return res.status(404).json({ detail: 'Appointment not found' });
225
+ }
226
+
227
+ res.json(appointment);
228
+ } catch (error) {
229
+ res.status(500).json({ detail: `Error completing appointment: ${error.message}` });
230
+ }
231
+ });
232
+
233
+ /**
234
+ * GET /api/appointments/:appointmentId/summary
235
+ * Generate an AI-powered pre-appointment summary
236
+ */
237
+ router.get('/appointments/:appointmentId/summary', requireStaff, async (req, res) => {
238
+ try {
239
+ const { appointmentId } = req.params;
240
+
241
+ if (!isValidObjectId(appointmentId)) {
242
+ return res.status(400).json({ detail: 'Invalid appointment ID format' });
243
+ }
244
+
245
+ const summary = await appointmentService.generatePreAppointmentSummary(appointmentId);
246
+
247
+ if (summary.error) {
248
+ return res.status(500).json({ detail: summary.error });
249
+ }
250
+
251
+ res.json(summary);
252
+ } catch (error) {
253
+ res.status(500).json({ detail: `Error generating summary: ${error.message}` });
254
+ }
255
+ });
256
+
257
+ /**
258
+ * GET /api/patients/:patientId/appointments
259
+ * Get all appointments for a specific patient
260
+ */
261
+ router.get('/patients/:patientId/appointments', getCurrentUser, async (req, res) => {
262
+ try {
263
+ const { patientId } = req.params;
264
+ const { include_past = 'false' } = req.query;
265
+
266
+ if (!isValidObjectId(patientId)) {
267
+ return res.status(400).json({ detail: 'Invalid patient ID format' });
268
+ }
269
+
270
+ const appointments = await appointmentService.getPatientAppointments(
271
+ patientId,
272
+ include_past === 'true'
273
+ );
274
+
275
+ res.json(appointments);
276
+ } catch (error) {
277
+ res.status(500).json({ detail: `Error fetching appointments: ${error.message}` });
278
+ }
279
+ });
280
+
281
+ export default router;
282
+
routes/auth.js ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express';
2
+ import { authService, ACCESS_TOKEN_EXPIRE_MINUTES } from '../services/authService.js';
3
+ import { getCurrentUser, requireDoctor, UserRole } from '../middleware/auth.js';
4
+ import { ObjectId, getUsersCollection } from '../db/mongo.js';
5
+
6
+ const router = express.Router();
7
+
8
+ /**
9
+ * POST /api/auth/register
10
+ * Register a new user account
11
+ */
12
+ router.post('/register', async (req, res) => {
13
+ try {
14
+ const { email, name, password, role = 'doctor' } = req.body;
15
+
16
+ // Validation
17
+ if (!email || !name || !password) {
18
+ return res.status(400).json({ detail: 'Email, name, and password are required' });
19
+ }
20
+
21
+ if (password.length < 6) {
22
+ return res.status(400).json({ detail: 'Password must be at least 6 characters' });
23
+ }
24
+
25
+ // Validate role - only doctor and patient allowed
26
+ const validRoles = [UserRole.DOCTOR, UserRole.PATIENT];
27
+ if (!validRoles.includes(role)) {
28
+ return res.status(400).json({ detail: 'Invalid role. Must be "doctor" or "patient"' });
29
+ }
30
+
31
+ const user = await authService.createUser({ email, name, password, role });
32
+ res.status(201).json(authService.userToResponse(user));
33
+ } catch (error) {
34
+ if (error.message.includes('already exists')) {
35
+ return res.status(400).json({ detail: error.message });
36
+ }
37
+ res.status(500).json({ detail: `Error creating user: ${error.message}` });
38
+ }
39
+ });
40
+
41
+ /**
42
+ * POST /api/auth/login
43
+ * Authenticate and receive a JWT token
44
+ */
45
+ router.post('/login', async (req, res) => {
46
+ try {
47
+ const { email, password } = req.body;
48
+
49
+ if (!email || !password) {
50
+ return res.status(400).json({ detail: 'Email and password are required' });
51
+ }
52
+
53
+ const user = await authService.authenticateUser(email, password);
54
+
55
+ if (!user) {
56
+ return res.status(401).json({
57
+ detail: 'Incorrect email or password',
58
+ headers: { 'WWW-Authenticate': 'Bearer' }
59
+ });
60
+ }
61
+
62
+ if (!user.is_active) {
63
+ return res.status(403).json({ detail: 'User account is disabled' });
64
+ }
65
+
66
+ // Create access token
67
+ const accessToken = authService.createAccessToken({
68
+ sub: user._id.toString(),
69
+ email: user.email,
70
+ role: user.role
71
+ });
72
+
73
+ res.json({
74
+ access_token: accessToken,
75
+ token_type: 'bearer'
76
+ });
77
+ } catch (error) {
78
+ res.status(500).json({ detail: `Error during login: ${error.message}` });
79
+ }
80
+ });
81
+
82
+ /**
83
+ * GET /api/auth/me
84
+ * Get the current authenticated user's profile
85
+ */
86
+ router.get('/me', getCurrentUser, async (req, res) => {
87
+ res.json(authService.userToResponse(req.user));
88
+ });
89
+
90
+ /**
91
+ * PUT /api/auth/me
92
+ * Update the current authenticated user's profile
93
+ */
94
+ router.put('/me', getCurrentUser, async (req, res) => {
95
+ try {
96
+ const { name, role } = req.body;
97
+ const updateData = {};
98
+
99
+ if (name) updateData.name = name;
100
+ if (role) updateData.role = role;
101
+
102
+ const updatedUser = await authService.updateUser(req.user._id.toString(), updateData);
103
+
104
+ if (!updatedUser) {
105
+ return res.status(404).json({ detail: 'User not found' });
106
+ }
107
+
108
+ res.json(authService.userToResponse(updatedUser));
109
+ } catch (error) {
110
+ res.status(500).json({ detail: `Error updating user: ${error.message}` });
111
+ }
112
+ });
113
+
114
+ /**
115
+ * POST /api/auth/logout
116
+ * Logout the current user
117
+ */
118
+ router.post('/logout', getCurrentUser, async (req, res) => {
119
+ res.json({
120
+ message: 'Successfully logged out',
121
+ user_id: req.user._id.toString()
122
+ });
123
+ });
124
+
125
+ /**
126
+ * GET /api/auth/users
127
+ * List all users (Doctor only)
128
+ */
129
+ router.get('/users', requireDoctor, async (req, res) => {
130
+ try {
131
+ const usersCollection = getUsersCollection();
132
+ const users = await usersCollection.find().toArray();
133
+ res.json(users.map(user => authService.userToResponse(user)));
134
+ } catch (error) {
135
+ res.status(500).json({ detail: `Error fetching users: ${error.message}` });
136
+ }
137
+ });
138
+
139
+ /**
140
+ * DELETE /api/auth/users/:userId
141
+ * Delete a user (Doctor only)
142
+ */
143
+ router.delete('/users/:userId', requireDoctor, async (req, res) => {
144
+ try {
145
+ const { userId } = req.params;
146
+
147
+ // Prevent self-deletion
148
+ if (req.user._id.toString() === userId) {
149
+ return res.status(400).json({ detail: 'Cannot delete your own account' });
150
+ }
151
+
152
+ const usersCollection = getUsersCollection();
153
+ const result = await usersCollection.deleteOne({ _id: new ObjectId(userId) });
154
+
155
+ if (result.deletedCount === 0) {
156
+ return res.status(404).json({ detail: 'User not found' });
157
+ }
158
+
159
+ res.json({ message: 'User deleted successfully', id: userId });
160
+ } catch (error) {
161
+ res.status(500).json({ detail: `Error deleting user: ${error.message}` });
162
+ }
163
+ });
164
+
165
+ export default router;
166
+
routes/patients.js ADDED
@@ -0,0 +1,431 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express';
2
+ import { ObjectId, getPatientsCollection } from '../db/mongo.js';
3
+ import { summarizePatient } from '../services/aiService.js';
4
+ import { ragService } from '../services/ragService.js';
5
+ import { vectorService } from '../services/vectorService.js';
6
+ import { timelineService } from '../services/timelineService.js';
7
+
8
+ const router = express.Router();
9
+
10
+ /**
11
+ * Helper to format patient document for response
12
+ */
13
+ const patientHelper = (patient) => ({
14
+ _id: patient._id.toString(),
15
+ name: patient.name || '',
16
+ age: patient.age || 0,
17
+ gender: patient.gender || '',
18
+ medical_history: patient.medical_history || [],
19
+ medications: patient.medications || [],
20
+ date_of_birth: patient.date_of_birth || null,
21
+ allergies: patient.allergies || [],
22
+ blood_type: patient.blood_type || null,
23
+ notes: patient.notes || null
24
+ });
25
+
26
+ /**
27
+ * Validate ObjectId format
28
+ */
29
+ const isValidObjectId = (id) => {
30
+ try {
31
+ return ObjectId.isValid(id) && new ObjectId(id).toString() === id;
32
+ } catch {
33
+ return false;
34
+ }
35
+ };
36
+
37
+ /**
38
+ * POST /api/patients/
39
+ * Create a new patient
40
+ */
41
+ router.post('/patients/', async (req, res) => {
42
+ try {
43
+ const patientsCollection = getPatientsCollection();
44
+ const patientData = req.body;
45
+
46
+ // Validation
47
+ if (!patientData.name || patientData.name.length < 1) {
48
+ return res.status(400).json({ detail: 'Name is required' });
49
+ }
50
+ if (patientData.age === undefined || patientData.age < 0 || patientData.age > 150) {
51
+ return res.status(400).json({ detail: 'Valid age (0-150) is required' });
52
+ }
53
+
54
+ const result = await patientsCollection.insertOne(patientData);
55
+ const patientId = result.insertedId.toString();
56
+
57
+ // Index patient data in vector database
58
+ try {
59
+ await vectorService.addPatientRecord(patientId, patientData);
60
+ } catch (error) {
61
+ console.log(`Warning: Failed to index patient in vector DB: ${error.message}`);
62
+ }
63
+
64
+ res.status(201).json({ id: patientId, message: 'Patient created successfully' });
65
+ } catch (error) {
66
+ res.status(400).json({ detail: `Error creating patient: ${error.message}` });
67
+ }
68
+ });
69
+
70
+ /**
71
+ * GET /api/patients/
72
+ * List all patients
73
+ */
74
+ router.get('/patients/', async (req, res) => {
75
+ try {
76
+ const patientsCollection = getPatientsCollection();
77
+ const patients = await patientsCollection.find().toArray();
78
+ res.json(patients.map(patientHelper));
79
+ } catch (error) {
80
+ res.status(500).json({ detail: `Error fetching patients: ${error.message}` });
81
+ }
82
+ });
83
+
84
+ /**
85
+ * GET /api/patients/search/semantic
86
+ * Semantic search across patients
87
+ */
88
+ router.get('/patients/search/semantic', async (req, res) => {
89
+ try {
90
+ const { query, limit = 5 } = req.query;
91
+
92
+ if (!query) {
93
+ return res.status(400).json({ detail: 'Query parameter is required' });
94
+ }
95
+
96
+ const results = await vectorService.searchPatientRecords(query, null, parseInt(limit));
97
+ res.json({
98
+ query,
99
+ results,
100
+ count: results.length
101
+ });
102
+ } catch (error) {
103
+ res.status(500).json({ detail: `Error performing semantic search: ${error.message}` });
104
+ }
105
+ });
106
+
107
+ /**
108
+ * GET /api/patients/:patientId
109
+ * Get a specific patient
110
+ */
111
+ router.get('/patients/:patientId', async (req, res) => {
112
+ try {
113
+ const { patientId } = req.params;
114
+
115
+ if (!isValidObjectId(patientId)) {
116
+ return res.status(400).json({ detail: 'Invalid patient ID format' });
117
+ }
118
+
119
+ const patientsCollection = getPatientsCollection();
120
+ const patient = await patientsCollection.findOne({ _id: new ObjectId(patientId) });
121
+
122
+ if (!patient) {
123
+ return res.status(404).json({ detail: 'Patient not found' });
124
+ }
125
+
126
+ res.json(patientHelper(patient));
127
+ } catch (error) {
128
+ res.status(500).json({ detail: `Error fetching patient: ${error.message}` });
129
+ }
130
+ });
131
+
132
+ /**
133
+ * PUT /api/patients/:patientId
134
+ * Update a patient
135
+ */
136
+ router.put('/patients/:patientId', async (req, res) => {
137
+ try {
138
+ const { patientId } = req.params;
139
+
140
+ if (!isValidObjectId(patientId)) {
141
+ return res.status(400).json({ detail: 'Invalid patient ID format' });
142
+ }
143
+
144
+ const patientsCollection = getPatientsCollection();
145
+ const updateData = req.body;
146
+
147
+ const result = await patientsCollection.updateOne(
148
+ { _id: new ObjectId(patientId) },
149
+ { $set: updateData }
150
+ );
151
+
152
+ if (result.matchedCount === 0) {
153
+ return res.status(404).json({ detail: 'Patient not found' });
154
+ }
155
+
156
+ const updatedPatient = await patientsCollection.findOne({ _id: new ObjectId(patientId) });
157
+
158
+ // Re-index patient data in vector database
159
+ try {
160
+ await vectorService.deletePatientRecords(patientId);
161
+ await vectorService.addPatientRecord(patientId, patientHelper(updatedPatient));
162
+ } catch (error) {
163
+ console.log(`Warning: Failed to re-index patient in vector DB: ${error.message}`);
164
+ }
165
+
166
+ res.json(patientHelper(updatedPatient));
167
+ } catch (error) {
168
+ res.status(500).json({ detail: `Error updating patient: ${error.message}` });
169
+ }
170
+ });
171
+
172
+ /**
173
+ * DELETE /api/patients/:patientId
174
+ * Delete a patient
175
+ */
176
+ router.delete('/patients/:patientId', async (req, res) => {
177
+ try {
178
+ const { patientId } = req.params;
179
+
180
+ if (!isValidObjectId(patientId)) {
181
+ return res.status(400).json({ detail: 'Invalid patient ID format' });
182
+ }
183
+
184
+ const patientsCollection = getPatientsCollection();
185
+ const result = await patientsCollection.deleteOne({ _id: new ObjectId(patientId) });
186
+
187
+ if (result.deletedCount === 0) {
188
+ return res.status(404).json({ detail: 'Patient not found' });
189
+ }
190
+
191
+ res.json({ message: 'Patient deleted successfully', id: patientId });
192
+ } catch (error) {
193
+ res.status(500).json({ detail: `Error deleting patient: ${error.message}` });
194
+ }
195
+ });
196
+
197
+ /**
198
+ * GET /api/patients/:patientId/summary
199
+ * Get AI-generated patient summary
200
+ */
201
+ router.get('/patients/:patientId/summary', async (req, res) => {
202
+ try {
203
+ const { patientId } = req.params;
204
+ const { query } = req.query;
205
+
206
+ if (!query) {
207
+ return res.status(400).json({ detail: 'Query parameter is required' });
208
+ }
209
+
210
+ if (!isValidObjectId(patientId)) {
211
+ return res.status(400).json({ detail: 'Invalid patient ID format' });
212
+ }
213
+
214
+ const patientsCollection = getPatientsCollection();
215
+ const patient = await patientsCollection.findOne({ _id: new ObjectId(patientId) });
216
+
217
+ if (!patient) {
218
+ return res.status(404).json({ detail: 'Patient not found' });
219
+ }
220
+
221
+ const summary = await summarizePatient(patient, query);
222
+ res.json(summary);
223
+ } catch (error) {
224
+ res.status(500).json({ detail: `Error generating summary: ${error.message}` });
225
+ }
226
+ });
227
+
228
+ /**
229
+ * GET /api/patients/:patientId/rag-summary
230
+ * Get RAG-enhanced patient summary
231
+ */
232
+ router.get('/patients/:patientId/rag-summary', async (req, res) => {
233
+ try {
234
+ const { patientId } = req.params;
235
+ const { query } = req.query;
236
+
237
+ if (!query) {
238
+ return res.status(400).json({ detail: 'Query parameter is required' });
239
+ }
240
+
241
+ if (!isValidObjectId(patientId)) {
242
+ return res.status(400).json({ detail: 'Invalid patient ID format' });
243
+ }
244
+
245
+ const patientsCollection = getPatientsCollection();
246
+ const patient = await patientsCollection.findOne({ _id: new ObjectId(patientId) });
247
+
248
+ if (!patient) {
249
+ return res.status(404).json({ detail: 'Patient not found' });
250
+ }
251
+
252
+ patient._id = patient._id.toString();
253
+ const summary = await ragService.generateRAGResponse(patientId, patient, query);
254
+ res.json(summary);
255
+ } catch (error) {
256
+ res.status(500).json({ detail: `Error generating RAG summary: ${error.message}` });
257
+ }
258
+ });
259
+
260
+ /**
261
+ * GET /api/patients/:patientId/suggest-treatment
262
+ * Get treatment suggestions for a condition
263
+ */
264
+ router.get('/patients/:patientId/suggest-treatment', async (req, res) => {
265
+ try {
266
+ const { patientId } = req.params;
267
+ const { condition } = req.query;
268
+
269
+ if (!condition) {
270
+ return res.status(400).json({ detail: 'Condition parameter is required' });
271
+ }
272
+
273
+ if (!isValidObjectId(patientId)) {
274
+ return res.status(400).json({ detail: 'Invalid patient ID format' });
275
+ }
276
+
277
+ const patientsCollection = getPatientsCollection();
278
+ const patient = await patientsCollection.findOne({ _id: new ObjectId(patientId) });
279
+
280
+ if (!patient) {
281
+ return res.status(404).json({ detail: 'Patient not found' });
282
+ }
283
+
284
+ patient._id = patient._id.toString();
285
+ const suggestions = await ragService.suggestTreatments(patientId, patient, condition);
286
+ res.json(suggestions);
287
+ } catch (error) {
288
+ res.status(500).json({ detail: `Error suggesting treatment: ${error.message}` });
289
+ }
290
+ });
291
+
292
+ /**
293
+ * GET /api/patients/:patientId/drug-interactions
294
+ * Analyze drug interactions for new medication
295
+ */
296
+ router.get('/patients/:patientId/drug-interactions', async (req, res) => {
297
+ try {
298
+ const { patientId } = req.params;
299
+ const { new_medication } = req.query;
300
+
301
+ if (!new_medication) {
302
+ return res.status(400).json({ detail: 'new_medication parameter is required' });
303
+ }
304
+
305
+ if (!isValidObjectId(patientId)) {
306
+ return res.status(400).json({ detail: 'Invalid patient ID format' });
307
+ }
308
+
309
+ const patientsCollection = getPatientsCollection();
310
+ const patient = await patientsCollection.findOne({ _id: new ObjectId(patientId) });
311
+
312
+ if (!patient) {
313
+ return res.status(404).json({ detail: 'Patient not found' });
314
+ }
315
+
316
+ patient._id = patient._id.toString();
317
+ const analysis = await ragService.analyzeDrugInteractions(patientId, patient, new_medication);
318
+ res.json(analysis);
319
+ } catch (error) {
320
+ res.status(500).json({ detail: `Error analyzing drug interactions: ${error.message}` });
321
+ }
322
+ });
323
+
324
+ /**
325
+ * GET /api/vector-db/stats
326
+ * Get vector database statistics
327
+ */
328
+ router.get('/vector-db/stats', async (req, res) => {
329
+ try {
330
+ const stats = await vectorService.getCollectionStats();
331
+ res.json(stats);
332
+ } catch (error) {
333
+ res.status(500).json({ detail: `Error fetching stats: ${error.message}` });
334
+ }
335
+ });
336
+
337
+ /**
338
+ * POST /api/vector-db/reindex
339
+ * Reindex all patients into the vector database
340
+ */
341
+ router.post('/vector-db/reindex', async (req, res) => {
342
+ try {
343
+ const patientsCollection = getPatientsCollection();
344
+ const patients = await patientsCollection.find().toArray();
345
+ let indexedCount = 0;
346
+ const errors = [];
347
+
348
+ for (const patient of patients) {
349
+ try {
350
+ const patientId = patient._id.toString();
351
+ const patientData = patientHelper(patient);
352
+ await vectorService.deletePatientRecords(patientId);
353
+ await vectorService.addPatientRecord(patientId, patientData);
354
+ indexedCount++;
355
+ } catch (error) {
356
+ errors.push(`${patient.name || 'Unknown'}: ${error.message}`);
357
+ }
358
+ }
359
+
360
+ res.json({
361
+ message: `Reindexed ${indexedCount} patients`,
362
+ total_patients: patients.length,
363
+ indexed: indexedCount,
364
+ errors: errors.length > 0 ? errors : null
365
+ });
366
+ } catch (error) {
367
+ res.status(500).json({ detail: `Error reindexing: ${error.message}` });
368
+ }
369
+ });
370
+
371
+ // Timeline Routes
372
+
373
+ /**
374
+ * GET /api/patients/:patientId/timeline
375
+ * Get the complete timeline of events for a patient
376
+ */
377
+ router.get('/patients/:patientId/timeline', async (req, res) => {
378
+ try {
379
+ const { patientId } = req.params;
380
+
381
+ if (!isValidObjectId(patientId)) {
382
+ return res.status(400).json({ detail: 'Invalid patient ID format' });
383
+ }
384
+
385
+ const timeline = await timelineService.getPatientTimeline(patientId);
386
+ res.json({ patient_id: patientId, events: timeline, count: timeline.length });
387
+ } catch (error) {
388
+ res.status(500).json({ detail: `Error fetching timeline: ${error.message}` });
389
+ }
390
+ });
391
+
392
+ /**
393
+ * GET /api/patients/:patientId/timeline/summary
394
+ * Get an AI-generated summary of the patient's medical timeline
395
+ */
396
+ router.get('/patients/:patientId/timeline/summary', async (req, res) => {
397
+ try {
398
+ const { patientId } = req.params;
399
+
400
+ if (!isValidObjectId(patientId)) {
401
+ return res.status(400).json({ detail: 'Invalid patient ID format' });
402
+ }
403
+
404
+ const summary = await timelineService.generateTimelineSummary(patientId);
405
+ res.json(summary);
406
+ } catch (error) {
407
+ res.status(500).json({ detail: `Error generating timeline summary: ${error.message}` });
408
+ }
409
+ });
410
+
411
+ /**
412
+ * GET /api/patients/:patientId/timeline/stats
413
+ * Get statistics about the patient's timeline
414
+ */
415
+ router.get('/patients/:patientId/timeline/stats', async (req, res) => {
416
+ try {
417
+ const { patientId } = req.params;
418
+
419
+ if (!isValidObjectId(patientId)) {
420
+ return res.status(400).json({ detail: 'Invalid patient ID format' });
421
+ }
422
+
423
+ const stats = await timelineService.getTimelineStats(patientId);
424
+ res.json(stats);
425
+ } catch (error) {
426
+ res.status(500).json({ detail: `Error fetching timeline stats: ${error.message}` });
427
+ }
428
+ });
429
+
430
+ export default router;
431
+
routes/warnings.js ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express';
2
+ import { ObjectId } from '../db/mongo.js';
3
+ import { warningService } from '../services/warningService.js';
4
+ import { getCurrentUser, requireStaff } from '../middleware/auth.js';
5
+
6
+ const router = express.Router();
7
+
8
+ /**
9
+ * Validate ObjectId format
10
+ */
11
+ const isValidObjectId = (id) => {
12
+ try {
13
+ return ObjectId.isValid(id) && new ObjectId(id).toString() === id;
14
+ } catch {
15
+ return false;
16
+ }
17
+ };
18
+
19
+ /**
20
+ * GET /api/patients/:patientId/warnings
21
+ * Get all clinical warnings for a specific patient
22
+ */
23
+ router.get('/patients/:patientId/warnings', getCurrentUser, async (req, res) => {
24
+ try {
25
+ const { patientId } = req.params;
26
+ const { include_acknowledged = 'false' } = req.query;
27
+
28
+ if (!isValidObjectId(patientId)) {
29
+ return res.status(400).json({ detail: 'Invalid patient ID format' });
30
+ }
31
+
32
+ const warnings = await warningService.getPatientWarnings(
33
+ patientId,
34
+ include_acknowledged === 'true'
35
+ );
36
+
37
+ res.json(warnings);
38
+ } catch (error) {
39
+ res.status(500).json({ detail: `Error fetching warnings: ${error.message}` });
40
+ }
41
+ });
42
+
43
+ /**
44
+ * POST /api/patients/:patientId/warnings/analyze
45
+ * Trigger AI analysis to generate clinical warnings for a patient
46
+ */
47
+ router.post('/patients/:patientId/warnings/analyze', requireStaff, async (req, res) => {
48
+ try {
49
+ const { patientId } = req.params;
50
+ const { new_medication, new_condition } = req.body || {};
51
+
52
+ if (!isValidObjectId(patientId)) {
53
+ return res.status(400).json({ detail: 'Invalid patient ID format' });
54
+ }
55
+
56
+ const warnings = await warningService.analyzePatientForWarnings(
57
+ patientId,
58
+ new_medication,
59
+ new_condition
60
+ );
61
+
62
+ res.json(warnings);
63
+ } catch (error) {
64
+ res.status(500).json({ detail: `Error analyzing patient: ${error.message}` });
65
+ }
66
+ });
67
+
68
+ /**
69
+ * PUT /api/warnings/:warningId/acknowledge
70
+ * Acknowledge a clinical warning
71
+ */
72
+ router.put('/warnings/:warningId/acknowledge', requireStaff, async (req, res) => {
73
+ try {
74
+ const { warningId } = req.params;
75
+ const { notes } = req.body || {};
76
+
77
+ if (!isValidObjectId(warningId)) {
78
+ return res.status(400).json({ detail: 'Invalid warning ID format' });
79
+ }
80
+
81
+ const userId = req.user._id.toString();
82
+ const warning = await warningService.acknowledgeWarning(warningId, userId, notes);
83
+
84
+ if (!warning) {
85
+ return res.status(404).json({ detail: 'Warning not found' });
86
+ }
87
+
88
+ res.json(warning);
89
+ } catch (error) {
90
+ res.status(500).json({ detail: `Error acknowledging warning: ${error.message}` });
91
+ }
92
+ });
93
+
94
+ /**
95
+ * GET /api/warnings/unacknowledged
96
+ * Get all unacknowledged warnings across all patients
97
+ */
98
+ router.get('/warnings/unacknowledged', getCurrentUser, async (req, res) => {
99
+ try {
100
+ const { limit = 50 } = req.query;
101
+
102
+ const warnings = await warningService.getAllUnacknowledgedWarnings(
103
+ Math.min(parseInt(limit), 100)
104
+ );
105
+
106
+ res.json(warnings);
107
+ } catch (error) {
108
+ res.status(500).json({ detail: `Error fetching warnings: ${error.message}` });
109
+ }
110
+ });
111
+
112
+ /**
113
+ * GET /api/warnings/stats
114
+ * Get warning statistics
115
+ */
116
+ router.get('/warnings/stats', getCurrentUser, async (req, res) => {
117
+ try {
118
+ const stats = await warningService.getWarningStats();
119
+ res.json(stats);
120
+ } catch (error) {
121
+ res.status(500).json({ detail: `Error fetching stats: ${error.message}` });
122
+ }
123
+ });
124
+
125
+ /**
126
+ * DELETE /api/patients/:patientId/warnings
127
+ * Delete all warnings for a patient (Staff only)
128
+ */
129
+ router.delete('/patients/:patientId/warnings', requireStaff, async (req, res) => {
130
+ try {
131
+ const { patientId } = req.params;
132
+
133
+ if (!isValidObjectId(patientId)) {
134
+ return res.status(400).json({ detail: 'Invalid patient ID format' });
135
+ }
136
+
137
+ const deletedCount = await warningService.deletePatientWarnings(patientId);
138
+ res.json({ message: `Deleted ${deletedCount} warnings`, deleted_count: deletedCount });
139
+ } catch (error) {
140
+ res.status(500).json({ detail: `Error deleting warnings: ${error.message}` });
141
+ }
142
+ });
143
+
144
+ export default router;
145
+
services/aiService.js ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { generateResponse } from './llmClient.js';
2
+
3
+ /**
4
+ * Summarize patient data based on a query using LLM
5
+ */
6
+ export const summarizePatient = async (patientData, query) => {
7
+ const prompt = `
8
+ You are a medical assistant AI.
9
+ Analyze the following patient data and answer the query in a concise,
10
+ structured, and professional format.
11
+
12
+ Patient data: ${JSON.stringify(patientData)}
13
+ Question: ${query}
14
+ `;
15
+
16
+ try {
17
+ const content = await generateResponse(prompt);
18
+ return { content, role: 'assistant' };
19
+ } catch (error) {
20
+ return { content: `Error generating summary: ${error.message}`, error: true };
21
+ }
22
+ };
services/appointmentService.js ADDED
@@ -0,0 +1,419 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { generateResponse } from './llmClient.js';
2
+ import { ObjectId, getAppointmentsCollection, getPatientsCollection, getUsersCollection } from '../db/mongo.js';
3
+ import dotenv from 'dotenv';
4
+
5
+ dotenv.config();
6
+
7
+ // Appointment status enum
8
+ export const AppointmentStatus = {
9
+ SCHEDULED: 'scheduled',
10
+ CONFIRMED: 'confirmed',
11
+ IN_PROGRESS: 'in_progress',
12
+ COMPLETED: 'completed',
13
+ CANCELLED: 'cancelled',
14
+ NO_SHOW: 'no_show'
15
+ };
16
+
17
+ class AppointmentService {
18
+ /**
19
+ * Convert MongoDB appointment document to response format
20
+ */
21
+ _appointmentHelper(appointment) {
22
+ return {
23
+ id: appointment._id.toString(),
24
+ patient_id: appointment.patient_id,
25
+ doctor_id: appointment.doctor_id || null,
26
+ date_time: appointment.date_time,
27
+ duration_minutes: appointment.duration_minutes || 30,
28
+ appointment_type: appointment.appointment_type || 'checkup',
29
+ purpose: appointment.purpose || '',
30
+ notes: appointment.notes || null,
31
+ location: appointment.location || null,
32
+ status: appointment.status || 'scheduled',
33
+ created_at: appointment.created_at,
34
+ updated_at: appointment.updated_at,
35
+ created_by: appointment.created_by || '',
36
+ cancelled_reason: appointment.cancelled_reason || null,
37
+ patient_name: appointment.patient_name || null,
38
+ doctor_name: appointment.doctor_name || null
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Add patient and doctor names to appointment
44
+ */
45
+ async _enrichWithNames(appointment) {
46
+ const patientsCollection = getPatientsCollection();
47
+ const usersCollection = getUsersCollection();
48
+
49
+ // Get patient name
50
+ try {
51
+ const patient = await patientsCollection.findOne({ _id: new ObjectId(appointment.patient_id) });
52
+ appointment.patient_name = patient ? patient.name : null;
53
+ } catch {
54
+ appointment.patient_name = null;
55
+ }
56
+
57
+ // Get doctor name
58
+ if (appointment.doctor_id) {
59
+ try {
60
+ const doctor = await usersCollection.findOne({ _id: new ObjectId(appointment.doctor_id) });
61
+ appointment.doctor_name = doctor ? doctor.name : null;
62
+ } catch {
63
+ appointment.doctor_name = null;
64
+ }
65
+ }
66
+
67
+ return appointment;
68
+ }
69
+
70
+ /**
71
+ * Create a new appointment
72
+ */
73
+ async createAppointment(appointmentData, userId) {
74
+ const appointmentsCollection = getAppointmentsCollection();
75
+ const now = new Date();
76
+
77
+ const appointmentDoc = {
78
+ ...appointmentData,
79
+ status: AppointmentStatus.SCHEDULED,
80
+ created_at: now,
81
+ updated_at: now,
82
+ created_by: userId
83
+ };
84
+
85
+ const result = await appointmentsCollection.insertOne(appointmentDoc);
86
+ appointmentDoc._id = result.insertedId;
87
+ const enriched = await this._enrichWithNames(appointmentDoc);
88
+
89
+ return this._appointmentHelper(enriched);
90
+ }
91
+
92
+ /**
93
+ * Get a single appointment by ID
94
+ */
95
+ async getAppointment(appointmentId) {
96
+ try {
97
+ const appointmentsCollection = getAppointmentsCollection();
98
+ const appointment = await appointmentsCollection.findOne({ _id: new ObjectId(appointmentId) });
99
+
100
+ if (appointment) {
101
+ const enriched = await this._enrichWithNames(appointment);
102
+ return this._appointmentHelper(enriched);
103
+ }
104
+ return null;
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * List appointments with optional filters
112
+ */
113
+ async listAppointments({ patientId, doctorId, status, fromDate, toDate, limit = 50 }) {
114
+ const appointmentsCollection = getAppointmentsCollection();
115
+ const query = {};
116
+
117
+ if (patientId) query.patient_id = patientId;
118
+ if (doctorId) query.doctor_id = doctorId;
119
+ if (status) query.status = status;
120
+
121
+ if (fromDate || toDate) {
122
+ query.date_time = {};
123
+ if (fromDate) query.date_time.$gte = fromDate;
124
+ if (toDate) query.date_time.$lte = toDate;
125
+ }
126
+
127
+ const appointments = await appointmentsCollection
128
+ .find(query)
129
+ .sort({ date_time: 1 })
130
+ .limit(limit)
131
+ .toArray();
132
+
133
+ const result = [];
134
+ for (const apt of appointments) {
135
+ const enriched = await this._enrichWithNames(apt);
136
+ result.push(this._appointmentHelper(enriched));
137
+ }
138
+
139
+ return result;
140
+ }
141
+
142
+ /**
143
+ * Get upcoming appointments
144
+ */
145
+ async getUpcomingAppointments(limit = 20) {
146
+ const appointmentsCollection = getAppointmentsCollection();
147
+ const now = new Date().toISOString();
148
+
149
+ const appointments = await appointmentsCollection
150
+ .find({
151
+ date_time: { $gte: now },
152
+ status: { $in: ['scheduled', 'confirmed'] }
153
+ })
154
+ .sort({ date_time: 1 })
155
+ .limit(limit)
156
+ .toArray();
157
+
158
+ const result = [];
159
+ for (const apt of appointments) {
160
+ const enriched = await this._enrichWithNames(apt);
161
+ result.push(this._appointmentHelper(enriched));
162
+ }
163
+
164
+ return result;
165
+ }
166
+
167
+ /**
168
+ * Get all appointments for a specific patient
169
+ */
170
+ async getPatientAppointments(patientId, includePast = false) {
171
+ const appointmentsCollection = getAppointmentsCollection();
172
+ const query = { patient_id: patientId };
173
+
174
+ if (!includePast) {
175
+ const now = new Date().toISOString();
176
+ query.$or = [
177
+ { date_time: { $gte: now } },
178
+ { status: { $in: ['scheduled', 'confirmed'] } }
179
+ ];
180
+ }
181
+
182
+ const appointments = await appointmentsCollection
183
+ .find(query)
184
+ .sort({ date_time: -1 })
185
+ .toArray();
186
+
187
+ const result = [];
188
+ for (const apt of appointments) {
189
+ const enriched = await this._enrichWithNames(apt);
190
+ result.push(this._appointmentHelper(enriched));
191
+ }
192
+
193
+ return result;
194
+ }
195
+
196
+ /**
197
+ * Update an appointment
198
+ */
199
+ async updateAppointment(appointmentId, updateData) {
200
+ const appointmentsCollection = getAppointmentsCollection();
201
+ const updateDict = { ...updateData, updated_at: new Date() };
202
+
203
+ const result = await appointmentsCollection.updateOne(
204
+ { _id: new ObjectId(appointmentId) },
205
+ { $set: updateDict }
206
+ );
207
+
208
+ if (result.matchedCount === 0) {
209
+ return null;
210
+ }
211
+
212
+ return await this.getAppointment(appointmentId);
213
+ }
214
+
215
+ /**
216
+ * Cancel an appointment
217
+ */
218
+ async cancelAppointment(appointmentId, reason = null) {
219
+ const appointmentsCollection = getAppointmentsCollection();
220
+
221
+ const result = await appointmentsCollection.updateOne(
222
+ { _id: new ObjectId(appointmentId) },
223
+ {
224
+ $set: {
225
+ status: AppointmentStatus.CANCELLED,
226
+ cancelled_reason: reason,
227
+ updated_at: new Date()
228
+ }
229
+ }
230
+ );
231
+
232
+ if (result.matchedCount === 0) {
233
+ return null;
234
+ }
235
+
236
+ return await this.getAppointment(appointmentId);
237
+ }
238
+
239
+ /**
240
+ * Mark an appointment as completed
241
+ */
242
+ async completeAppointment(appointmentId, notes = null) {
243
+ const appointmentsCollection = getAppointmentsCollection();
244
+ const updateData = {
245
+ status: AppointmentStatus.COMPLETED,
246
+ updated_at: new Date()
247
+ };
248
+
249
+ if (notes) {
250
+ updateData.notes = notes;
251
+ }
252
+
253
+ const result = await appointmentsCollection.updateOne(
254
+ { _id: new ObjectId(appointmentId) },
255
+ { $set: updateData }
256
+ );
257
+
258
+ if (result.matchedCount === 0) {
259
+ return null;
260
+ }
261
+
262
+ return await this.getAppointment(appointmentId);
263
+ }
264
+
265
+ /**
266
+ * Check for scheduling conflicts
267
+ */
268
+ async checkConflicts(doctorId, dateTime, durationMinutes, excludeId = null) {
269
+ const appointmentsCollection = getAppointmentsCollection();
270
+
271
+ // Parse the proposed datetime
272
+ let proposedStart, proposedEnd;
273
+ try {
274
+ proposedStart = new Date(dateTime.replace('Z', '+00:00'));
275
+ proposedEnd = new Date(proposedStart.getTime() + durationMinutes * 60000);
276
+ } catch {
277
+ return [];
278
+ }
279
+
280
+ // Find overlapping appointments
281
+ const query = {
282
+ doctor_id: doctorId,
283
+ status: { $in: ['scheduled', 'confirmed'] }
284
+ };
285
+
286
+ if (excludeId) {
287
+ query._id = { $ne: new ObjectId(excludeId) };
288
+ }
289
+
290
+ const appointments = await appointmentsCollection.find(query).toArray();
291
+
292
+ const conflicts = [];
293
+ for (const apt of appointments) {
294
+ try {
295
+ const aptStart = new Date(apt.date_time.replace('Z', '+00:00'));
296
+ const aptEnd = new Date(aptStart.getTime() + (apt.duration_minutes || 30) * 60000);
297
+
298
+ // Check for overlap
299
+ if (!(proposedEnd <= aptStart || proposedStart >= aptEnd)) {
300
+ conflicts.push(this._appointmentHelper(apt));
301
+ }
302
+ } catch {
303
+ continue;
304
+ }
305
+ }
306
+
307
+ return conflicts;
308
+ }
309
+
310
+ /**
311
+ * Generate an AI-powered pre-appointment summary
312
+ */
313
+ async generatePreAppointmentSummary(appointmentId) {
314
+ const appointmentsCollection = getAppointmentsCollection();
315
+ const patientsCollection = getPatientsCollection();
316
+
317
+ const appointment = await appointmentsCollection.findOne({ _id: new ObjectId(appointmentId) });
318
+ if (!appointment) {
319
+ return { error: 'Appointment not found' };
320
+ }
321
+
322
+ // Get patient data
323
+ const patient = await patientsCollection.findOne({ _id: new ObjectId(appointment.patient_id) });
324
+ if (!patient) {
325
+ return { error: 'Patient not found' };
326
+ }
327
+
328
+ // Build context
329
+ const medicalHistory = patient.medical_history || [];
330
+ const historyText = medicalHistory.map(item => {
331
+ if (typeof item === 'string') return `- ${item}`;
332
+ return `- ${item.condition || 'Unknown'}`;
333
+ }).join('\n');
334
+
335
+ const medications = patient.medications || [];
336
+ const medsText = medications.map(med => {
337
+ if (typeof med === 'object') {
338
+ return `- ${med.name || 'Unknown'}: ${med.dosage || 'N/A'}`;
339
+ }
340
+ return `- ${med}`;
341
+ }).join('\n');
342
+
343
+ const prompt = `You are a medical AI assistant preparing a pre-appointment briefing for a doctor.
344
+
345
+ UPCOMING APPOINTMENT:
346
+ - Purpose: ${appointment.purpose || 'General checkup'}
347
+ - Type: ${appointment.appointment_type || 'checkup'}
348
+ - Notes: ${appointment.notes || 'None'}
349
+
350
+ PATIENT INFORMATION:
351
+ - Name: ${patient.name || 'Unknown'}
352
+ - Age: ${patient.age || 'Unknown'}
353
+ - Gender: ${patient.gender || 'Unknown'}
354
+ - Blood Type: ${patient.blood_type || 'Unknown'}
355
+ - Allergies: ${(patient.allergies || []).join(', ') || 'None recorded'}
356
+
357
+ MEDICAL HISTORY:
358
+ ${historyText || 'No medical history recorded'}
359
+
360
+ CURRENT MEDICATIONS:
361
+ ${medsText || 'No medications recorded'}
362
+
363
+ Please provide a concise pre-appointment briefing including:
364
+ 1. **Patient Overview**: Quick summary of the patient
365
+ 2. **Key Concerns**: Important medical factors to consider
366
+ 3. **Suggested Focus Areas**: What to examine based on history
367
+ 4. **Questions to Ask**: Relevant questions for this appointment type
368
+ 5. **Precautions**: Any special considerations
369
+
370
+ Keep it concise and actionable for a busy doctor.`;
371
+
372
+ try {
373
+ const summary = await generateResponse(prompt);
374
+
375
+ return {
376
+ appointment_id: appointmentId,
377
+ patient_name: patient.name,
378
+ purpose: appointment.purpose,
379
+ summary,
380
+ generated_at: new Date().toISOString()
381
+ };
382
+ } catch (error) {
383
+ return { error: `Error generating summary: ${error.message}` };
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Get appointment statistics
389
+ */
390
+ async getAppointmentStats() {
391
+ const appointmentsCollection = getAppointmentsCollection();
392
+
393
+ const pipeline = [
394
+ {
395
+ $group: {
396
+ _id: '$status',
397
+ count: { $sum: 1 }
398
+ }
399
+ }
400
+ ];
401
+
402
+ const results = await appointmentsCollection.aggregate(pipeline).toArray();
403
+
404
+ const stats = {
405
+ total: 0,
406
+ by_status: {}
407
+ };
408
+
409
+ for (const r of results) {
410
+ stats.by_status[r._id] = r.count;
411
+ stats.total += r.count;
412
+ }
413
+
414
+ return stats;
415
+ }
416
+ }
417
+
418
+ // Singleton instance
419
+ export const appointmentService = new AppointmentService();
services/authService.js ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import jwt from 'jsonwebtoken';
2
+ import bcrypt from 'bcryptjs';
3
+ import { ObjectId, getUsersCollection } from '../db/mongo.js';
4
+ import dotenv from 'dotenv';
5
+
6
+ dotenv.config();
7
+
8
+ const SECRET_KEY = process.env.JWT_SECRET_KEY || 'your-super-secret-key-change-in-production';
9
+ const ALGORITHM = 'HS256';
10
+ export const ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24; // 24 hours
11
+
12
+ class AuthService {
13
+ /**
14
+ * Verify a password against its hash
15
+ */
16
+ verifyPassword(plainPassword, hashedPassword) {
17
+ return bcrypt.compareSync(plainPassword, hashedPassword);
18
+ }
19
+
20
+ /**
21
+ * Hash a password
22
+ */
23
+ getPasswordHash(password) {
24
+ const salt = bcrypt.genSaltSync(10);
25
+ return bcrypt.hashSync(password, salt);
26
+ }
27
+
28
+ /**
29
+ * Create a JWT access token
30
+ */
31
+ createAccessToken(data, expiresInMinutes = ACCESS_TOKEN_EXPIRE_MINUTES) {
32
+ const payload = {
33
+ ...data,
34
+ exp: Math.floor(Date.now() / 1000) + (expiresInMinutes * 60)
35
+ };
36
+ return jwt.sign(payload, SECRET_KEY, { algorithm: ALGORITHM });
37
+ }
38
+
39
+ /**
40
+ * Decode and validate a JWT token
41
+ */
42
+ decodeToken(token) {
43
+ try {
44
+ const payload = jwt.verify(token, SECRET_KEY, { algorithms: [ALGORITHM] });
45
+ const userId = payload.sub;
46
+ const email = payload.email;
47
+ const role = payload.role;
48
+
49
+ if (!userId) {
50
+ return null;
51
+ }
52
+
53
+ return { userId, email, role };
54
+ } catch (error) {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Get a user by email
61
+ */
62
+ async getUserByEmail(email) {
63
+ const usersCollection = getUsersCollection();
64
+ return await usersCollection.findOne({ email });
65
+ }
66
+
67
+ /**
68
+ * Get a user by ID
69
+ */
70
+ async getUserById(userId) {
71
+ try {
72
+ const usersCollection = getUsersCollection();
73
+ return await usersCollection.findOne({ _id: new ObjectId(userId) });
74
+ } catch (error) {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Create a new user
81
+ */
82
+ async createUser(userData) {
83
+ const usersCollection = getUsersCollection();
84
+
85
+ // Check if user already exists
86
+ const existingUser = await this.getUserByEmail(userData.email);
87
+ if (existingUser) {
88
+ throw new Error('User with this email already exists');
89
+ }
90
+
91
+ // Create user document
92
+ const now = new Date();
93
+ const userDoc = {
94
+ email: userData.email,
95
+ name: userData.name,
96
+ role: userData.role || 'doctor',
97
+ hashed_password: this.getPasswordHash(userData.password),
98
+ created_at: now,
99
+ updated_at: now,
100
+ is_active: true
101
+ };
102
+
103
+ const result = await usersCollection.insertOne(userDoc);
104
+ userDoc._id = result.insertedId;
105
+ return userDoc;
106
+ }
107
+
108
+ /**
109
+ * Authenticate a user by email and password
110
+ */
111
+ async authenticateUser(email, password) {
112
+ const user = await this.getUserByEmail(email);
113
+ if (!user) {
114
+ return null;
115
+ }
116
+ if (!this.verifyPassword(password, user.hashed_password)) {
117
+ return null;
118
+ }
119
+ return user;
120
+ }
121
+
122
+ /**
123
+ * Convert a user document to a response object
124
+ */
125
+ userToResponse(user) {
126
+ return {
127
+ id: user._id.toString(),
128
+ email: user.email,
129
+ name: user.name,
130
+ role: user.role,
131
+ created_at: user.created_at,
132
+ is_active: user.is_active !== false
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Update a user's information
138
+ */
139
+ async updateUser(userId, updateData) {
140
+ const usersCollection = getUsersCollection();
141
+ updateData.updated_at = new Date();
142
+
143
+ const result = await usersCollection.updateOne(
144
+ { _id: new ObjectId(userId) },
145
+ { $set: updateData }
146
+ );
147
+
148
+ if (result.matchedCount === 0) {
149
+ return null;
150
+ }
151
+
152
+ return await this.getUserById(userId);
153
+ }
154
+ }
155
+
156
+ // Singleton instance
157
+ export const authService = new AuthService();
158
+
services/llmClient.js ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import OpenAI from 'openai';
2
+ import dotenv from 'dotenv';
3
+
4
+ dotenv.config();
5
+
6
+ const client = new OpenAI({
7
+ baseURL: 'https://router.huggingface.co/v1',
8
+ apiKey: process.env.HF_TOKEN
9
+ });
10
+
11
+ const MODEL = 'meta-llama/Llama-3.2-3B-Instruct:novita';
12
+
13
+ /**
14
+ * Generate a response using the Llama model
15
+ * @param {string} prompt - The prompt to send to the model
16
+ * @returns {Promise<string>} - The generated text response
17
+ */
18
+ export const generateResponse = async (prompt) => {
19
+ const completion = await client.chat.completions.create({
20
+ model: MODEL,
21
+ messages: [
22
+ {
23
+ role: 'user',
24
+ content: prompt
25
+ }
26
+ ]
27
+ });
28
+
29
+ return completion.choices[0].message.content;
30
+ };
31
+
32
+ /**
33
+ * Generate a response with system message
34
+ * @param {string} systemPrompt - The system message
35
+ * @param {string} userPrompt - The user message
36
+ * @returns {Promise<string>} - The generated text response
37
+ */
38
+ export const generateResponseWithSystem = async (systemPrompt, userPrompt) => {
39
+ const completion = await client.chat.completions.create({
40
+ model: MODEL,
41
+ messages: [
42
+ {
43
+ role: 'system',
44
+ content: systemPrompt
45
+ },
46
+ {
47
+ role: 'user',
48
+ content: userPrompt
49
+ }
50
+ ]
51
+ });
52
+
53
+ return completion.choices[0].message.content;
54
+ };
55
+
56
+ export { client, MODEL };
57
+
services/ragService.js ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { generateResponse } from './llmClient.js';
2
+ import { vectorService } from './vectorService.js';
3
+ import dotenv from 'dotenv';
4
+
5
+ dotenv.config();
6
+
7
+ class RAGService {
8
+ constructor() {
9
+ this.vectorService = vectorService;
10
+ }
11
+
12
+ async retrievePatientContext(patientId, query, nResults = 5) {
13
+ const patientResults = await this.vectorService.searchPatientRecords(
14
+ query,
15
+ patientId,
16
+ nResults
17
+ );
18
+ return { patient_records: patientResults };
19
+ }
20
+
21
+ async retrieveResearchContext(query, nResults = 3) {
22
+ return await this.vectorService.searchResearchPapers(query, nResults);
23
+ }
24
+
25
+ formatContextForLLM(patientContext, researchContext) {
26
+ const contextParts = [];
27
+
28
+ contextParts.push('=== PATIENT MEDICAL RECORDS ===');
29
+ if (patientContext.patient_records && patientContext.patient_records.length > 0) {
30
+ patientContext.patient_records.forEach((record, idx) => {
31
+ contextParts.push(`\n${idx + 1}. ${record.document}`);
32
+ contextParts.push(` Type: ${record.metadata.type || 'unknown'}`);
33
+ });
34
+ } else {
35
+ contextParts.push('No patient records found.');
36
+ }
37
+
38
+ contextParts.push('\n\n=== RELEVANT MEDICAL RESEARCH ===');
39
+ if (researchContext && researchContext.length > 0) {
40
+ researchContext.forEach((paper, idx) => {
41
+ contextParts.push(`\n${idx + 1}. Title: ${paper.metadata.title || 'Unknown'}`);
42
+ contextParts.push(` Content: ${paper.document}`);
43
+ });
44
+ } else {
45
+ contextParts.push('No relevant research found.');
46
+ }
47
+
48
+ return contextParts.join('\n');
49
+ }
50
+
51
+ async generateRAGResponse(patientId, patientData, query) {
52
+ const patientContext = await this.retrievePatientContext(patientId, query, 5);
53
+ const researchContext = await this.retrieveResearchContext(query, 3);
54
+ const formattedContext = this.formatContextForLLM(patientContext, researchContext);
55
+
56
+ const prompt = `
57
+ You are an expert medical AI assistant.
58
+
59
+ PATIENT:
60
+ - Name: ${patientData.name}
61
+ - Age: ${patientData.age}
62
+ - Gender: ${patientData.gender}
63
+
64
+ CONTEXT:
65
+ ${formattedContext}
66
+
67
+ DOCTOR'S QUESTION:
68
+ ${query}
69
+
70
+ Please give a medically accurate, evidence-based answer.
71
+ `;
72
+
73
+ try {
74
+ const text = await generateResponse(prompt);
75
+
76
+ return {
77
+ text,
78
+ patient_records_used: (patientContext.patient_records || []).length,
79
+ research_papers_used: researchContext.length,
80
+ sources: {
81
+ patient_records: (patientContext.patient_records || []).map(r => ({
82
+ type: r.metadata.type,
83
+ content: r.document.substring(0, 200) + '...'
84
+ })),
85
+ research_papers: researchContext.map(r => ({
86
+ title: r.metadata.title,
87
+ excerpt: r.document.substring(0, 200) + '...'
88
+ }))
89
+ }
90
+ };
91
+ } catch (error) {
92
+ return { text: `Error generating AI response: ${error.message}`, error: true };
93
+ }
94
+ }
95
+
96
+ async suggestTreatments(patientId, patientData, condition) {
97
+ const query = `Treatment options for ${condition}`;
98
+ return await this.generateRAGResponse(patientId, patientData, query);
99
+ }
100
+
101
+ async analyzeDrugInteractions(patientId, patientData, newMedication) {
102
+ const currentMeds = (patientData.medications || []).map(m => m.name).join(', ');
103
+ const query = `Drug interactions between ${newMedication} and current medications: ${currentMeds}`;
104
+ return await this.generateRAGResponse(patientId, patientData, query);
105
+ }
106
+ }
107
+
108
+ export const ragService = new RAGService();
services/timelineService.js ADDED
@@ -0,0 +1,286 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { generateResponse } from './llmClient.js';
2
+ import { ObjectId, getPatientsCollection, getAppointmentsCollection, getWarningsCollection } from '../db/mongo.js';
3
+ import dotenv from 'dotenv';
4
+
5
+ dotenv.config();
6
+
7
+ class TimelineService {
8
+ /**
9
+ * Try to parse a date string in various formats
10
+ */
11
+ _parseDate(dateStr) {
12
+ if (!dateStr) return null;
13
+
14
+ const formats = [
15
+ // ISO format
16
+ /^(\d{4})-(\d{2})-(\d{2})$/,
17
+ // DD/MM/YYYY
18
+ /^(\d{2})\/(\d{2})\/(\d{4})$/,
19
+ // MM/DD/YYYY
20
+ /^(\d{2})\/(\d{2})\/(\d{4})$/,
21
+ ];
22
+
23
+ // Try direct Date parsing first
24
+ const date = new Date(dateStr);
25
+ if (!isNaN(date.getTime())) {
26
+ return date;
27
+ }
28
+
29
+ return null;
30
+ }
31
+
32
+ /**
33
+ * Create a timeline event
34
+ */
35
+ _createEvent(eventType, title, description, date = null, dateStr = '', metadata = {}) {
36
+ return {
37
+ type: eventType,
38
+ title,
39
+ description,
40
+ date: date ? date.toISOString() : null,
41
+ date_display: dateStr || (date ? date.toLocaleDateString('en-US', {
42
+ year: 'numeric',
43
+ month: 'long',
44
+ day: 'numeric'
45
+ }) : 'Unknown date'),
46
+ metadata: metadata || {},
47
+ sort_date: date ? date.getTime() : 0
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Generate a chronological timeline of all patient events
53
+ */
54
+ async getPatientTimeline(patientId) {
55
+ const patientsCollection = getPatientsCollection();
56
+ const appointmentsCollection = getAppointmentsCollection();
57
+ const warningsCollection = getWarningsCollection();
58
+
59
+ const patient = await patientsCollection.findOne({ _id: new ObjectId(patientId) });
60
+ if (!patient) {
61
+ return [];
62
+ }
63
+
64
+ const events = [];
65
+
66
+ // Process medical history
67
+ const medicalHistory = patient.medical_history || [];
68
+ for (const item of medicalHistory) {
69
+ if (typeof item === 'object' && item !== null) {
70
+ // Structured MedicalEvent
71
+ const date = this._parseDate(item.date_diagnosed);
72
+ events.push(this._createEvent(
73
+ 'condition',
74
+ `Diagnosed: ${item.condition || 'Unknown'}`,
75
+ item.notes || '',
76
+ date,
77
+ item.date_diagnosed || '',
78
+ {
79
+ severity: item.severity,
80
+ resolved: item.date_resolved
81
+ }
82
+ ));
83
+
84
+ // Add resolution event if exists
85
+ if (item.date_resolved) {
86
+ const resolvedDate = this._parseDate(item.date_resolved);
87
+ events.push(this._createEvent(
88
+ 'condition_resolved',
89
+ `Resolved: ${item.condition || 'Unknown'}`,
90
+ 'Condition resolved',
91
+ resolvedDate,
92
+ item.date_resolved || '',
93
+ { original_condition: item.condition }
94
+ ));
95
+ }
96
+ } else {
97
+ // Legacy string format
98
+ events.push(this._createEvent(
99
+ 'condition',
100
+ `Medical History: ${item}`,
101
+ item,
102
+ null,
103
+ '',
104
+ { legacy_format: true }
105
+ ));
106
+ }
107
+ }
108
+
109
+ // Process medications
110
+ const medications = patient.medications || [];
111
+ for (const med of medications) {
112
+ if (typeof med === 'object' && med !== null) {
113
+ const startDate = this._parseDate(med.start_date);
114
+ events.push(this._createEvent(
115
+ 'medication_start',
116
+ `Started: ${med.name || 'Unknown Medication'}`,
117
+ `Dosage: ${med.dosage || 'N/A'}`,
118
+ startDate,
119
+ med.start_date || '',
120
+ {
121
+ medication_name: med.name,
122
+ dosage: med.dosage
123
+ }
124
+ ));
125
+
126
+ // Add end event if medication was stopped
127
+ if (med.end_date) {
128
+ const endDate = this._parseDate(med.end_date);
129
+ events.push(this._createEvent(
130
+ 'medication_end',
131
+ `Stopped: ${med.name || 'Unknown Medication'}`,
132
+ 'Medication course completed',
133
+ endDate,
134
+ med.end_date || '',
135
+ { medication_name: med.name }
136
+ ));
137
+ }
138
+ }
139
+ }
140
+
141
+ // Process appointments
142
+ try {
143
+ const appointments = await appointmentsCollection.find({ patient_id: patientId }).toArray();
144
+ for (const apt of appointments) {
145
+ let aptDate = apt.date_time;
146
+ if (typeof aptDate === 'string') {
147
+ aptDate = this._parseDate(aptDate);
148
+ }
149
+ events.push(this._createEvent(
150
+ 'appointment',
151
+ `Appointment: ${apt.purpose || 'Check-up'}`,
152
+ apt.notes || '',
153
+ aptDate instanceof Date ? aptDate : null,
154
+ '',
155
+ {
156
+ status: apt.status,
157
+ doctor_id: apt.doctor_id
158
+ }
159
+ ));
160
+ }
161
+ } catch (error) {
162
+ // Appointments collection might not have data yet
163
+ }
164
+
165
+ // Process warnings (only acknowledged ones for history)
166
+ try {
167
+ const warnings = await warningsCollection.find({
168
+ patient_id: patientId,
169
+ is_acknowledged: true
170
+ }).toArray();
171
+
172
+ for (const warning of warnings) {
173
+ const createdAt = warning.created_at;
174
+ events.push(this._createEvent(
175
+ 'warning',
176
+ `Warning: ${warning.title || 'Clinical Warning'}`,
177
+ warning.description || '',
178
+ createdAt instanceof Date ? createdAt : null,
179
+ '',
180
+ {
181
+ severity: warning.severity,
182
+ warning_type: warning.warning_type
183
+ }
184
+ ));
185
+ }
186
+ } catch (error) {
187
+ // Warnings collection might not have data yet
188
+ }
189
+
190
+ // Sort events by date (most recent first), unknown dates at the end
191
+ events.sort((a, b) => b.sort_date - a.sort_date);
192
+
193
+ // Remove sort_date from output
194
+ return events.map(event => {
195
+ const { sort_date, ...rest } = event;
196
+ return rest;
197
+ });
198
+ }
199
+
200
+ /**
201
+ * Generate an AI-powered summary of the patient's medical timeline
202
+ */
203
+ async generateTimelineSummary(patientId) {
204
+ const patientsCollection = getPatientsCollection();
205
+ const patient = await patientsCollection.findOne({ _id: new ObjectId(patientId) });
206
+
207
+ if (!patient) {
208
+ return { error: 'Patient not found' };
209
+ }
210
+
211
+ const timeline = await this.getPatientTimeline(patientId);
212
+
213
+ // Format timeline for AI
214
+ const timelineText = timeline.slice(0, 20).map(event =>
215
+ `- ${event.date_display}: ${event.title} - ${event.description}`
216
+ ).join('\n');
217
+
218
+ const prompt = `You are a medical AI assistant. Analyze this patient's medical timeline and provide a comprehensive summary.
219
+
220
+ PATIENT: ${patient.name || 'Unknown'}
221
+ Age: ${patient.age || 'Unknown'}
222
+ Gender: ${patient.gender || 'Unknown'}
223
+
224
+ MEDICAL TIMELINE:
225
+ ${timelineText || 'No timeline events available.'}
226
+
227
+ Please provide:
228
+ 1. **Overview**: A brief summary of the patient's medical journey
229
+ 2. **Key Milestones**: The most significant medical events
230
+ 3. **Patterns**: Any notable patterns in conditions or treatments
231
+ 4. **Current Status**: Assessment of current health based on timeline
232
+ 5. **Recommendations**: Suggested follow-ups or areas of attention
233
+
234
+ Be concise but thorough. Use bullet points where appropriate.`;
235
+
236
+ try {
237
+ const summary = await generateResponse(prompt);
238
+
239
+ return {
240
+ patient_id: patientId,
241
+ patient_name: patient.name,
242
+ summary,
243
+ events_analyzed: timeline.length,
244
+ generated_at: new Date().toISOString()
245
+ };
246
+ } catch (error) {
247
+ return { error: `Error generating summary: ${error.message}` };
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Get statistics about the patient's timeline
253
+ */
254
+ async getTimelineStats(patientId) {
255
+ const timeline = await this.getPatientTimeline(patientId);
256
+
257
+ const stats = {
258
+ total_events: timeline.length,
259
+ by_type: {},
260
+ date_range: {
261
+ earliest: null,
262
+ latest: null
263
+ }
264
+ };
265
+
266
+ const dates = [];
267
+ for (const event of timeline) {
268
+ const eventType = event.type;
269
+ stats.by_type[eventType] = (stats.by_type[eventType] || 0) + 1;
270
+ if (event.date) {
271
+ dates.push(event.date);
272
+ }
273
+ }
274
+
275
+ if (dates.length > 0) {
276
+ dates.sort();
277
+ stats.date_range.earliest = dates[0];
278
+ stats.date_range.latest = dates[dates.length - 1];
279
+ }
280
+
281
+ return stats;
282
+ }
283
+ }
284
+
285
+ // Singleton instance
286
+ export const timelineService = new TimelineService();
services/vectorService.js ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ChromaClient } from 'chromadb';
2
+
3
+ class VectorService {
4
+ constructor() {
5
+ this.chromaClient = null;
6
+ this.patientCollection = null;
7
+ this.researchCollection = null;
8
+ this.initialized = false;
9
+ }
10
+
11
+ async initialize() {
12
+ if (this.initialized) return;
13
+
14
+ try {
15
+ this.chromaClient = new ChromaClient({
16
+ path: './chroma_db'
17
+ });
18
+
19
+ this.patientCollection = await this._getOrCreateCollection('patient_records');
20
+ this.researchCollection = await this._getOrCreateCollection('medical_research');
21
+ this.initialized = true;
22
+ console.log('✓ Vector database initialized');
23
+ } catch (error) {
24
+ console.log(`⚠️ Vector database initialization warning: ${error.message}`);
25
+ }
26
+ }
27
+
28
+ async _getOrCreateCollection(name) {
29
+ try {
30
+ return await this.chromaClient.getCollection({ name });
31
+ } catch {
32
+ return await this.chromaClient.createCollection({
33
+ name,
34
+ metadata: { description: `Collection for ${name}` }
35
+ });
36
+ }
37
+ }
38
+
39
+ async addPatientRecord(patientId, patientData) {
40
+ await this.initialize();
41
+
42
+ const documents = [];
43
+ const metadatas = [];
44
+ const ids = [];
45
+
46
+ // Add medical history
47
+ if (patientData.medical_history) {
48
+ patientData.medical_history.forEach((condition, idx) => {
49
+ const docId = `patient_${patientId}_history_${idx}`;
50
+ const conditionText = typeof condition === 'string' ? condition : condition.condition || 'Unknown';
51
+ documents.push(`Patient ${patientData.name} has medical history: ${conditionText}`);
52
+ metadatas.push({
53
+ patient_id: patientId,
54
+ patient_name: patientData.name,
55
+ type: 'medical_history',
56
+ condition: conditionText,
57
+ age: String(patientData.age || ''),
58
+ gender: patientData.gender || ''
59
+ });
60
+ ids.push(docId);
61
+ });
62
+ }
63
+
64
+ // Add medications
65
+ if (patientData.medications) {
66
+ patientData.medications.forEach((med, idx) => {
67
+ const docId = `patient_${patientId}_medication_${idx}`;
68
+ let medText = `Patient ${patientData.name} is taking ${med.name}, dosage: ${med.dosage}, started: ${med.start_date}`;
69
+ if (med.end_date) {
70
+ medText += `, ended: ${med.end_date}`;
71
+ }
72
+ documents.push(medText);
73
+ metadatas.push({
74
+ patient_id: patientId,
75
+ patient_name: patientData.name,
76
+ type: 'medication',
77
+ medication_name: med.name,
78
+ dosage: med.dosage,
79
+ start_date: med.start_date
80
+ });
81
+ ids.push(docId);
82
+ });
83
+ }
84
+
85
+ // Add summary
86
+ const medHistory = patientData.medical_history || ['None'];
87
+ const medNames = (patientData.medications || []).map(m => m.name);
88
+ const summaryText = `
89
+ Patient Profile: ${patientData.name}, Age: ${patientData.age}, Gender: ${patientData.gender}.
90
+ Medical History: ${medHistory.join(', ')}.
91
+ Current Medications: ${medNames.join(', ') || 'None'}.
92
+ `.trim();
93
+
94
+ documents.push(summaryText);
95
+ metadatas.push({
96
+ patient_id: patientId,
97
+ patient_name: patientData.name,
98
+ type: 'summary',
99
+ age: String(patientData.age || ''),
100
+ gender: patientData.gender || ''
101
+ });
102
+ ids.push(`patient_${patientId}_summary`);
103
+
104
+ if (documents.length > 0) {
105
+ try {
106
+ await this.patientCollection.upsert({
107
+ documents,
108
+ metadatas,
109
+ ids
110
+ });
111
+ console.log(` ✓ Indexed ${documents.length} records for patient ${patientData.name}`);
112
+ } catch (error) {
113
+ console.log(` ✗ Error indexing patient ${patientData.name}: ${error.message}`);
114
+ }
115
+ }
116
+
117
+ return documents.length;
118
+ }
119
+
120
+ async searchPatientRecords(query, patientId = null, nResults = 5) {
121
+ await this.initialize();
122
+
123
+ try {
124
+ const whereFilter = patientId ? { patient_id: patientId } : undefined;
125
+
126
+ const results = await this.patientCollection.query({
127
+ queryTexts: [query],
128
+ nResults,
129
+ where: whereFilter
130
+ });
131
+
132
+ const formattedResults = [];
133
+ if (results && results.documents && results.documents[0]) {
134
+ for (let idx = 0; idx < results.documents[0].length; idx++) {
135
+ formattedResults.push({
136
+ document: results.documents[0][idx],
137
+ metadata: results.metadatas[0][idx],
138
+ distance: results.distances ? results.distances[0][idx] : null
139
+ });
140
+ }
141
+ }
142
+
143
+ return formattedResults;
144
+ } catch (error) {
145
+ console.log(`Search error: ${error.message}`);
146
+ return [];
147
+ }
148
+ }
149
+
150
+ async addResearchPaper(paperId, title, content, metadata) {
151
+ await this.initialize();
152
+
153
+ try {
154
+ const chunkSize = 1000;
155
+ const chunks = [];
156
+ const words = content.split(' ');
157
+
158
+ for (let i = 0; i < words.length; i += chunkSize) {
159
+ const chunk = words.slice(i, i + chunkSize).join(' ');
160
+ chunks.push(chunk);
161
+ }
162
+
163
+ const documents = [];
164
+ const metadatas = [];
165
+ const ids = [];
166
+
167
+ chunks.forEach((chunk, idx) => {
168
+ documents.push(chunk);
169
+ const chunkMetadata = {
170
+ paper_id: paperId,
171
+ title,
172
+ chunk_index: String(idx),
173
+ total_chunks: String(chunks.length),
174
+ ...Object.fromEntries(Object.entries(metadata).map(([k, v]) => [k, String(v)]))
175
+ };
176
+ metadatas.push(chunkMetadata);
177
+ ids.push(`research_${paperId}_chunk_${idx}`);
178
+ });
179
+
180
+ if (documents.length > 0) {
181
+ await this.researchCollection.upsert({
182
+ documents,
183
+ metadatas,
184
+ ids
185
+ });
186
+ }
187
+
188
+ return chunks.length;
189
+ } catch (error) {
190
+ console.log(`Error adding research paper: ${error.message}`);
191
+ return 0;
192
+ }
193
+ }
194
+
195
+ async searchResearchPapers(query, nResults = 5) {
196
+ await this.initialize();
197
+
198
+ try {
199
+ const results = await this.researchCollection.query({
200
+ queryTexts: [query],
201
+ nResults
202
+ });
203
+
204
+ const formattedResults = [];
205
+ if (results && results.documents && results.documents[0]) {
206
+ for (let idx = 0; idx < results.documents[0].length; idx++) {
207
+ formattedResults.push({
208
+ document: results.documents[0][idx],
209
+ metadata: results.metadatas[0][idx],
210
+ distance: results.distances ? results.distances[0][idx] : null
211
+ });
212
+ }
213
+ }
214
+
215
+ return formattedResults;
216
+ } catch (error) {
217
+ console.log(`Research search error: ${error.message}`);
218
+ return [];
219
+ }
220
+ }
221
+
222
+ async deletePatientRecords(patientId) {
223
+ await this.initialize();
224
+
225
+ try {
226
+ const results = await this.patientCollection.get({
227
+ where: { patient_id: patientId }
228
+ });
229
+
230
+ if (results && results.ids && results.ids.length > 0) {
231
+ await this.patientCollection.delete({ ids: results.ids });
232
+ }
233
+ return true;
234
+ } catch (error) {
235
+ console.log(`Error deleting patient records: ${error.message}`);
236
+ return false;
237
+ }
238
+ }
239
+
240
+ async getCollectionStats() {
241
+ await this.initialize();
242
+
243
+ try {
244
+ const patientCount = await this.patientCollection.count();
245
+ const researchCount = await this.researchCollection.count();
246
+
247
+ return {
248
+ patient_records_count: patientCount,
249
+ research_papers_count: researchCount
250
+ };
251
+ } catch {
252
+ return {
253
+ patient_records_count: 0,
254
+ research_papers_count: 0
255
+ };
256
+ }
257
+ }
258
+ }
259
+
260
+ export const vectorService = new VectorService();
261
+
services/warningService.js ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { generateResponse } from './llmClient.js';
2
+ import { ObjectId, getWarningsCollection, getPatientsCollection } from '../db/mongo.js';
3
+ import dotenv from 'dotenv';
4
+
5
+ dotenv.config();
6
+
7
+ // Warning types enum
8
+ export const WarningType = {
9
+ DRUG_INTERACTION: 'drug_interaction',
10
+ ALLERGY: 'allergy',
11
+ CONTRAINDICATION: 'contraindication',
12
+ ABNORMAL_PATTERN: 'abnormal_pattern',
13
+ DOSAGE_ALERT: 'dosage_alert',
14
+ DUPLICATE_THERAPY: 'duplicate_therapy'
15
+ };
16
+
17
+ // Warning severity enum
18
+ export const WarningSeverity = {
19
+ LOW: 'low',
20
+ MEDIUM: 'medium',
21
+ HIGH: 'high',
22
+ CRITICAL: 'critical'
23
+ };
24
+
25
+ class WarningService {
26
+ /**
27
+ * Convert MongoDB warning document to response format
28
+ */
29
+ _warningHelper(warning) {
30
+ return {
31
+ id: warning._id.toString(),
32
+ patient_id: warning.patient_id,
33
+ warning_type: warning.warning_type,
34
+ severity: warning.severity,
35
+ title: warning.title,
36
+ description: warning.description,
37
+ related_medications: warning.related_medications || [],
38
+ related_conditions: warning.related_conditions || [],
39
+ recommendation: warning.recommendation || null,
40
+ created_at: warning.created_at,
41
+ is_acknowledged: warning.is_acknowledged || false,
42
+ acknowledged_by: warning.acknowledged_by || null,
43
+ acknowledged_at: warning.acknowledged_at || null
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Get all warnings for a patient
49
+ */
50
+ async getPatientWarnings(patientId, includeAcknowledged = false) {
51
+ const warningsCollection = getWarningsCollection();
52
+ const query = { patient_id: patientId };
53
+
54
+ if (!includeAcknowledged) {
55
+ query.is_acknowledged = false;
56
+ }
57
+
58
+ const warnings = await warningsCollection
59
+ .find(query)
60
+ .sort({ created_at: -1 })
61
+ .toArray();
62
+
63
+ return warnings.map(w => this._warningHelper(w));
64
+ }
65
+
66
+ /**
67
+ * Get all unacknowledged warnings across all patients
68
+ */
69
+ async getAllUnacknowledgedWarnings(limit = 50) {
70
+ const warningsCollection = getWarningsCollection();
71
+
72
+ const warnings = await warningsCollection
73
+ .find({ is_acknowledged: false })
74
+ .sort({ severity: -1, created_at: -1 })
75
+ .limit(limit)
76
+ .toArray();
77
+
78
+ return warnings.map(w => this._warningHelper(w));
79
+ }
80
+
81
+ /**
82
+ * Acknowledge a clinical warning
83
+ */
84
+ async acknowledgeWarning(warningId, userId, notes = null) {
85
+ const warningsCollection = getWarningsCollection();
86
+
87
+ const result = await warningsCollection.updateOne(
88
+ { _id: new ObjectId(warningId) },
89
+ {
90
+ $set: {
91
+ is_acknowledged: true,
92
+ acknowledged_by: userId,
93
+ acknowledged_at: new Date(),
94
+ acknowledgement_notes: notes
95
+ }
96
+ }
97
+ );
98
+
99
+ if (result.matchedCount === 0) {
100
+ return null;
101
+ }
102
+
103
+ const warning = await warningsCollection.findOne({ _id: new ObjectId(warningId) });
104
+ return this._warningHelper(warning);
105
+ }
106
+
107
+ /**
108
+ * Create a new clinical warning
109
+ */
110
+ async createWarning(warningData) {
111
+ const warningsCollection = getWarningsCollection();
112
+
113
+ const warningDoc = {
114
+ ...warningData,
115
+ created_at: new Date(),
116
+ is_acknowledged: false
117
+ };
118
+
119
+ const result = await warningsCollection.insertOne(warningDoc);
120
+ warningDoc._id = result.insertedId;
121
+ return this._warningHelper(warningDoc);
122
+ }
123
+
124
+ /**
125
+ * Delete all warnings for a patient
126
+ */
127
+ async deletePatientWarnings(patientId) {
128
+ const warningsCollection = getWarningsCollection();
129
+ const result = await warningsCollection.deleteMany({ patient_id: patientId });
130
+ return result.deletedCount;
131
+ }
132
+
133
+ /**
134
+ * Use AI to analyze patient data and generate clinical warnings
135
+ */
136
+ async analyzePatientForWarnings(patientId, newMedication = null, newCondition = null) {
137
+ const patientsCollection = getPatientsCollection();
138
+ const patient = await patientsCollection.findOne({ _id: new ObjectId(patientId) });
139
+
140
+ if (!patient) {
141
+ return [];
142
+ }
143
+
144
+ // Build the analysis prompt
145
+ const medications = patient.medications || [];
146
+ const medNames = medications
147
+ .filter(m => typeof m === 'object')
148
+ .map(m => m.name || '');
149
+
150
+ let conditions = patient.medical_history || [];
151
+
152
+ if (newMedication) {
153
+ medNames.push(`${newMedication} (NEW)`);
154
+ }
155
+ if (newCondition) {
156
+ conditions = [...conditions, `${newCondition} (NEW)`];
157
+ }
158
+
159
+ const conditionsText = conditions.map(c => {
160
+ if (typeof c === 'string') return c;
161
+ return c.condition || 'Unknown';
162
+ }).join(', ');
163
+
164
+ const prompt = `You are a clinical decision support AI. Analyze this patient's data for potential clinical warnings.
165
+
166
+ PATIENT INFORMATION:
167
+ - Age: ${patient.age || 'Unknown'}
168
+ - Gender: ${patient.gender || 'Unknown'}
169
+ - Medical Conditions: ${conditionsText || 'None recorded'}
170
+ - Current Medications: ${medNames.join(', ') || 'None recorded'}
171
+
172
+ Analyze for:
173
+ 1. DRUG INTERACTIONS - Check if any medications interact negatively with each other
174
+ 2. CONTRAINDICATIONS - Check if any medications are contraindicated given the patient's conditions
175
+ 3. ALLERGY RISKS - Common allergy cross-reactions
176
+ 4. DOSAGE CONCERNS - Age-related dosage considerations
177
+ 5. DUPLICATE THERAPY - Multiple drugs for the same purpose
178
+
179
+ Return a JSON array of warnings. Each warning should have:
180
+ - warning_type: one of "drug_interaction", "allergy", "contraindication", "abnormal_pattern", "dosage_alert", "duplicate_therapy"
181
+ - severity: one of "low", "medium", "high", "critical"
182
+ - title: short title (max 50 chars)
183
+ - description: detailed explanation
184
+ - related_medications: array of medication names involved
185
+ - related_conditions: array of conditions involved
186
+ - recommendation: suggested action
187
+
188
+ If there are no warnings, return an empty array [].
189
+ Return ONLY valid JSON, no markdown or other text.`;
190
+
191
+ try {
192
+ let responseText = await generateResponse(prompt);
193
+ responseText = responseText.trim();
194
+
195
+ // Remove markdown code blocks if present
196
+ if (responseText.startsWith('```')) {
197
+ responseText = responseText.split('```')[1];
198
+ if (responseText.startsWith('json')) {
199
+ responseText = responseText.substring(4);
200
+ }
201
+ }
202
+ responseText = responseText.trim();
203
+
204
+ const warningsData = JSON.parse(responseText);
205
+
206
+ if (!Array.isArray(warningsData)) {
207
+ return [];
208
+ }
209
+
210
+ // Create warnings in database
211
+ const createdWarnings = [];
212
+ for (const warning of warningsData) {
213
+ try {
214
+ const warningCreate = {
215
+ patient_id: patientId,
216
+ warning_type: warning.warning_type || 'abnormal_pattern',
217
+ severity: warning.severity || 'medium',
218
+ title: (warning.title || 'Clinical Warning').substring(0, 100),
219
+ description: warning.description || '',
220
+ related_medications: warning.related_medications || [],
221
+ related_conditions: warning.related_conditions || [],
222
+ recommendation: warning.recommendation || null
223
+ };
224
+
225
+ const createdWarning = await this.createWarning(warningCreate);
226
+ createdWarnings.push(createdWarning);
227
+ } catch (error) {
228
+ console.log(`Error creating warning: ${error.message}`);
229
+ continue;
230
+ }
231
+ }
232
+
233
+ return createdWarnings;
234
+ } catch (error) {
235
+ if (error instanceof SyntaxError) {
236
+ console.log(`JSON parse error: ${error.message}`);
237
+ } else {
238
+ console.log(`Error analyzing patient: ${error.message}`);
239
+ }
240
+ return [];
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Get warning statistics
246
+ */
247
+ async getWarningStats() {
248
+ const warningsCollection = getWarningsCollection();
249
+
250
+ const pipeline = [
251
+ {
252
+ $group: {
253
+ _id: {
254
+ severity: '$severity',
255
+ acknowledged: '$is_acknowledged'
256
+ },
257
+ count: { $sum: 1 }
258
+ }
259
+ }
260
+ ];
261
+
262
+ const results = await warningsCollection.aggregate(pipeline).toArray();
263
+
264
+ const stats = {
265
+ total: 0,
266
+ unacknowledged: 0,
267
+ by_severity: {
268
+ critical: 0,
269
+ high: 0,
270
+ medium: 0,
271
+ low: 0
272
+ }
273
+ };
274
+
275
+ for (const r of results) {
276
+ const count = r.count;
277
+ stats.total += count;
278
+
279
+ if (!r._id.acknowledged) {
280
+ stats.unacknowledged += count;
281
+ const severity = r._id.severity;
282
+ if (severity in stats.by_severity) {
283
+ stats.by_severity[severity] += count;
284
+ }
285
+ }
286
+ }
287
+
288
+ return stats;
289
+ }
290
+ }
291
+
292
+ // Singleton instance
293
+ export const warningService = new WarningService();