An explainable AI system for medical diagnosis that takes patient data (text medical records + radiotherapy/CT images) and produces diagnoses WITH a full reasoning trail explaining how each conclusion was reached.
The key innovation is a dual-model convergence loop: two AI models with different strategies analyse the same data independently, and a feedback loop forces them to converge — or explicitly flags where they disagree for human review. The reasoning chain built during this process IS the explainability.
┌─────────────────────────────────────────────────────────────────────────┐
│ PHASE 1: INPUT │
│ │
│ UI/UX ──► Patient Data Ingestion │
│ • Past text medical records (medical_history from DB) │
│ • Past radiotherapy/CT images (prior scans from DB) │
│ • Latest radiotherapy/CT image (is_latest=1 from DB) │
│ • All loaded via image_path in scans table │
│ │
│ Packaged as structured patient context ──► │
│ │
└──────────────────────────────┬──────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ PHASE 2: DUAL-MODEL FEEDBACK LOOP │
│ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │
│ │ FEEDBACK LOOP — max N iterations (default 5) │ │
│ │ │ │
│ │ ┌──────────────────────┐ ┌──────────────────────┐ │ │
│ │ │ CONCLUSIONS MODEL │ │ ITERATIVE MODEL │ │ │
│ │ │ (High Entropy 0.7) │ │ (Low Entropy 0.1) │ │ │
│ │ │ │ │ │ │ │
│ │ │ Input: │ │ Input: │ │ │
│ │ │ • Patient data │ │ • Patient data │ │ │
│ │ │ • All images │ │ • All images │ │ │
│ │ │ • Logical steps DB │ │ • Logical steps DB │ │ │
│ │ │ │ │ • Flagged regions │ │ │
│ │ │ Output: │ │ │ │ │
│ │ │ ALL possible FINAL │ │ Output: │ │ │
│ │ │ conclusions at once │ │ SINGLE next logical │ │ │
│ │ │ (favours false │ │ step only. No leaps. │ │ │
│ │ │ positives over │ │ Can only output a │ │ │
│ │ │ false negatives) │ │ FINAL conclusion │ │ │
│ │ │ │ │ when ALL prerequisite│ │ │
│ │ │ + spatial regions │ │ steps exist. │ │ │
│ │ │ (bounding boxes) │ │ + spatial regions │ │ │
│ │ └──────────┬───────────┘ └──────────┬────────────┘ │ │
│ │ │ │ │ │
│ │ └──────────┬─────────────────┘ │ │
│ │ ▼ │ │
│ │ ┌────────────────────────┐ │ │
│ │ │ COMPARISON MODEL │ │ │
│ │ │ (Temperature 0.0) │ │ │
│ │ │ │ │ │
│ │ │ Compares FINAL │ │ │
│ │ │ conclusions from │ │ │
│ │ │ both models. │ │ │
│ │ │ │ │ │
│ │ │ Output: structured │ │ │
│ │ │ JSON per conclusion: │ │ │
│ │ │ matched/mismatched │ │ │
│ │ │ + spatial regions │ │ │
│ │ └───────────┬────────────┘ │ │
│ │ │ │ │
│ │ ┌────────┴────────┐ │ │
│ │ ▼ ▼ │ │
│ │ ┌──────────────┐ ┌───────────────┐ │ │
│ │ │ ✗ MISMATCH │ │ ✓ MATCH │ │ │
│ │ │ │ │ │ │ │
│ │ │ 1. Append │ │ Converged! │ │ │
│ │ │ iterative │ │ Exit loop. │──────────► PHASE 3 │ │
│ │ │ model's │ │ │ │ │
│ │ │ step to │ └───────────────┘ │ │
│ │ │ logical │ │ │
│ │ │ steps DB │ │ │
│ │ │ │ │ │
│ │ │ 2. Flag │ │ │
│ │ │ mismatched│ │ │
│ │ │ IMAGE │ │ │
│ │ │ REGIONS │ │ │
│ │ │ as "to be │ │ │
│ │ │ double- │ │ │
│ │ │ checked" │ │ │
│ │ │ │ │ │
│ │ │ 3. Loop ──►──┤ (back to top with updated data) │ │
│ │ │ │ │ │
│ │ │ 4. If max │ │ │
│ │ │ iterations│ │ │
│ │ │ reached: │ │ │
│ │ │ EXIT with │──────────► PHASE 3 (as human_review) │ │
│ │ │ "human │ │ │
│ │ │ review" │ │ │
│ │ └──────────────┘ │ │
│ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │
└──────────────────────────────┬──────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ PHASE 3: EXPLAINABILITY OUTPUT │
│ │
│ ┌──────────────────────┐ ┌──────────────────────────┐ │
│ │ SUMMARY MODEL │ │ RESULTS TO UI/UX │ │
│ │ (Temperature 0.3) │ │ │ │
│ │ │ │ • FINAL diagnoses │ │
│ │ Input: │ │ • Bounding box coords │ │
│ │ • Patient data │ │ overlaid on image │ │
│ │ • Full logical │ │ • Severity + confidence │ │
│ │ steps chain │ │ per finding │ │
│ │ • FINAL conclusions │ │ • Step-by-step │ │
│ │ • Any unresolved │ │ reasoning chain │ │
│ │ regions │ │ • Human-readable │ │
│ │ │ │ summary narrative │ │
│ │ Output: │ │ • Unresolved regions │ │
│ │ Human-readable │ │ flagged for clinician │ │
│ │ narrative explaining│ │ │ │
│ │ HOW each conclusion │ │ THIS IS THE │ │
│ │ was derived step │ │ "EXPLAINABLE AI" │ │
│ │ by step. │ │ DELIVERABLE │ │
│ └──────────────────────┘ └──────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
- Conclusions Model (high entropy): A "shotgun" approach — finds everything, including edge cases. Favours false positives over false negatives. Better to catch something that isn't there than miss something that is.
- Iterative Model (low entropy): A "first principles" approach — builds conclusions one careful step at a time. Can only reach a FINAL diagnosis when all prerequisite logical steps exist. Assumed to have near-zero hallucination.
- The feedback loop forces convergence: The iterative model slowly builds up to what the conclusions model jumped to immediately. When they agree, we have high confidence. When they don't, we know exactly where and why.
The iterative model's logical steps chain IS the explanation. Each step says "given X, I concluded Y because Z." The summary model just narrates this chain in plain English. There's no black box — every conclusion has a traceable path from raw data to diagnosis.
We'd rather the conclusions model flags 10 things and 3 turn out to be nothing (false positives eliminated by the iterative model's careful checking) than miss 1 real finding (false negative that never gets checked).
Max 5 iterations. If models don't converge, the case exits as human_review with the disagreement areas explicitly flagged. This is a FEATURE — "the AI couldn't reach consensus here, please look at this."
The comparison model outputs JSON with per-conclusion match/mismatch + spatial bounding boxes, not a binary yes/no. This enables precise re-checking of specific image regions.
Logical steps DB grows each cycle. After cycle 3, older steps are summarised to stay within token limits. Only the latest 3 cycles are kept in full detail.
Even when models agree, low-confidence agreement still flags for human review. Both models output confidence scores per finding.
If the iterative model reaches a FINAL conclusion on cycle 1 AND it matches the conclusions model, skip the loop entirely. Saves time and API cost for obvious/clear cases.
Both models output percentage-based bounding boxes: { x_pct, y_pct, w_pct, h_pct } plus named anatomical regions as fallback (e.g. "right upper lobe, anterior segment"). The frontend positions absolutely-placed divs using these percentages over the image.
When conclusions differ between models, the SPECIFIC IMAGE REGIONS where they disagree are highlighted in the logical steps DB with coordinates. The iterative model is instructed to re-examine those exact regions with explicit logical reasoning in the next cycle.
Every exit path produces useful output:
- Converged → Full diagnosis + explanation + confidence
- Human review (max iterations) → Partial diagnosis + explicit disagreement areas + explanation of what was confirmed vs unresolved
- Error → Whatever was collected so far is preserved in DB
The frontend overlays bounding boxes on the radiotherapy image using percentage coordinates from the model output:
.bbox {
position: absolute;
left: ${bbox.x_pct}%;
top: ${bbox.y_pct}%;
width: ${bbox.w_pct}%;
height: ${bbox.h_pct}%;
border: 2px solid <severity-colour>;
}| State | Colour | Border | Meaning |
|---|---|---|---|
| Critical | #ff4d6a (red) | Solid | Both models agreed, high confidence, urgent finding |
| Moderate | #f5a623 (amber) | Solid | Both agreed, moderate confidence or severity |
| Low | #34d399 (green) | Solid | Both agreed, low concern / stable / incidental |
| Human Review | #818cf8 (purple) | Dashed, pulsing | Models disagreed or low confidence — clinician must check |
- Findings — List of findings with severity, confidence bars, model agreement indicator, bounding box coordinates. Click a finding → highlights it on the image.
- Reasoning — Step-by-step chain showing each logical step from the iterative model across all cycles. This is the explainability trail.
- API JSON — Raw structured output for developers / integration.
| Component | Technology |
|---|---|
| Backend | Node.js + Express |
| AI | Anthropic Claude Sonnet 4.5 via @anthropic-ai/sdk |
| Database | SQLite via better-sqlite3 |
| Frontend | React or vanilla HTML/JS (frontend team's choice) |
| Version Control | GitHub (existing private repo) |
All 4 model roles (iterative, conclusions, comparison, summary) use the SAME Claude Sonnet model with DIFFERENT system prompts and temperature settings.
explainable-ai/ # Existing GitHub repo
├── PLAN.md # THIS FILE — the single source of truth
├── package.json
├── .env # API key — GITIGNORED
├── .gitignore
│
├── data/
│ ├── explainable.db # SQLite database (committed — demo data)
│ └── scans/ # Pre-prepared PNGs from DICOM (committed)
│ ├── patient_001_scan_01.png
│ ├── patient_001_scan_02.png
│ └── ...
│
├── server/
│ ├── index.js # Express server entry point
│ │
│ ├── db/
│ │ ├── schema.sql # SQLite table definitions
│ │ ├── seed.sql # Demo patient + case + scan data
│ │ ├── connection.js # better-sqlite3 wrapper
│ │ └── queries.js # Reusable DB query functions
│ │
│ ├── engine/
│ │ ├── orchestrator.js # THE MAIN FEEDBACK LOOP
│ │ ├── iterativeModel.js # Low-entropy prompt + API call
│ │ ├── conclusionsModel.js # High-entropy prompt + API call
│ │ ├── comparisonModel.js # Structured comparison prompt
│ │ └── summaryModel.js # Final explainability narrative
│ │
│ ├── prompts/
│ │ ├── iterative.txt # System prompt for iterative model
│ │ ├── conclusions.txt # System prompt for conclusions model
│ │ ├── comparison.txt # System prompt for comparison model
│ │ └── summary.txt # System prompt for summary model
│ │
│ ├── routes/
│ │ ├── cases.js # Case endpoints
│ │ ├── patients.js # Patient endpoints
│ │ └── results.js # Results endpoints
│ │
│ └── utils/
│ ├── imageLoader.js # Reads PNG from DB path → base64
│ └── logger.js # Cycle-by-cycle debug logging
│
└── frontend/ # Frontend team's domain
└── (React or HTML/JS app)
node_modules/
.env
Everything else is committed, including data/explainable.db and data/scans/*.png (small demo files, not real patient data).
ANTHROPIC_API_KEY=sk-ant-... # Your Anthropic API key
DB_PATH=./data/explainable.db # SQLite file path
MAX_ITERATIONS=5 # Feedback loop cap
PORT=3001 # Express server port-- server/db/schema.sql
CREATE TABLE IF NOT EXISTS patients (
uid TEXT PRIMARY KEY,
name TEXT NOT NULL,
dob TEXT NOT NULL,
nhs_number TEXT UNIQUE NOT NULL,
medical_history TEXT, -- JSON or free text of past records
contact_details TEXT -- JSON: {"phone": "...", "email": "...", "address": "..."}
);
CREATE TABLE IF NOT EXISTS cases (
uid TEXT PRIMARY KEY,
patient_id TEXT NOT NULL REFERENCES patients(uid),
status TEXT DEFAULT 'pending', -- pending | running | converged | human_review | error
created_at TEXT DEFAULT (datetime('now')),
completed_at TEXT,
total_cycles INTEGER DEFAULT 0,
final_output TEXT -- JSON: the full results payload (see API response format)
);
CREATE TABLE IF NOT EXISTS scans (
uid TEXT PRIMARY KEY,
case_id TEXT NOT NULL REFERENCES cases(uid),
image_path TEXT NOT NULL, -- relative path: "data/scans/patient_001_scan_01.png"
scan_type TEXT NOT NULL, -- 'ct_thorax', 'mri_brain', 'xray_chest', etc.
date TEXT NOT NULL,
is_latest INTEGER DEFAULT 0 -- 1 = the newest scan being analysed this case
);
CREATE TABLE IF NOT EXISTS iteration_steps (
uid TEXT PRIMARY KEY,
case_id TEXT NOT NULL REFERENCES cases(uid),
step_number INTEGER NOT NULL,
new_deduction TEXT NOT NULL, -- JSON: what this step concluded
new_reasonings TEXT NOT NULL, -- JSON: reasoning chain for this step
cumulative_deductions TEXT, -- JSON: all deductions up to this point
cumulative_reasonings TEXT, -- JSON: full reasoning chain up to this point
flagged_regions TEXT, -- JSON: [{x_pct, y_pct, w_pct, h_pct, reason}] from comparison
created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS conclusion_runs (
uid TEXT PRIMARY KEY,
case_id TEXT NOT NULL REFERENCES cases(uid),
run_number INTEGER NOT NULL,
triggered_by_iteration_id TEXT REFERENCES iteration_steps(uid),
output_text TEXT NOT NULL, -- JSON: full conclusions array with bboxes
created_at TEXT DEFAULT (datetime('now'))
);The engine reads scans.image_path from SQLite, loads the PNG from disk, converts to base64 for the Claude API.
// server/utils/imageLoader.js
const fs = require('fs');
const path = require('path');
function loadImageAsBase64(imagePath) {
const fullPath = path.resolve(process.cwd(), imagePath);
const buffer = fs.readFileSync(fullPath);
return buffer.toString('base64');
}
module.exports = { loadImageAsBase64 };Express also serves images statically so the frontend can display them:
app.use('/scans', express.static('data/scans'));
// Frontend loads: <img src="http://localhost:3001/scans/patient_001_scan_01.png" />// server/engine/orchestrator.js — PSEUDOCODE (Claude Code builds the real version)
async function runDiagnosis(caseId) {
// ── PHASE 1: LOAD CONTEXT ──────────────────────────────────
const patient = db.getPatientByCase(caseId);
const scans = db.getScans(caseId);
const latestScan = scans.find(s => s.is_latest === 1);
const priorScans = scans.filter(s => s.is_latest === 0);
// Load images from disk paths stored in DB
const latestImageBase64 = loadImageAsBase64(latestScan.image_path);
const priorImagesBase64 = priorScans.map(s => loadImageAsBase64(s.image_path));
// ── PHASE 2: FEEDBACK LOOP ─────────────────────────────────
let logicalStepsDB = [];
let flaggedRegions = [];
let cycle = 0;
let converged = false;
let iterativeResult, conclusionsResult, comparison;
db.updateCaseStatus(caseId, 'running');
while (cycle < MAX_ITERATIONS && !converged) {
cycle++;
// 2a. Run BOTH models in PARALLEL
[iterativeResult, conclusionsResult] = await Promise.all([
iterativeModel.run({
patient,
latestImageBase64,
priorImagesBase64,
logicalSteps: logicalStepsDB,
flaggedRegions, // "double-check these specific areas"
}),
conclusionsModel.run({
patient,
latestImageBase64,
priorImagesBase64,
logicalSteps: logicalStepsDB,
})
]);
// 2b. Save iteration step to DB
const stepId = db.saveIterationStep({
caseId,
stepNumber: cycle,
newDeduction: JSON.stringify(iterativeResult.deduction),
newReasonings: JSON.stringify(iterativeResult.reasoning),
cumulativeDeductions: JSON.stringify(
logicalStepsDB.map(s => s.deduction).concat(iterativeResult.deduction)
),
cumulativeReasonings: JSON.stringify(
logicalStepsDB.map(s => s.reasoning).concat(iterativeResult.reasoning)
),
flaggedRegions: JSON.stringify([]),
});
// 2c. Save conclusion run to DB
db.saveConclusionRun({
caseId,
runNumber: cycle,
triggeredByIterationId: stepId,
outputText: JSON.stringify(conclusionsResult),
});
// 2d. COMPARE — do the models' FINAL conclusions match?
comparison = await comparisonModel.run({
iterativeFinals: iterativeResult.finalConclusions,
conclusionsFinals: conclusionsResult.final_conclusions,
});
// 2e. BRANCH
if (comparison.allMatched) {
converged = true;
// ONE-SHOT ESCAPE: if cycle 1 and matched, we're done immediately
} else {
// Append iterative model's step to the logical chain
logicalStepsDB.push({
step: cycle,
deduction: iterativeResult.deduction,
reasoning: iterativeResult.reasoning,
region: iterativeResult.region,
});
// Flag mismatched regions for iterative model to re-examine
flaggedRegions = comparison.mismatchedRegions;
// Save flagged regions back to this step's DB record
db.updateIterationStepFlags(stepId, JSON.stringify(flaggedRegions));
// CONTEXT WINDOW MANAGEMENT: summarise old steps after cycle 3
if (cycle >= 3) {
// Keep latest 3 full, summarise older ones
// (implementation detail for Claude Code)
}
}
}
// ── PHASE 3: GENERATE EXPLANATION ──────────────────────────
const summary = await summaryModel.run({
patient,
logicalSteps: logicalStepsDB,
finalConclusions: iterativeResult.finalConclusions || [],
unresolvedRegions: converged ? [] : flaggedRegions,
});
// Save final output
const finalOutput = {
findings: (iterativeResult.finalConclusions || []).map((f, i) => ({
id: `F${i + 1}`,
label: f.conclusion,
severity: f.severity,
confidence: f.confidence,
bbox: f.region,
models_agreed: comparison.matched?.some(m =>
m.iterative === f.conclusion
) || false,
converged_cycle: cycle,
})),
unresolved: converged ? [] : (flaggedRegions || []).map(r => ({
bbox: r,
reason: r.reason,
severity: 'human_review',
})),
reasoning: logicalStepsDB.map((s, i) => ({
step: i + 1,
cycle: s.step,
deduction: s.deduction,
reasoning: s.reasoning,
})),
summary: summary,
};
db.updateCase(caseId, {
status: converged ? 'converged' : 'human_review',
totalCycles: cycle,
completedAt: new Date().toISOString(),
finalOutput: JSON.stringify(finalOutput),
});
return finalOutput;
}All 4 roles use claude-sonnet-4-5-20250514 via the Anthropic SDK. They differ in system prompt and temperature.
- Temperature: 0.1
- Max tokens: 2000
- System prompt purpose: "You are a meticulous, cautious radiologist. Given the patient data, images, and the existing chain of logical steps, output ONLY the single next logical medical conclusion. No logical leaps. One step at a time. You may ONLY output a FINAL diagnosis when ALL prerequisite logical steps have been established in the chain. If flagged regions are provided, you MUST address those specific regions with explicit logical reasoning."
- Output JSON format:
{
"deduction": "The 2.4cm nodule from prior CT has grown to 2.8cm — 4mm interval growth in 6 weeks",
"reasoning": "Comparing measurement on slice 142 of current scan vs slice 138 of 12-Jan scan. Growth confirmed by measuring longest axis in axial plane.",
"region": { "x_pct": 53, "y_pct": 27, "w_pct": 16, "h_pct": 14 },
"anatomical_region": "Right upper lobe, anterior segment",
"is_final": false,
"final_conclusions": null
}When is_final: true:
{
"deduction": "...",
"reasoning": "...",
"region": { ... },
"anatomical_region": "...",
"is_final": true,
"final_conclusions": [
{
"conclusion": "Suspicious RUL mass with interval growth — recommend tissue sampling / PET-CT",
"severity": "critical",
"confidence": 0.94,
"region": { "x_pct": 53, "y_pct": 27, "w_pct": 16, "h_pct": 14 },
"anatomical_region": "Right upper lobe, anterior segment"
}
]
}- Temperature: 0.7
- Max tokens: 3000
- System prompt purpose: "You are a thorough radiologist performing a comprehensive review. Given the patient data, images, and any existing logical steps, identify ALL possible findings, abnormalities, and diagnoses. Err heavily on the side of inclusion — it is far better to flag something that turns out to be benign than to miss a real finding. Output every finding with a bounding box region on the image."
- Output JSON format:
{
"final_conclusions": [
{
"conclusion": "Right upper lobe mass — suspected malignancy",
"severity": "critical",
"confidence": 0.94,
"region": { "x_pct": 53, "y_pct": 27, "w_pct": 16, "h_pct": 14 },
"anatomical_region": "Right upper lobe, anterior segment"
},
{
"conclusion": "Right hilar lymphadenopathy — possibly metastatic",
"severity": "moderate",
"confidence": 0.78,
"region": { "x_pct": 33, "y_pct": 44, "w_pct": 12, "h_pct": 10 },
"anatomical_region": "Right hilum"
}
]
}- Temperature: 0.0
- Max tokens: 1500
- System prompt purpose: "You are a medical logic comparator. Given two sets of medical conclusions (one from an iterative model, one from a conclusions model), determine which conclusions match semantically and which don't. Two conclusions match if they refer to the same finding/region even if worded differently. Output structured JSON."
- Output JSON format:
{
"allMatched": false,
"matched": [
{
"iterative": "Suspicious RUL mass with interval growth",
"conclusions": "Right upper lobe mass — suspected malignancy",
"region": { "x_pct": 53, "y_pct": 27, "w_pct": 16, "h_pct": 14 }
}
],
"mismatched": [
{
"source": "conclusions_only",
"conclusion": "Left paratracheal node borderline enlargement",
"region": { "x_pct": 28, "y_pct": 35, "w_pct": 10, "h_pct": 8 },
"reason": "Iterative model has not reached this conclusion — insufficient logical steps"
}
],
"mismatchedRegions": [
{
"x_pct": 28, "y_pct": 35, "w_pct": 10, "h_pct": 8,
"reason": "Left paratracheal node — flagged by conclusions model, not confirmed by iterative model"
}
]
}- Temperature: 0.3
- Max tokens: 2000
- System prompt purpose: "You are a medical report writer. Given the patient data, the full chain of logical reasoning steps, and the final conclusions, write a clear human-readable narrative that explains step-by-step how each diagnosis was reached. If there are unresolved regions, explain what was found and why the system could not reach consensus."
- Output: Plain text narrative string.
Triggers the feedback loop asynchronously. Returns immediately:
{ "status": "running", "caseId": "case_001" }Poll this while the loop is running:
{ "status": "running", "currentCycle": 2, "maxCycles": 5 }Full output when complete:
{
"status": "converged",
"totalCycles": 3,
"findings": [
{
"id": "F1",
"label": "Right Upper Lobe Mass",
"severity": "critical",
"confidence": 0.94,
"bbox": { "x_pct": 53, "y_pct": 27, "w_pct": 16, "h_pct": 14 },
"anatomical_region": "Right upper lobe, anterior segment",
"models_agreed": true,
"converged_cycle": 1
},
{
"id": "F2",
"label": "Left Paratracheal Node",
"severity": "human_review",
"confidence": 0.42,
"bbox": { "x_pct": 28, "y_pct": 35, "w_pct": 10, "h_pct": 8 },
"anatomical_region": "Station 4L",
"models_agreed": false,
"flagged_reason": "max_iterations_no_convergence"
}
],
"reasoning": [
{
"step": 1,
"cycle": 1,
"deduction": "Patient has 20 pack-year smoking history. Baseline risk elevated.",
"reasoning": "Extracted from medical_history field."
},
{
"step": 2,
"cycle": 1,
"deduction": "Prior CT showed 2.4cm RUL nodule. Current shows 2.8cm.",
"reasoning": "Comparing slice 142 current vs slice 138 prior."
}
],
"summary": "The analysis identified a 2.8cm spiculated mass in the right upper lobe...",
"unresolved": [
{
"bbox": { "x_pct": 28, "y_pct": 35, "w_pct": 10, "h_pct": 8 },
"reason": "Left paratracheal node — borderline measurement, models did not converge"
}
]
}// server/db/queries.js — functions the engine and routes need
getPatientByCase(caseId) // JOIN patients + cases → patient object
getScans(caseId) // All scans for a case, ordered by date
saveIterationStep({ caseId, stepNumber, ... }) // INSERT → iteration_steps, return uid
saveConclusionRun({ caseId, runNumber, ... }) // INSERT → conclusion_runs, return uid
updateIterationStepFlags(stepId, flaggedRegions) // UPDATE iteration_steps.flagged_regions
updateCaseStatus(caseId, status) // UPDATE cases.status
updateCase(caseId, { status, totalCycles, completedAt, finalOutput })
getCaseResults(caseId) // SELECT case with final_output parsed
getCaseStatus(caseId) // SELECT status + total_cycles for polling
getAllPatients() // SELECT * FROM patients
getCasesForPatient(patientId) // SELECT * FROM cases WHERE patient_id = ?cd your-existing-repoSave this entire document as PLAN.md in the repo root.
echo "ANTHROPIC_API_KEY=sk-ant-your-key-here" > .env
echo "DB_PATH=./data/explainable.db" >> .env
echo "MAX_ITERATIONS=5" >> .env
echo "PORT=3001" >> .envmkdir -p data/scansDrop your pre-prepared PNG scans into data/scans/.
Read PLAN.md in the project root. This is the COMPLETE architecture for an
explainable AI medical diagnosis system. It contains the full system design,
database schema, feedback loop logic, model configurations, API endpoints,
and output formats.
Build the complete Node.js backend following that plan exactly.
Key points:
- Images are loaded from file paths stored in the SQLite scans table (NO upload)
- Use @anthropic-ai/sdk with claude-sonnet-4-5-20250514 for all 4 model roles
- Each model role has a different temperature:
iterative=0.1, conclusions=0.7, comparison=0.0, summary=0.3
- The feedback loop in orchestrator.js runs iterative + conclusions models
in parallel with Promise.all, then compares, then loops or exits
- Use better-sqlite3 for SQLite
- Serve data/scans/ as static files for frontend image display
- All JSON output formats are specified in PLAN.md sections 2.8 and 2.9
Build in this order:
1. package.json (express, @anthropic-ai/sdk, better-sqlite3, dotenv, uuid, cors)
2. .gitignore
3. server/db/connection.js
4. server/db/schema.sql
5. server/db/queries.js (all functions listed in PLAN.md section 2.10)
6. server/utils/imageLoader.js
7. server/utils/logger.js
8. server/prompts/ — all 4 prompt .txt files with full system prompts
9. server/engine/iterativeModel.js
10. server/engine/conclusionsModel.js
11. server/engine/comparisonModel.js
12. server/engine/summaryModel.js
13. server/engine/orchestrator.js — the main feedback loop
14. server/routes/patients.js
15. server/routes/cases.js (including the POST /run that triggers orchestrator)
16. server/routes/results.js
17. server/index.js — entry point with CORS, JSON parsing, static files, routes
npm install
node server/index.jsTest with:
curl -X POST http://localhost:3001/api/cases/case_001/run
# Poll:
curl http://localhost:3001/api/cases/case_001/status
# Get results:
curl http://localhost:3001/api/cases/case_001/resultsgit add .
git commit -m "feat: complete explainable AI engine with feedback loop"
git push- Read this PLAN.md — specifically sections 2.5 (schema) and 2.10 (query functions)
- Create seed data in
server/db/seed.sql:- 1-2 demo patients with realistic medical_history (smoking history, prior conditions, etc.)
- 1 case per patient
- 2-3 scans per case, with
image_pathpointing to actual PNGs indata/scans/and one markedis_latest = 1
- Verify queries.js — Claude Code will generate this, but check it matches the schema
- Test the DB independently:
sqlite3 data/explainable.db < server/db/schema.sql sqlite3 data/explainable.db < server/db/seed.sql sqlite3 data/explainable.db "SELECT * FROM patients;"
- Read this PLAN.md — specifically sections 1.5 (UI display), 2.9 (API endpoints)
- Set up project in
/frontendfolder - Build these views:
- Patient list → Case list → Results view
- Results view: image with bbox overlays + findings sidebar + reasoning chain
- API integration:
POST /api/cases/:id/runto trigger analysis- Poll
GET /api/cases/:id/statusevery 2 seconds - Fetch
GET /api/cases/:id/resultswhen status isconvergedorhuman_review - Load images from
GET /scans/:filename
- Bbox overlay code:
{findings.map(f => ( <div key={f.id} style={{ position: 'absolute', left: `${f.bbox.x_pct}%`, top: `${f.bbox.y_pct}%`, width: `${f.bbox.w_pct}%`, height: `${f.bbox.h_pct}%`, border: `2px ${f.severity === 'human_review' ? 'dashed' : 'solid'} ${severityColours[f.severity]}`, }} /> ))}
- Reference the interactive prototype from our conversation for layout patterns
| Time | Algorithm Lead (You) | DB Teammate | Frontend |
|---|---|---|---|
| 0-1h | PLAN.md in repo, Claude Code scaffolds engine | Schema + seed data + test queries | Project setup, layout scaffold |
| 1-3h | Prompt engineering — test individual model calls | queries.js + integration with engine | Patient list + case list views |
| 3-5h | Full feedback loop testing, tune temperatures | Fix edge cases, help test API | Image overlay + bbox rendering |
| 5-7h | End-to-end: DB → engine → API → frontend | API endpoint testing | Connect to API, polling, results view |
| 7-8h | Cache a successful demo run as backup | Seed compelling demo case | Reasoning chain tab, polish |
| 8h+ | Practice demo, prepare architecture slides | Help with slides | Final visual polish |
| Risk | Mitigation |
|---|---|
| Claude outputs unreliable bounding boxes | Models also output anatomical_region strings; maintain a lookup table mapping regions → approximate % coords as fallback |
| Feedback loop never converges | Max 5 iterations + human_review exit = graceful degradation. This is a feature for the demo. |
| API rate limits / slowness during demo | Cache one complete successful run's JSON. Serve from cache if API is slow. Judges won't know. |
| Context window fills up after many cycles | Summarise logical steps older than 3 cycles. Keep only latest 3 in full. |
| PNG resolution too low for Claude to analyse | Use high-res PNGs (1024x1024+). Include scan metadata (slice number, window/level) in the text prompt. |
| Team merge conflicts | Engine (server/engine/), DB (server/db/), frontend (frontend/) are in separate folders — minimal overlap. |
| Iterative model never reaches FINAL | Add a check: if iterative model has produced N steps without reaching FINAL, nudge it with "you have established sufficient steps, please evaluate whether a FINAL conclusion can now be made" |