Skip to content

Latest commit

 

History

History
898 lines (772 loc) · 41.2 KB

File metadata and controls

898 lines (772 loc) · 41.2 KB

ExplainDx — Complete System Plan

Explainable AI Medical Diagnosis System · Dublin Hack Europe 2026


PART 1: SYSTEM ARCHITECTURE & THEORY


1.1 The Core Idea

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.


1.2 Full System Schematic

┌─────────────────────────────────────────────────────────────────────────┐
│  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             │              │
│  └──────────────────────┘    └──────────────────────────┘              │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

1.3 Why This Architecture Works

The Dual-Model Strategy

  • 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.

Why This Is Explainable

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.

The "Over-Code Rather Than Under-Code" Philosophy

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).


1.4 System Improvements (built into the design)

① Termination Cap

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."

② Structured Comparison

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.

③ Context Window Management

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.

④ Confidence Thresholds

Even when models agree, low-confidence agreement still flags for human review. Both models output confidence scores per finding.

⑤ One-Shot Escape

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.

⑥ Spatial Region Format

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.

⑦ Flagged Region Double-Checking

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.

⑧ Graceful Degradation

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

1.5 UI/UX Results Display

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>;
}

Four Severity States

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

UI Tabs

  1. Findings — List of findings with severity, confidence bars, model agreement indicator, bounding box coordinates. Click a finding → highlights it on the image.
  2. Reasoning — Step-by-step chain showing each logical step from the iterative model across all cycles. This is the explainability trail.
  3. API JSON — Raw structured output for developers / integration.

PART 2: IMPLEMENTATION


2.1 Tech Stack

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.


2.2 Project Structure

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)

2.3 .gitignore

node_modules/
.env

Everything else is committed, including data/explainable.db and data/scans/*.png (small demo files, not real patient data).


2.4 Environment Variables (.env) — GITIGNORED

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

2.5 Database Schema

-- 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'))
);

2.6 Image Loading (DB is source of truth — NO uploads)

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" />

2.7 Core Engine — Orchestrator Feedback Loop

// 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;
}

2.8 Model Configurations

All 4 roles use claude-sonnet-4-5-20250514 via the Anthropic SDK. They differ in system prompt and temperature.

Iterative Model

  • 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"
    }
  ]
}

Conclusions Model

  • 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"
    }
  ]
}

Comparison Model

  • 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"
    }
  ]
}

Summary 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.

2.9 API Endpoints

POST /api/cases/:caseId/run

Triggers the feedback loop asynchronously. Returns immediately:

{ "status": "running", "caseId": "case_001" }

GET /api/cases/:caseId/status

Poll this while the loop is running:

{ "status": "running", "currentCycle": 2, "maxCycles": 5 }

GET /api/cases/:caseId/results

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"
    }
  ]
}

GET /api/patients

GET /api/patients/:id/cases

GET /api/scans/:caseId

Static: GET /scans/:filename → serves from data/scans/


2.10 DB Query Functions (for DB teammate)

// 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 = ?

PART 3: EXECUTION PLAN


3.1 Your Steps Right Now (in order)

Step 1: Clone and set up

cd your-existing-repo

Step 2: Add this file

Save this entire document as PLAN.md in the repo root.

Step 3: Create .env

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" >> .env

Step 4: Add demo images

mkdir -p data/scans

Drop your pre-prepared PNG scans into data/scans/.

Step 5: Open Claude Code in VS Code and paste this prompt:

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

Step 6: Let Claude Code build — then test

npm install
node server/index.js

Test 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/results

Step 7: Commit and push

git add .
git commit -m "feat: complete explainable AI engine with feedback loop"
git push

3.2 DB Teammate — What to Do

  1. Read this PLAN.md — specifically sections 2.5 (schema) and 2.10 (query functions)
  2. 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_path pointing to actual PNGs in data/scans/ and one marked is_latest = 1
  3. Verify queries.js — Claude Code will generate this, but check it matches the schema
  4. 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;"

3.3 Frontend Teammate — What to Do

  1. Read this PLAN.md — specifically sections 1.5 (UI display), 2.9 (API endpoints)
  2. Set up project in /frontend folder
  3. Build these views:
    • Patient list → Case list → Results view
    • Results view: image with bbox overlays + findings sidebar + reasoning chain
  4. API integration:
    • POST /api/cases/:id/run to trigger analysis
    • Poll GET /api/cases/:id/status every 2 seconds
    • Fetch GET /api/cases/:id/results when status is converged or human_review
    • Load images from GET /scans/:filename
  5. 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]}`,
      }} />
    ))}
  6. Reference the interactive prototype from our conversation for layout patterns

3.4 Hackathon Timeline

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

3.5 Risks & Mitigations

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"