An agentic AI system that reads patient source-note PDFs and produces structured, clinically safe discharge summary drafts for clinician review.
pip install pdfplumber
python main.pyOutputs appear in ./outputs/.
The agent uses a Plan → Act → Observe → Re-plan loop:
PLAN (what do I need?)
↓
TOOL CALL (pdf_extract / drug_check / escalation / conflict_detect)
↓
OBSERVE (what did I get? is it complete?)
↓
RE-PLAN (what's still missing? any conflicts?)
↓
COMPILE (structured summary with all flags)
MAX_STEPS = 30— agent cannot run forever- Every step emits:
reasoning → action → inputs → result → next_plan - Tools can fail — agent retries, falls back, or reports — never crashes
| Tool | Purpose | Agent decides when |
|---|---|---|
PDFExtractor |
Extract text from PDFs (pdfplumber + OCR fallback) | Always first |
DrugInteractionChecker |
Check med interactions from curated DB | After medication extraction |
EscalationTool |
Log safety-critical items for clinician | When drug interaction/conflict found |
ConflictDetector |
Final pass for cross-field conflicts | Before compilation |
This is the core safety constraint. Implementation:
- Every field initialised to
MISSING_MARKER = "[MISSING — FLAG FOR CLINICIAN REVIEW]" - Fields are only overwritten when sourced from a document
- Pending results:
PENDING_MARKER = "[PENDING — RESULT NOT YET AVAILABLE]" - No inference, no "reasonable defaults", no hallucination
- The guardrail is hard-coded — not part of the learnable policy (Part 2)
- Tool failure: caught in
_step(), field left as MISSING, flag raised - Missing data: explicit marker + flag for clinician
- Conflicting information: both values preserved, conflict logged, clinician flagged
- Example (Patient 2): Admission record final dx ≠ consultation sheets dx
- Example (Patient 2): ABG sodium (114) ≠ Biochemistry sodium (127) on same day
- Example (Patient 2): Urine C/S (no bacteriuria) ≠ CT KUB (bilateral pyelonephritis)
- DAMA: both patients flagged as discharged against advice/on request
- Pending results: explicitly listed in Section 9
reward = 0.4 × (1 - normalized_edit_distance) + 0.6 × section_accuracy
normalized_edit_distance: Levenshtein distance / max_length ∈ [0,1]section_accuracy: fraction of sections with NED < 0.2- Higher reward = less editing needed
Simulated Reviewer (Hidden Policy)
The SimulatedDoctorReviewer applies a consistent, hidden editing policy:
- Replace vague dose entries ("Not specified") with standard phrasing
- Truncate hospital course to concise clinical narrative (≤10 sentences)
- Keep only top 5 critical flags
- Add discharge sodium annotation if missing
- Standardise DAMA documentation
- Normalise dates to DD-MMM-YYYY
- Append clinician sign-off line
- After each doctor edit, structured lessons are extracted
- Lessons stored in
correction_memory.json - Next agent run injects top-K lessons into planning context
- No GPU/fine-tuning required; immediately measurable; transparent
See outputs/learning_loop_report.txt for full curve.
1. Cold Start Problem With 2 synthetic patients, the correction memory is sparse. First iterations have no lessons. Mitigation: Seed with curated clinician-reviewed corrections.
2. Gaming Risk An agent could lower edit distance by producing vaguer/shorter summaries. Mitigation: Section accuracy (0.6 weight) penalises missing/incomplete sections regardless of length.
3. Safety Preservation The correction memory only affects formatting/style — never the no-fabrication guardrail. Even after learning: MISSING markers are preserved, conflicts are still flagged, no lab value or medication dose is ever invented.
4. Distribution Shift Simulated doctor has a fixed policy. Real doctors vary. Mitigation in production: Per-clinician correction memories.
5. Limited Data 2 patients → minimal signal. Real improvement needs 50+ pairs. The mechanism is correct; data volume is the constraint.
discharge_agent/
├── main.py # Entry point — runs all patients + learning loop
├── agents/
│ ├── discharge_agent.py # Patient 2 agent (DKA + full chart)
│ ├── patient1_agent.py # Patient 1 agent (GE + UTI)
│ ├── report_formatter.py # Formats summaries, traces, JSON
│ └── learning_loop.py # Part 2: simulated reviewer + correction memory
├── tools/
│ ├── pdf_extractor.py # PDF text extraction (pdfplumber + OCR)
│ ├── drug_interaction.py # Drug interaction checker (curated DB)
│ ├── escalation.py # Escalation logger
│ └── conflict_detector.py # Cross-field conflict detector
└── outputs/ # All generated outputs
├── Patient_1_*_discharge_summary.txt
├── Patient_1_*_step_trace.txt
├── Patient_1_*_full_result.json
├── Patient_2_*_discharge_summary.txt
├── Patient_2_*_step_trace.txt
├── Patient_2_*_full_result.json
├── learning_loop_report.txt
├── learning_metrics.json
└── correction_memory.json
- OCR pipeline: Integrate Tesseract/Google Vision for handwritten page extraction
- LLM extraction layer: Use Claude API to extract structured fields from raw OCR text
- Vector similarity: Use embeddings to match lessons to relevant sections (vs. keyword)
- Multi-patient evaluation: Run on 20+ patients to show meaningful improvement curve
- Per-section reward: Track which sections improve fastest to focus learning
- DPO fine-tuning: Collect 100+ (draft, edited) pairs and fine-tune a small model
- Clinician UI: Web interface for reviewing, editing, and auto-submitting corrections back
- Real drug interaction API: Integrate DrugBank/OpenFDA instead of curated mock DB
- All output is explicitly marked DRAFT FOR CLINICIAN REVIEW
- No output should be used as a finalised clinical document without clinician sign-off
- DAMA (Discharge Against Medical Advice) events are prominently flagged
- Pending results are listed separately so clinicians cannot miss them
- Conflicting information is preserved — never silently resolved
Built for the Dscribe AI Engineer Take-Home Assignment. All patient data is synthetic.