-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.js
More file actions
138 lines (121 loc) · 5.49 KB
/
Copy pathserver.js
File metadata and controls
138 lines (121 loc) · 5.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const { buildSystemPrompt } = require('./prompt-template');
const callOpenAI = require('./providers/openai');
const callAnthropic = require('./providers/anthropic');
const callGemini = require('./providers/gemini');
const callLocal = require('./providers/local');
const { CLINICAL_TOOLS, toOpenAITools, toGeminiTools } = require('./clinical-tools/definitions');
const { ToolExecutor } = require('./clinical-tools/executor');
const app = express();
const PORT = process.env.PORT || 3001;
const allowedOrigins = (process.env.ALLOWED_ORIGINS || 'http://localhost:8080')
.split(',')
.map((o) => o.trim())
.filter(Boolean);
const allowedOriginSet = new Set(allowedOrigins);
app.use(
cors({
origin: (origin, callback) => {
// Allow requests with no origin (e.g. curl, server-to-server)
if (!origin) return callback(null, true);
if (allowedOriginSet.has(origin)) return callback(null, true);
callback(new Error(`CORS: origin '${origin}' not allowed`));
},
}),
);
app.use(express.json({ limit: '1mb' }));
const PROVIDERS_WITH_TOOLS = {
...(process.env.OPENAI_API_KEY && { openai: callOpenAI.callOpenAIWithTools }),
...(process.env.ANTHROPIC_API_KEY && { anthropic: callAnthropic.callAnthropicWithTools }),
...(process.env.GEMINI_API_KEY && { gemini: callGemini.callGeminiWithTools }),
local: callLocal.callLocalWithTools,
};
const LLM_PROVIDER = process.env.LLM_PROVIDER;
const LLM_MODEL = process.env.LLM_MODEL;
if (!LLM_PROVIDER || !LLM_MODEL) {
console.error('LLM_PROVIDER and LLM_MODEL must be set in .env');
process.exit(1);
}
const activeCallWithTools = PROVIDERS_WITH_TOOLS[LLM_PROVIDER];
if (!activeCallWithTools) {
console.error(`Unknown or unconfigured LLM_PROVIDER: "${LLM_PROVIDER}". Available: ${Object.keys(PROVIDERS_WITH_TOOLS).join(', ')}`);
process.exit(1);
}
const activeTools =
LLM_PROVIDER === 'gemini' ? toGeminiTools() :
LLM_PROVIDER === 'anthropic' ? CLINICAL_TOOLS :
toOpenAITools();
/**
* POST /api/generate-visit-summary
*
* Body: { visitUuid, patientUuid }
* Response: { summary }
*
* The LLM calls clinical tools on demand to fetch visit data from OpenMRS.
* Provider and model are configured via LLM_PROVIDER and LLM_MODEL env vars.
* OpenMRS credentials are read from OPENMRS_USERNAME / OPENMRS_PASSWORD env vars.
*/
app.post('/api/generate-visit-summary', async (req, res) => {
const { visitUuid, patientUuid } = req.body ?? {};
const missingFields = ['visitUuid', 'patientUuid'].filter(
(f) => !req.body?.[f],
);
if (missingFields.length) {
return res.status(400).json({ error: `Missing required fields: ${missingFields.join(', ')}` });
}
console.log(`[POST /api/generate-visit-summary] provider=${LLM_PROVIDER} model=${LLM_MODEL} visit=${visitUuid}`);
const start = Date.now();
try {
const executor = new ToolExecutor({ visitUuid, patientUuid });
const systemPrompt = buildSystemPrompt();
const { content: summary, usage } = await activeCallWithTools(LLM_MODEL, systemPrompt, activeTools, executor);
const tokens =
usage?.total != null
? `${usage.input} in / ${usage.output} out / ${usage.total} total tokens`
: 'token usage unavailable';
console.log(
`[POST /api/generate-visit-summary] ${LLM_PROVIDER}/${LLM_MODEL} responded in ${Date.now() - start}ms — ${tokens}`,
);
return res.json({ summary });
} catch (err) {
console.error(
`[POST /api/generate-visit-summary] ${LLM_PROVIDER}/${LLM_MODEL} failed in ${Date.now() - start}ms: ${err.message}`,
);
return res.status(502).json({ error: err.message });
}
});
app.get('/health', (_req, res) => res.json({ status: 'ok' }));
async function checkActiveProvider() {
if (LLM_PROVIDER === 'openai') {
await fetch('https://api.openai.com/v1/models', {
headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` },
})
.then((r) => console.log('[openai] ', r.ok ? 'ready' : `error (${r.status})`))
.catch((e) => console.log('[openai] unreachable:', e.message));
} else if (LLM_PROVIDER === 'anthropic') {
await fetch('https://api.anthropic.com/v1/models', {
headers: { 'x-api-key': process.env.ANTHROPIC_API_KEY, 'anthropic-version': '2023-06-01' },
})
.then((r) => console.log('[anthropic] ', r.ok ? 'ready' : `error (${r.status})`))
.catch((e) => console.log('[anthropic] unreachable:', e.message));
} else if (LLM_PROVIDER === 'gemini') {
await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${process.env.GEMINI_API_KEY}`)
.then((r) => console.log('[gemini] ', r.ok ? 'ready' : `error (${r.status})`))
.catch((e) => console.log('[gemini] unreachable:', e.message));
} else if (LLM_PROVIDER === 'local') {
const localBase = (process.env.LOCAL_MODEL_BASE_URL || 'http://localhost:11434/v1').replace(/\/$/, '');
await fetch(`${localBase}/models`, {
headers: { Authorization: `Bearer ${process.env.LOCAL_MODEL_API_KEY || 'local'}` },
})
.then((r) => console.log('[local] ', r.ok ? `ready (${localBase})` : `error (${r.status}) — ${localBase}`))
.catch((e) => console.log(`[local] unreachable (${localBase}):`, e.message));
}
}
app.listen(PORT, () => {
console.log(`openmrs-ai-proxy-server listening on http://localhost:${PORT}`);
console.log(`Allowed origins: ${allowedOrigins.join(', ')}`);
console.log(`Active LLM: ${LLM_PROVIDER} / ${LLM_MODEL}`);
checkActiveProvider();
});