A turn-based, deterministic EMS training simulator with LLM-rendered roleplay.
Live demo: emt-sim.vercel.app
The demo is a hosted instance for evaluation purposes. Depending on how it's currently configured, it may run with a
mockLLM provider (deterministic template text instead of real generated dialogue) or a live provider with rate limits. See Configuration if you want your own instance with full LLM roleplay.
SceneSim puts a learner through a prehospital EMS call — dispatch, scene arrival, patient assessment, treatment, transport, and radio handoff — as a turn-based text simulation. The core design principle:
A deterministic engine owns all patient truth. Vitals, deterioration, contraindications, and scoring are computed by plain Python logic against scenario data. The LLM is used only to render that engine-computed state as natural-language dialogue (patient, caregiver, proctor). It never decides what happens medically, and it cannot reveal information the engine hasn't authorized.
This means:
- Reproducible — the same scenario, seed, and actions always produce the same clinical outcome.
- Gradeable — scoring is a deterministic function of what the learner actually did, not an LLM's opinion.
- Safe to extend without an LLM — the entire engine is unit-testable, and a
mockLLM mode renders template text with no external API calls at all. - Content-driven — new scenarios and county protocols are JSON files validated against schemas. No code changes are needed to add a case.
Important
SceneSim is an educational training tool. It is not a certified medical device, not a source of medical advice, and must not be used to guide real patient care.
- Architecture
- Repository layout
- Getting started
- Configuration
- Testing
- Authoring content
- Deployment
- Documentation index
- Contributing
- Citing this project
- Related work
- License
┌──────────────────────────────────────────────┐
│ Frontend (Next.js) │
│ Chat UI / Action Palette / Panels │
├──────────────────────────────────────────────┤
│ API Layer (FastAPI) │
│ Routes: /scenarios, /sessions, /protocols │
├──────────────────────────────────────────────┤
│ Services Layer │
│ scenario_loader │ protocol_resolver │ llm │
│ evaluation │ transcript │ report │ intent │
├──────────────────────────────────────────────┤
│ Engine Layer (Deterministic) │
│ sim_engine │ state_machine │ action_resolver │
│ reveal_rules │ deterioration │ guardrails │
│ scoring │ randomizer │ vitals_model │
├──────────────────────────────────────────────┤
│ Models Layer (Pydantic) │
│ scenario │ protocol │ session │ action │
│ turn │ review │
├──────────────────────────────────────────────┤
│ DB / Repository Layer │
│ SQLite (dev) → Postgres (prod) │
├──────────────────────────────────────────────┤
│ Content Layer (JSON files) │
│ scenarios │ protocols │ action_catalog │
│ schemas │ prompts │
└──────────────────────────────────────────────┘
A learner turn flows: frontend → POST /sessions/{id}/turns → deterministic engine resolves
the action, checks contraindications, reveals findings, applies deterioration → the engine's
facts are handed to the LLM only to be phrased as dialogue → response returned and persisted.
See docs/architecture.md for the full breakdown and
docs/prompt_design.md for exactly what the LLM is and isn't allowed to see.
This is a two-app repo developed and run independently:
| Path | What it is |
|---|---|
app/, components/, lib/, hooks/ (repo root) |
The Next.js 16 / React 19 frontend |
apps/api/ |
The FastAPI + Pydantic v2 backend (the simulation engine lives here) |
content/ |
Scenarios, county protocols, the action catalog, and their JSON Schemas |
scripts/ |
Dev utility scripts (content validation, dev server, session seeding) |
docs/ |
Architecture, API, authoring, and evaluation-rubric guides |
apps/web is a vestigial placeholder package (not the real frontend) — ignore it; the working
Next.js app is at the repo root. See CLAUDE.md for a deeper architectural tour aimed
at contributors working in the codebase.
- Node.js 20+ and npm (or pnpm)
- Python 3.12+
- Optionally, a Together.ai API key if you want real LLM-rendered
dialogue instead of the deterministic
mockprovider
git clone https://github.com/evanqua/scenesim.git
cd scenesim
cp .env.example .envThe defaults in .env.example run entirely locally: SQLite database, LLM_PROVIDER=mock (no
API key required). Edit .env if you want to point at Together.ai or Postgres — see
Configuration.
cd apps/api
pip install -e ".[dev]"
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000Or, from the repo root, use the convenience wrapper: python scripts/dev_server.py.
The API is now at http://localhost:8000 (interactive docs at http://localhost:8000/docs).
In a second terminal, from the repo root:
npm install
npm run devOpen http://localhost:3000. The frontend calls the backend at
NEXT_PUBLIC_API_URL (defaults to http://localhost:8000 if unset — see
Configuration).
With the backend running, seed and run a scripted session directly against the API:
python scripts/seed_session.py chf_pulmonary_edema_01This creates a session, performs a few actions (bsi, scene_safe, obtain_vitals), and ends
it — useful as a smoke test or as a reference for calling the API yourself. See
docs/api_reference.md for every endpoint.
All configuration is environment variables, loaded from .env at the repo root (see
.env.example for the full, documented list). The ones you're most likely to
change:
| Variable | Purpose |
|---|---|
LLM_PROVIDER |
mock (default, no external calls) or together (real LLM roleplay via Together.ai) |
TOGETHER_API_KEY, TOGETHER_MODEL, TOGETHER_BASE_URL |
Together.ai credentials/model, used when LLM_PROVIDER=together |
DATABASE_URL |
SQLite by default; swap for a Postgres DSN in production |
ALLOWED_ORIGINS |
Comma-separated CORS allowlist for production deployments |
RANDOM_SEED |
Fix the engine's randomizer seed for reproducible runs (e.g. in tests) |
NEXT_PUBLIC_API_URL |
Frontend-only, not in .env.example (Next.js env conventions differ from the backend's). Set this to your backend's URL when frontend and backend aren't both on localhost:8000 — e.g. in Vercel project settings for a deployed frontend. |
# Backend — pytest, from apps/api/
cd apps/api
pytest
# Backend — lint / format / typecheck
ruff check .
black .
mypy .
# Frontend — vitest, from the repo root
npm run test # single run
npm run test:watch # watch mode
npx vitest run path/to/file.test.ts # a single test file
# Content — validate every scenario/protocol JSON file against its schema
python scripts/validate_content.pyScenarios, protocols, and actions are plain JSON validated against schemas in
content/schemas/ — you can add a new clinical case without touching any Python or TypeScript:
docs/scenario_authoring_guide.md— scenario structure, reveal rules (revealed_if_asked/revealed_if_action/revealed_if_no_action), contraindications, and how deterioration worksdocs/protocol_authoring_guide.md— county protocol packs and how they're resolved by county/cert level/tagsdocs/evaluation_rubric.md— how scoring and debrief domains workcontent/templates/scenario.template.json— starting point for a new scenario
Always run python scripts/validate_content.py and cd apps/api && pytest after adding or
editing content.
The frontend is deployed on Vercel (the live demo). The backend (FastAPI) is a standard ASGI app and can be deployed anywhere that runs Python 3.12 (a container, a VM, Fly.io, Render, Railway, etc.) — this repo doesn't prescribe a specific backend host. Wherever you deploy it:
- Set
ALLOWED_ORIGINSto your frontend's origin(s) for CORS. - Set
DATABASE_URLto a persistent database (Postgres recommended) — the default SQLite file is fine for local dev but not for most hosted setups. - Set
LLM_PROVIDER=togetherplusTOGETHER_API_KEYif you want real LLM roleplay in production. - Point the frontend's
NEXT_PUBLIC_API_URLat the deployed backend's URL. - No database migrations exist yet (see
CLAUDE.md); the schema is currently created via SQLAlchemycreate_allat startup.
| Doc | Covers |
|---|---|
CLAUDE.md |
Codebase tour for contributors: turn-processing data flow, key modules, known quirks |
docs/architecture.md |
System layers and the turn lifecycle |
docs/api_reference.md |
Every REST endpoint, request/response shapes |
docs/prompt_design.md |
What the LLM sees and the rules it must follow |
docs/evaluation_rubric.md |
How deterministic scoring and debriefs work |
docs/scenario_authoring_guide.md |
How to write a new scenario |
docs/protocol_authoring_guide.md |
How to write a new county protocol pack |
Contributions are welcome — see CONTRIBUTING.md for dev setup, coding
conventions, and the PR process, and CODE_OF_CONDUCT.md for community
expectations. For security issues, see SECURITY.md.
If you use SceneSim in research, teaching, or as a basis for other software, please cite it —
see CITATION.cff for structured citation metadata (GitHub renders a "Cite this
repository" button from this file automatically).
SceneSim is an independent project. Readers interested in LLM-based Socratic tutoring for case-based clinical education may also be interested in related prior work by the same author:
Golchini, N., Passalacqua, E., Vaughn, L., & Abdulnour, R.-E. E. (2025). Socratic AI: An Adaptive Tutor for Clinical Case Based Learning. medRxiv. https://doi.org/10.1101/2025.06.22.25329661
SceneSim is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0-or-later). In short: you're free to use, modify, and redistribute this software, including running it as a network service — but if you do run a modified version as a service, you must make your modified source available to that service's users. See the LICENSE file for the full terms.