Aegis Triage is an internship-quality symptom-triage prototype with a Streamlit chat interface. It gathers a small set of patient-reported facts and returns exactly one controlled outcome: Emergency, Urgent, Routine, or Self-care.
This project is a demonstration prototype, not a diagnostic system or a clinically validated medical device. It does not diagnose disease, prescribe treatment, recommend medication dosages, or replace a healthcare professional.
The application deliberately separates language understanding from safety decisions:
- A local deterministic pre-screen checks every new message for explicit emergency warning signs before any Anthropic API call.
- Claude Sonnet 5 uses required tool calling to extract structured facts and exact quotes from only the newest patient message. It cannot submit a triage level.
- Deterministic semantic evidence checks verify quote presence, polarity, patient subject, certainty, current timing, numbers, duration, and progression. Unsupported facts are discarded and remain unknown.
- Pydantic validates every update and session state. Missing information remains
None; it is never silently converted toFalse. - Contradictory safety-relevant answers retain both values in structured conflict state. Classification pauses until the patient resolves the conflict.
- Deterministic Python rules run the emergency registry again over validated state and make every triage decision.
- Controlled templates render the reason, action, safety net, and the configured emergency number 111.
Claude interprets patient wording. Python owns policy, precedence, and final outcomes. Patient text is treated as untrusted data and cannot override these rules.
app.py Streamlit UI and session state
assets/styles.css Shared responsive component styling
assets/theme-light.css Light-theme colour variables
assets/theme-dark.css Dark-theme colour variables
triage/config.py Environment configuration
triage/models.py Pydantic facts, conflicts, and response contracts
triage/evidence.py Deterministic semantic evidence validation
triage/rules.py Typed emergency registry and four-level policy
triage/llm_client.py Anthropic required-tool extraction adapter
triage/conversation.py Workflow, conflicts, failures, and bounded clarification
evaluation/cases.json Regression and post-rule adversarial regression fixtures
scripts/evaluate.py Split local metrics runner; never calls Claude
tests/ Unit, workflow, evaluation, integration, and UI tests
The sidebar Dark mode control switches between accessible light and dark themes. The shared layout lives in assets/styles.css; each theme file supplies colour variables and contrast-specific semantic result shades. The choice is stored in Streamlit session state, survives ordinary reruns and Start new assessment, and never alters messages or clinical state. Visible result labels remain present because colour is not the only outcome signal.
- Emergency: any specifically defined emergency rule below. The controlled action tells the user to call 111 or go to the nearest emergency department now.
- Urgent: reported severity of 7–10, worsening symptoms, or confusion when no Emergency rule matches. The controlled action is an in-person assessment within 24 hours.
- Self-care: severity 0–3, duration no longer than 72 hours, improving or stable progression, and every required emergency warning sign explicitly answered as absent.
- Routine: the controlled fallback for a complete case that matches no Emergency, Urgent, or Self-care rule.
The thresholds above are a small demonstration policy, not a clinical guideline.
The same registry is used for pre-API screening and validated-state evaluation. Each rule has a stable code and independently testable predicate.
- Chest pain together with breathing difficulty
- Severe or uncontrolled bleeding
- Loss of consciousness or unresponsiveness
- Severe breathing difficulty
- A conservative combination of at least two stroke warning signs: facial drooping, one-sided weakness, and speech difficulty
- Throat or tongue swelling together with breathing difficulty
- Ongoing or repeated seizure, or failure to regain responsiveness
- Suspected poisoning or overdose expressed through scoped, current patient language
- Explicit immediate suicide or serious self-harm danger
- Major trauma together with severe bleeding, loss of consciousness, or breathing difficulty
Local phrase handling checks negation, uncertainty, hypothetical and informational wording, historical context, and third-person subjects. It intentionally favors scoped combinations over broad disease keywords. It cannot recognize every way a real patient may describe an emergency.
Python 3.11 or newer is recommended. In Windows PowerShell:
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install -r requirements.txt
Copy-Item .env.example .envEdit the copied .env file:
ANTHROPIC_API_KEY=your_real_api_key
ANTHROPIC_MODEL=claude-sonnet-5
EMERGENCY_NUMBER=111
MAX_FIELD_CLARIFICATION_ATTEMPTS=2Never commit .env. The repository keeps .env.example as a safe template and .gitignore excludes local secrets.
Start the app:
streamlit run app.pyWithout an API key, the app still performs local emergency pre-screening but refuses to guess a non-emergency triage level.
Run normal tests without authorizing an external API call:
python -m pytest -m "not integration"Run the fixture-labelled deterministic evaluation:
python scripts/evaluate.pyThe evaluator reports the original regression set, the separately marked unseen/adversarial set, and their combined result. Each section includes case count, emergency recall, emergency precision, false positives, false negatives, per-rule pass rates, and overall deterministic case pass rate. Labels come from evaluation/cases.json, never from Claude. The explicit emergency fixture sets target 100% recall.
The test types mean different things:
- Unit and workflow tests check code contracts with mocks and deterministic extractors. Passing them demonstrates that the implemented code behaves as asserted.
- Deterministic evaluation results measure only the curated local fixture set. They are regression results, not medical accuracy or population-level performance.
- Optional real-API integration results check whether the configured Claude model can complete one structured extraction request at that moment. They do not validate triage policy or clinical performance.
- Clinical validation has not been performed. No test or fixture result in this repository should be described as clinical safety, clinical accuracy, or medical-device validation.
Normal test runs skip the integration test. It can make a billable external request, so run it only when you intentionally authorize that request and have configured ANTHROPIC_API_KEY:
$env:RUN_ANTHROPIC_INTEGRATION_TEST = "1"
python -m pytest -m integration tests/test_anthropic_integration.py -vv -p no:cacheprovider
Remove-Item Env:RUN_ANTHROPIC_INTEGRATION_TESTThe claude-sonnet-5 request retains required tool use, a 25-second client timeout, SDK retries, and one retry for invalid structured output. It does not send non-default temperature, top_p, or top_k parameters.
Local developer logs record only sanitized exception class, HTTP status code, and request ID when those values are available. The application does not log API keys, complete patient messages, or raw Anthropic exception text, and patient-facing failures remain non-technical.
Messages are not intentionally logged by this application. Non-emergency messages are sent to the Anthropic API for language processing. Avoid entering names, identification numbers, contact details, or other unnecessary personal information.
Current case state and chat history are held in Streamlit session state and reset by Start new assessment. This repository does not claim that data never leaves the device; Anthropic API processing is external and is subject to the service configuration and applicable terms.
- This is an English-language demonstration with deliberately limited phrase patterns and a small triage policy.
- It has no physical examination, vital signs, medical records, clinician review, or location-aware emergency routing.
- Emergency phrase screening can miss novel, vague, misspelled, multilingual, or adversarial wording and can still produce false positives.
- Semantic evidence checks are deterministic and reviewable but are not a full natural-language understanding system.
- The Claude extractor may fail, refuse, time out, or return malformed tool output. In those cases the app assigns no non-emergency level.
- After repeated unclear answers, the app uses a constrained response format and then stops without guessing, directing the user to professional advice.
- Rules, thresholds, warning signs, privacy controls, accessibility, localization, security, and usability have not been clinically or regulatorily validated.
- A production healthcare system would require clinician-authored policy, representative safety studies, formal risk management, monitoring, incident response, privacy/security review, and applicable regulatory approval.