Skip to content

evanqua/scenesim

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

11 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

SceneSim

A turn-based, deterministic EMS training simulator with LLM-rendered roleplay.

License: AGPL v3 Live Demo DOI

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 mock LLM 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.


What is SceneSim?

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 mock LLM 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.

Table of contents

Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  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.

Repository layout

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.

Getting started

Prerequisites

  • 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 mock provider

1. Clone and configure

git clone https://github.com/evanqua/scenesim.git
cd scenesim
cp .env.example .env

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

2. Run the backend

cd apps/api
pip install -e ".[dev]"
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000

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

3. Run the frontend

In a second terminal, from the repo root:

npm install
npm run dev

Open http://localhost:3000. The frontend calls the backend at NEXT_PUBLIC_API_URL (defaults to http://localhost:8000 if unset β€” see Configuration).

4. Try it without the UI (optional)

With the backend running, seed and run a scripted session directly against the API:

python scripts/seed_session.py chf_pulmonary_edema_01

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

Configuration

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.

Testing

# 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.py

Authoring content

Scenarios, 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 works
  • docs/protocol_authoring_guide.md β€” county protocol packs and how they're resolved by county/cert level/tags
  • docs/evaluation_rubric.md β€” how scoring and debrief domains work
  • content/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.

Deployment

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_ORIGINS to your frontend's origin(s) for CORS.
  • Set DATABASE_URL to a persistent database (Postgres recommended) β€” the default SQLite file is fine for local dev but not for most hosted setups.
  • Set LLM_PROVIDER=together plus TOGETHER_API_KEY if you want real LLM roleplay in production.
  • Point the frontend's NEXT_PUBLIC_API_URL at the deployed backend's URL.
  • No database migrations exist yet (see CLAUDE.md); the schema is currently created via SQLAlchemy create_all at startup.

Documentation index

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

Contributing

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.

Citing this project

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

DOI

Related work

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

License

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.

About

A turn-based, deterministic EMS training simulator with LLM-rendered roleplay.

Resources

License

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors