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.