Skip to content

Commit c8fb342

Browse files
Replace external LLM dependency with fully embedded AI triage engine
Add src/lib/triageAI.js: self-contained rule-based intelligence engine with medical knowledge, drug interaction/toxicity detection, rejection sign analysis, risk assessment, and patient communication generation. Update appClient.js to route all InvokeLLM calls through the embedded engine. Rewrite Settings page to show built-in AI engine status. Simplify ImportRulesDialog to use direct offline file parsing. All 42 tests pass, zero lint errors. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 2e0ac68 commit c8fb342

4 files changed

Lines changed: 1196 additions & 401 deletions

File tree

src/api/appClient.js

Lines changed: 16 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -243,58 +243,15 @@ class LocalAuth {
243243
redirectToLogin() {}
244244
}
245245

246-
// --- Integrations ---
246+
// --- Integrations (Built-in AI Engine) ---
247247
class LocalIntegrations {
248248
constructor() {
249+
this._engine = null;
250+
this._enginePromise = null;
249251
this.Core = {
250252
InvokeLLM: async (params) => {
251-
const endpoint = ipc ? await ipc.settings.get('llm_endpoint') : localStorage.getItem(`${DB_PREFIX}llm_endpoint`) || '';
252-
const apiKey = ipc ? await ipc.settings.get('llm_api_key') : localStorage.getItem(`${DB_PREFIX}llm_api_key`) || '';
253-
const model = ipc ? await ipc.settings.get('llm_model') : localStorage.getItem(`${DB_PREFIX}llm_model`) || 'gpt-4';
254-
255-
if (endpoint) {
256-
try {
257-
const response = await fetch(endpoint, {
258-
method: 'POST',
259-
headers: { 'Content-Type': 'application/json', ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}) },
260-
body: JSON.stringify({
261-
model: model || 'gpt-4',
262-
messages: [
263-
...(params.system_prompt ? [{ role: 'system', content: params.system_prompt }] : []),
264-
{ role: 'user', content: params.prompt || params.user_prompt || '' },
265-
],
266-
...(params.response_json_schema ? { response_format: { type: 'json_object' } } : {}),
267-
}),
268-
});
269-
const data = await response.json();
270-
let content = data.choices?.[0]?.message?.content || JSON.stringify(data);
271-
if (params.response_json_schema) {
272-
const cleaned = content.replace(/^```(?:json)?\s*\n?/i, '').replace(/\n?```\s*$/i, '').trim();
273-
try { return JSON.parse(cleaned); } catch {
274-
try { return JSON.parse(content); } catch { return content; }
275-
}
276-
}
277-
return content;
278-
} catch (error) {
279-
console.warn('[TriageLink] LLM call failed:', error.message);
280-
}
281-
}
282-
283-
if (params.response_json_schema) {
284-
return {
285-
urgency_level: 'non-urgent',
286-
matched_rule_id: 'GENERAL_PROTOCOL',
287-
complaint_category: 'General Triage Protocol',
288-
action_required: 'Offline mode — configure an LLM endpoint in Settings to enable AI-powered analysis.',
289-
reasoning: 'No LLM endpoint configured. Connect a local LLM (e.g. Ollama) or set an OpenAI-compatible API key in Settings.',
290-
confidence_score: 0,
291-
ai_summary: 'Offline Mode | No AI analysis available — please configure an LLM endpoint in Settings.',
292-
patient_condition_summary: 'AI analysis unavailable in offline mode. Please review the complaint manually and apply hospital protocols.',
293-
needs_clarification: false,
294-
follow_up_actions: ['Configure an LLM endpoint in Settings to enable AI-powered triage analysis.'],
295-
};
296-
}
297-
return 'Offline mode — configure an LLM endpoint in Settings for AI-powered analysis.';
253+
const engine = await this._getEngine();
254+
return engine.routeInvokeLLM(params);
298255
},
299256
SendEmail: async (params) => { console.log('[TriageLink] Email queued:', params); return { success: true, message: 'Email logged locally' }; },
300257
SendSMS: async (params) => { console.log('[TriageLink] SMS queued:', params); return { success: true, message: 'SMS logged locally' }; },
@@ -303,6 +260,17 @@ class LocalIntegrations {
303260
ExtractDataFromUploadedFile: async (params) => { console.log('[TriageLink] Data extraction:', params); return { data: {}, success: true }; },
304261
};
305262
}
263+
264+
async _getEngine() {
265+
if (this._engine) return this._engine;
266+
if (!this._enginePromise) {
267+
this._enginePromise = import('@/lib/triageAI.js').then(mod => {
268+
this._engine = mod;
269+
return mod;
270+
});
271+
}
272+
return this._enginePromise;
273+
}
306274
}
307275

308276
// --- Audit helper (renderer-side) ---
@@ -395,21 +363,8 @@ class AppClient {
395363
if (ipc) {
396364
await migrateLocalStorageIfNeeded();
397365
if (this.auth.init) await this.auth.init();
398-
await this._ensureLLMDefaults();
399366
}
400367
}
401-
402-
async _ensureLLMDefaults() {
403-
try {
404-
const existing = await this.settings.get('llm_endpoint');
405-
if (!existing) {
406-
await this.settings.set('llm_endpoint', 'http://localhost:11434/v1/chat/completions');
407-
await this.settings.set('llm_model', 'llama3.1:8b');
408-
await this.settings.set('llm_api_key', '');
409-
console.log('[TriageLink] Ollama defaults configured');
410-
}
411-
} catch {}
412-
}
413368
}
414369

415370
export const appClient = new AppClient();

src/components/rules/ImportRulesDialog.jsx

Lines changed: 6 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useState, useRef, useCallback } from "react";
22
import { appClient } from "@/api/appClient";
3-
import { useMutation, useQueryClient } from "@tanstack/react-query";
3+
import { useQueryClient } from "@tanstack/react-query";
44
import {
55
Dialog,
66
DialogContent,
@@ -29,44 +29,7 @@ import {
2929
} from "lucide-react";
3030
import { parseFile, ACCEPTED_FILE_TYPES, FILE_TYPE_LABELS, extractRulesFromStructuredData, extractRulesFromText } from "@/lib/fileParser";
3131

32-
const EXTRACTION_PROMPT = `You are an expert medical triage data extraction AI. Your task is to analyze a document containing transplant hospital criteria, paging rules, and triage protocols, then extract structured rules from it.
33-
34-
DOCUMENT CONTENT:
35-
{DOCUMENT_TEXT}
36-
37-
INSTRUCTIONS:
38-
1. Carefully read the entire document content above
39-
2. Identify ALL triage rules, paging criteria, alert thresholds, escalation paths, and contact routing instructions
40-
3. For each rule found, extract the structured fields listed below
41-
4. If a field is not explicitly stated, infer it from context or mark it as empty
42-
5. Pay special attention to: complaint categories, trigger criteria, paging numbers, escalation paths, urgency levels, organ types, patient types
43-
44-
Extract each rule as a JSON object. Return a JSON object with this exact structure:
45-
{
46-
"rules": [
47-
{
48-
"complaint_category": "The complaint or condition category (e.g., Fever, Pain, Bleeding, Medication Issue)",
49-
"trigger_criteria": "Specific threshold or criteria that triggers this rule (e.g., Fever >100.5°F, Creatinine >2.0)",
50-
"action_required": "What action to take / who to page / routing instructions",
51-
"contact_method": "phone | secure_page | email | urgent_page",
52-
"contact_info": "Phone/pager number if mentioned",
53-
"escalation_path": "Who to escalate to if no response",
54-
"priority": "routine | urgent | emergency",
55-
"patient_type": "pre-transplant | post-transplant | non-transplant (if specified)",
56-
"organ_type": "kidney | liver | kidney-pancreas (if specified)",
57-
"documentation_notes": "Any required documentation or special instructions"
58-
}
59-
],
60-
"hospital_name": "Name of the hospital if mentioned in the document",
61-
"summary": "Brief 2-3 sentence summary of what the document contains"
62-
}
63-
64-
IMPORTANT:
65-
- Extract EVERY rule you can find, even partial ones
66-
- Use exact paging numbers and contact info from the document
67-
- Preserve medical terminology exactly as written
68-
- If the document has tabular data, treat each row as a potential rule
69-
- Return valid JSON only`;
32+
// No external prompt needed — extraction is handled by the built-in file parser
7033

7134
function getFileIcon(fileName) {
7235
const ext = fileName.split('.').pop().toLowerCase();
@@ -203,41 +166,10 @@ export default function ImportRulesDialog({ open, onOpenChange, hospitals }) {
203166
setParseError(null);
204167

205168
try {
206-
const docText = parsedContent.text.substring(0, 15000);
207-
const prompt = EXTRACTION_PROMPT.replace('{DOCUMENT_TEXT}', docText);
208-
209-
const result = await appClient.integrations.Core.InvokeLLM({
210-
prompt,
211-
response_json_schema: {
212-
type: "object",
213-
properties: {
214-
rules: { type: "array" },
215-
hospital_name: { type: "string" },
216-
summary: { type: "string" },
217-
},
218-
},
219-
});
220-
221-
let parsed;
222-
try {
223-
parsed = typeof result === 'string' ? JSON.parse(result) : result;
224-
} catch {
225-
parsed = { rules: [] };
226-
}
227-
228-
if (parsed.rules && parsed.rules.length > 0) {
229-
applyExtractionResult(parsed);
230-
} else {
231-
const offlineResult = extractOffline();
232-
applyExtractionResult(offlineResult);
233-
}
234-
} catch {
235-
try {
236-
const offlineResult = extractOffline();
237-
applyExtractionResult(offlineResult);
238-
} catch (offlineErr) {
239-
setParseError(`Extraction failed: ${offlineErr.message}`);
240-
}
169+
const result = extractOffline();
170+
applyExtractionResult(result);
171+
} catch (err) {
172+
setParseError(`Extraction failed: ${err.message}`);
241173
} finally {
242174
setIsExtracting(false);
243175
}

0 commit comments

Comments
 (0)