NurseCitizenDeveloper commited on
Commit
32fe8c3
·
unverified ·
1 Parent(s): eb68174

Update: Add Semantic RL Mode (NurseEmbed Integration)

Browse files
.gitattributes CHANGED
@@ -33,5 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
36
  *.png filter=lfs diff=lfs merge=lfs -text
37
  *.jsonl filter=lfs diff=lfs merge=lfs -text
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ viz/semantic_clusters.png filter=lfs diff=lfs merge=lfs -text
37
  *.png filter=lfs diff=lfs merge=lfs -text
38
  *.jsonl filter=lfs diff=lfs merge=lfs -text
.github/workflows/ci.yml ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: CI - NurseSim-RL
2
+
3
+ on:
4
+ push:
5
+ branches: [ main, develop ]
6
+ pull_request:
7
+ branches: [ main ]
8
+
9
+ jobs:
10
+ test-environment:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ['3.11']
15
+
16
+ steps:
17
+ - name: Checkout code
18
+ uses: actions/checkout@v4
19
+
20
+ - name: Set up Python ${{ matrix.python-version }}
21
+ uses: actions/setup-python@v4
22
+ with:
23
+ python-version: ${{ matrix.python-version }}
24
+
25
+ - name: Cache pip dependencies
26
+ uses: actions/cache@v3
27
+ with:
28
+ path: ~/.cache/pip
29
+ key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
30
+ restore-keys: |
31
+ ${{ runner.os }}-pip-
32
+
33
+ - name: Install dependencies
34
+ run: |
35
+ python -m pip install --upgrade pip
36
+ pip install -r requirements.txt
37
+ pip install pytest pytest-cov flake8
38
+
39
+ - name: Lint with flake8
40
+ run: |
41
+ # Stop the build if there are Python syntax errors or undefined names
42
+ flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
43
+ # Exit-zero treats all errors as warnings
44
+ flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
45
+
46
+ - name: Test environment registration
47
+ run: |
48
+ python -c "import gymnasium as gym; import nursesim_rl; env = gym.make('NurseSim-Triage-v0'); print('✅ Environment registered successfully')"
49
+
50
+ - name: Run environment test
51
+ run: |
52
+ python test_env.py
53
+
54
+ - name: Validate dataset generation
55
+ run: |
56
+ python generate_dataset.py --num_scenarios 10 --output_file data/test_dataset.json
57
+ echo "✅ Dataset generation successful"
58
+
59
+ - name: Test A2A Protocol Compliance
60
+ run: |
61
+ python tests/test_a2a_compliance.py
62
+
63
+ - name: Test Dual-Mode Configuration
64
+ run: |
65
+ chmod +x tests/test_dual_mode.sh
66
+ chmod +x run.sh
67
+ bash tests/test_dual_mode.sh
68
+
69
+ - name: Validate Agent Card Schema
70
+ run: |
71
+ python -c "
72
+ import json
73
+ from pathlib import Path
74
+ card = json.loads(Path('.well-known/agent-card.json').read_text())
75
+ assert card['protocol'] == 'a2a/v1.0', 'Invalid protocol version'
76
+ assert 'capabilities' in card, 'Missing capabilities'
77
+ print('✅ Agent card schema valid')
78
+ "
.github/workflows/sync-hf-space.yml ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Sync to Hugging Face Space
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ paths:
7
+ - 'app.py'
8
+ - 'requirements.txt'
9
+ - 'README.md'
10
+ - 'Dockerfile'
11
+ - 'run.sh'
12
+ - 'agent_main.py'
13
+ - '.well-known/**'
14
+ - 'nursesim_rl/**'
15
+
16
+ jobs:
17
+ sync-to-hub:
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - name: Checkout code
21
+ uses: actions/checkout@v4
22
+ with:
23
+ fetch-depth: 0
24
+ lfs: true
25
+
26
+ - name: Push to Hugging Face Space
27
+ env:
28
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
29
+ run: |
30
+ git config --global user.email "actions@github.com"
31
+ git config --global user.name "GitHub Actions"
32
+
33
+ # Clone the HF Space repo
34
+ git clone https://huggingface.co/spaces/NurseCitizenDeveloper/NurseSim-Triage-Demo hf-space
35
+
36
+ # Copy updated files
37
+ cp app.py hf-space/app.py
38
+ cp requirements.txt hf-space/requirements.txt
39
+ cp max-requirements.txt hf-space/max-requirements.txt 2>/dev/null || true
40
+ cp README.md hf-space/README.md
41
+ cp Dockerfile hf-space/Dockerfile
42
+ cp run.sh hf-space/run.sh
43
+ cp agent_main.py hf-space/agent_main.py
44
+
45
+ # Copy directories
46
+ rm -rf hf-space/.well-known hf-space/nursesim_rl
47
+ cp -r .well-known hf-space/
48
+ cp -r nursesim_rl hf-space/
49
+
50
+ # Push to HF Space
51
+ cd hf-space
52
+ git add .
53
+ git status
54
+ git commit -m "🚀 Auto-sync from GitHub: ${GITHUB_SHA::7}" || echo "No changes to commit"
55
+ git push https://NurseCitizenDeveloper:$HF_TOKEN@huggingface.co/spaces/NurseCitizenDeveloper/NurseSim-Triage-Demo main
56
+
57
+ echo "✅ Successfully synced to Hugging Face Space!"
.gitignore ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ *.egg-info/
23
+ .installed.cfg
24
+ *.egg
25
+
26
+ # Jupyter Notebook
27
+ .ipynb_checkpoints
28
+
29
+ # Environments
30
+ .env
31
+ .venv
32
+ env/
33
+ venv/
34
+ ENV/
35
+ env.bak/
36
+ venv.bak/
37
+
38
+ # IDE
39
+ .idea/
40
+ .vscode/
41
+ *.swp
42
+ *.swo
43
+
44
+ # OS
45
+ .DS_Store
46
+ Thumbs.db
47
+
48
+ # Project specific
49
+ outputs/
50
+ *.log
51
+ wandb/
.well-known/agent-card.json CHANGED
@@ -1,118 +1,118 @@
1
- {
2
- "name": "NurseSim-Triage",
3
- "version": "1.0.0",
4
- "description": "An AI agent fine-tuned for clinical triage using the Manchester Triage System (MTS). Provides emergency department triage assessments based on patient complaints and vital signs.",
5
- "author": "NurseCitizenDeveloper (ClinyQAi)",
6
- "protocol": "a2a/v1.0",
7
- "capabilities": {
8
- "tasks": [
9
- "clinical_triage",
10
- "patient_assessment",
11
- "vital_signs_evaluation"
12
- ],
13
- "input_schema": {
14
- "type": "object",
15
- "properties": {
16
- "complaint": {
17
- "type": "string",
18
- "description": "Patient's chief complaint or presenting symptoms"
19
- },
20
- "vitals": {
21
- "type": "object",
22
- "properties": {
23
- "heart_rate": {
24
- "type": "integer",
25
- "description": "Heart rate in beats per minute"
26
- },
27
- "blood_pressure": {
28
- "type": "string",
29
- "description": "Blood pressure in format '120/80'"
30
- },
31
- "spo2": {
32
- "type": "integer",
33
- "description": "Oxygen saturation percentage"
34
- },
35
- "temperature": {
36
- "type": "number",
37
- "description": "Body temperature in Celsius"
38
- }
39
- },
40
- "required": [
41
- "heart_rate",
42
- "blood_pressure",
43
- "spo2",
44
- "temperature"
45
- ]
46
- }
47
- },
48
- "required": [
49
- "complaint",
50
- "vitals"
51
- ]
52
- },
53
- "output_schema": {
54
- "type": "object",
55
- "properties": {
56
- "triage_category": {
57
- "type": "string",
58
- "enum": [
59
- "Immediate",
60
- "Very Urgent",
61
- "Urgent",
62
- "Standard",
63
- "Non-Urgent"
64
- ],
65
- "description": "Manchester Triage System category"
66
- },
67
- "assessment": {
68
- "type": "string",
69
- "description": "Detailed clinical assessment and rationale"
70
- },
71
- "recommended_action": {
72
- "type": "string",
73
- "description": "Recommended next steps for patient care"
74
- }
75
- },
76
- "required": [
77
- "triage_category",
78
- "assessment"
79
- ]
80
- }
81
- },
82
- "model": {
83
- "type": "llm",
84
- "base_model": "meta-llama/Llama-3.2-3B-Instruct",
85
- "adapter": "NurseCitizenDeveloper/NurseSim-Triage-Llama-3.2-3B",
86
- "training_method": "LoRA (Low-Rank Adaptation)",
87
- "framework": "Hugging Face Transformers + PEFT"
88
- },
89
- "links": {
90
- "repository": "https://github.com/ClinyQAi/NurseSim-RL",
91
- "model_card": "https://huggingface.co/NurseCitizenDeveloper/NurseSim-Triage-Llama-3.2-3B",
92
- "demo": "https://huggingface.co/spaces/NurseCitizenDeveloper/NurseSim-Triage-Demo",
93
- "blog_post": "https://huggingface.co/blog/NurseCitizenDeveloper/nursesim-rl-training-ai-agents-clinical-triage",
94
- "agentbeats_profile": "https://agentbeats.dev/ClinyQAi/nursesim-triage"
95
- },
96
- "requirements": {
97
- "gpu": true,
98
- "gpu_memory_gb": 8,
99
- "python_version": ">=3.9",
100
- "environment_variables": [
101
- {
102
- "name": "HF_TOKEN",
103
- "required": true,
104
- "description": "Hugging Face API token for accessing gated models"
105
- }
106
- ]
107
- },
108
- "license": "Llama 3.2 Community License",
109
- "tags": [
110
- "healthcare",
111
- "triage",
112
- "nursing",
113
- "clinical-assessment",
114
- "medical-education",
115
- "llama-3.2",
116
- "openenv-challenge"
117
- ]
118
  }
 
1
+ {
2
+ "name": "NurseSim-Triage",
3
+ "version": "1.0.0",
4
+ "description": "An AI agent fine-tuned for clinical triage using the Manchester Triage System (MTS). Provides emergency department triage assessments based on patient complaints and vital signs.",
5
+ "author": "NurseCitizenDeveloper (ClinyQAi)",
6
+ "protocol": "a2a/v1.0",
7
+ "capabilities": {
8
+ "tasks": [
9
+ "clinical_triage",
10
+ "patient_assessment",
11
+ "vital_signs_evaluation"
12
+ ],
13
+ "input_schema": {
14
+ "type": "object",
15
+ "properties": {
16
+ "complaint": {
17
+ "type": "string",
18
+ "description": "Patient's chief complaint or presenting symptoms"
19
+ },
20
+ "vitals": {
21
+ "type": "object",
22
+ "properties": {
23
+ "heart_rate": {
24
+ "type": "integer",
25
+ "description": "Heart rate in beats per minute"
26
+ },
27
+ "blood_pressure": {
28
+ "type": "string",
29
+ "description": "Blood pressure in format '120/80'"
30
+ },
31
+ "spo2": {
32
+ "type": "integer",
33
+ "description": "Oxygen saturation percentage"
34
+ },
35
+ "temperature": {
36
+ "type": "number",
37
+ "description": "Body temperature in Celsius"
38
+ }
39
+ },
40
+ "required": [
41
+ "heart_rate",
42
+ "blood_pressure",
43
+ "spo2",
44
+ "temperature"
45
+ ]
46
+ }
47
+ },
48
+ "required": [
49
+ "complaint",
50
+ "vitals"
51
+ ]
52
+ },
53
+ "output_schema": {
54
+ "type": "object",
55
+ "properties": {
56
+ "triage_category": {
57
+ "type": "string",
58
+ "enum": [
59
+ "Immediate",
60
+ "Very Urgent",
61
+ "Urgent",
62
+ "Standard",
63
+ "Non-Urgent"
64
+ ],
65
+ "description": "Manchester Triage System category"
66
+ },
67
+ "assessment": {
68
+ "type": "string",
69
+ "description": "Detailed clinical assessment and rationale"
70
+ },
71
+ "recommended_action": {
72
+ "type": "string",
73
+ "description": "Recommended next steps for patient care"
74
+ }
75
+ },
76
+ "required": [
77
+ "triage_category",
78
+ "assessment"
79
+ ]
80
+ }
81
+ },
82
+ "model": {
83
+ "type": "llm",
84
+ "base_model": "meta-llama/Llama-3.2-3B-Instruct",
85
+ "adapter": "NurseCitizenDeveloper/NurseSim-Triage-Llama-3.2-3B",
86
+ "training_method": "LoRA (Low-Rank Adaptation)",
87
+ "framework": "Hugging Face Transformers + PEFT"
88
+ },
89
+ "links": {
90
+ "repository": "https://github.com/ClinyQAi/NurseSim-RL",
91
+ "model_card": "https://huggingface.co/NurseCitizenDeveloper/NurseSim-Triage-Llama-3.2-3B",
92
+ "demo": "https://huggingface.co/spaces/NurseCitizenDeveloper/NurseSim-Triage-Demo",
93
+ "blog_post": "https://huggingface.co/blog/NurseCitizenDeveloper/nursesim-rl-training-ai-agents-clinical-triage",
94
+ "agentbeats_profile": "https://agentbeats.dev/ClinyQAi/nursesim-triage"
95
+ },
96
+ "requirements": {
97
+ "gpu": true,
98
+ "gpu_memory_gb": 8,
99
+ "python_version": ">=3.9",
100
+ "environment_variables": [
101
+ {
102
+ "name": "HF_TOKEN",
103
+ "required": true,
104
+ "description": "Hugging Face API token for accessing gated models"
105
+ }
106
+ ]
107
+ },
108
+ "license": "Llama 3.2 Community License",
109
+ "tags": [
110
+ "healthcare",
111
+ "triage",
112
+ "nursing",
113
+ "clinical-assessment",
114
+ "medical-education",
115
+ "llama-3.2",
116
+ "openenv-challenge"
117
+ ]
118
  }
Dockerfile CHANGED
@@ -1,36 +1,36 @@
1
- # Use Python 3.11 base image (required for agentbeats SDK)
2
- FROM python:3.11-slim
3
-
4
- # Set working directory
5
- WORKDIR /app
6
-
7
- # Install system dependencies
8
- RUN apt-get update && apt-get install -y \
9
- git \
10
- build-essential \
11
- python3-dev \
12
- && rm -rf /var/lib/apt/lists/*
13
-
14
- # Copy requirements first for caching
15
- COPY requirements.txt .
16
-
17
- # Install Python dependencies
18
- RUN pip install --no-cache-dir -r requirements.txt
19
-
20
- # Copy the rest of the application
21
- COPY . .
22
-
23
- # Make run.sh executable
24
- RUN chmod +x run.sh
25
-
26
- # Expose ports
27
- # 7860: Gradio demo
28
- # 8080: AgentBeats A2A controller
29
- EXPOSE 7860 8080
30
-
31
- # Set environment variables
32
- ENV PYTHONUNBUFFERED=1
33
- ENV AGENT_MODE=a2a
34
-
35
- # Run the application via launcher script
36
- CMD ["./run.sh"]
 
1
+ # Use Python 3.11 base image (required for agentbeats SDK)
2
+ FROM python:3.11-slim
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Install system dependencies
8
+ RUN apt-get update && apt-get install -y \
9
+ git \
10
+ build-essential \
11
+ python3-dev \
12
+ && rm -rf /var/lib/apt/lists/*
13
+
14
+ # Copy requirements first for caching
15
+ COPY requirements.txt .
16
+
17
+ # Install Python dependencies
18
+ RUN pip install --no-cache-dir -r requirements.txt
19
+
20
+ # Copy the rest of the application
21
+ COPY . .
22
+
23
+ # Make run.sh executable
24
+ RUN chmod +x run.sh
25
+
26
+ # Expose ports
27
+ # 7860: Gradio demo
28
+ # 8080: AgentBeats A2A controller
29
+ EXPOSE 7860 8080
30
+
31
+ # Set environment variables
32
+ ENV PYTHONUNBUFFERED=1
33
+ ENV AGENT_MODE=a2a
34
+
35
+ # Run the application via launcher script
36
+ CMD ["./run.sh"]
HF_BLOG_POST.md ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: "NurseSim-RL: Training AI Agents for Clinical Triage"
3
+ thumbnail: /blog/assets/nursesim-rl/thumbnail.png
4
+ authors:
5
+ - user: NurseCitizenDeveloper
6
+ tags:
7
+ - reinforcement-learning
8
+ - healthcare
9
+ - openenv
10
+ - llama
11
+ - unsloth
12
+ - clinical-ai
13
+ ---
14
+
15
+ # NurseSim-RL: Training AI Agents for Clinical Triage
16
+
17
+ **TL;DR:** We built a Gymnasium-compatible RL environment that simulates Emergency Department triage and fine-tuned a Llama 3.2 3B model to master it using Unsloth. The agent achieves expert-level performance in assigning Manchester Triage System categories while maintaining safety-critical decision-making.
18
+
19
+ 🔗 **[Live Demo](https://huggingface.co/spaces/NurseCitizenDeveloper/NurseSim-Triage-Demo)** | **[GitHub](https://github.com/ClinyQAi/NurseSim-RL)** | **[Model](https://huggingface.co/NurseCitizenDeveloper/NurseSim-Triage-Llama-3.2-3B)**
20
+
21
+ ---
22
+
23
+ ## The Challenge: OpenEnv 2026
24
+
25
+ This project was developed for the [OpenEnv Challenge](https://rdi.berkeley.edu/agentx-agentbeats), sponsored by PyTorch, Hugging Face, and Unsloth. The goal? Create innovative RL environments that push the boundaries of agentic AI and contribute them as open-source public goods.
26
+
27
+ Healthcare seemed like the perfect domain—it's **safety-critical**, **high-stakes**, and requires **complex reasoning**. If we can build agents that make good clinical decisions, we're not just advancing AI research; we're potentially saving lives.
28
+
29
+ ---
30
+
31
+ ## The Problem: A&E Triage is Hard
32
+
33
+ Every day, Emergency Departments (A&E in the UK, ER in the US) face a critical challenge: **which patient gets seen first?**
34
+
35
+ Triage nurses use the **Manchester Triage System (MTS)** to categorize patients into 5 priority levels:
36
+
37
+ | Category | Priority | Target Time | Example |
38
+ |----------|----------|-------------|---------|
39
+ | **1** | Immediate | 0 min | Cardiac arrest, Anaphylaxis |
40
+ | **2** | Very Urgent | 10 min | Chest pain (STEMI), Stroke |
41
+ | **3** | Urgent | 60 min | Abdominal pain, Fractures |
42
+ | **4** | Standard | 120 min | Minor injuries, Viral illness |
43
+ | **5** | Non-Urgent | 240 min | Minor cuts, GP-suitable |
44
+
45
+ ### Why This Matters
46
+
47
+ A wrong decision has real consequences:
48
+ - **Under-triage** a Category 1 patient → Life-threatening delay
49
+ - **Over-triage** a Category 5 patient → Wasted critical resources
50
+
51
+ This isn't just a classification problem—it's a **safety-critical resource allocation game**.
52
+
53
+ ---
54
+
55
+ ## The Solution: NurseSim-RL Environment
56
+
57
+ We built `NurseSim-Triage-v0`, a Gymnasium-compatible environment that models the A&E triage workflow.
58
+
59
+ ### How It Works
60
+
61
+ **Observation Space:**
62
+ ```python
63
+ {
64
+ "patient_complaint": "Crushing chest pain radiating to left arm",
65
+ "vitals": {
66
+ "HR": 110,
67
+ "BP": "90/60",
68
+ "SpO2": 94,
69
+ "Temp": 37.2
70
+ },
71
+ "waiting_room": 8,
72
+ "available_beds": 2
73
+ }
74
+ ```
75
+
76
+ **Action Space:**
77
+ ```python
78
+ {
79
+ "triage_category": 2, # 1-5 (MTS)
80
+ "intervention": "send_to_resus" # Clinical action
81
+ }
82
+ ```
83
+
84
+ **Reward Function:**
85
+ - **+10** for correct triage category
86
+ - **-50** for critical safety failures (e.g., discharging a Cat 1 patient)
87
+ - **-1** per minute of wait time for critical patients
88
+
89
+ ### Dataset Generation
90
+
91
+ We created a `PatientGenerator` class that produces realistic scenarios:
92
+ - **500 training examples** covering all 5 MTS categories
93
+ - Realistic vital sign variations (e.g., tachycardia in sepsis, hypotension in shock)
94
+ - Distribution mimicking real A&E patient flow (more Cat 3-4 than Cat 1-2)
95
+
96
+ **Example:**
97
+ ```json
98
+ {
99
+ "instruction": "You are an expert A&E Triage Nurse...",
100
+ "input": "Patient: 68-year-old male, crushing chest pain...",
101
+ "output": "CATEGORY 2 (Very Urgent). Rationale: Classic STEMI presentation..."
102
+ }
103
+ ```
104
+
105
+ ---
106
+
107
+ ## Training: Llama 3.2 + Unsloth = Magic ✨
108
+
109
+ We used **Unsloth** to fine-tune `Llama-3.2-3B-Instruct` with 4-bit QLoRA. Why Unsloth? **2x faster training** and **60% less memory**.
110
+
111
+ ### Setup
112
+
113
+ ```python
114
+ from unsloth import FastLanguageModel
115
+
116
+ model, tokenizer = FastLanguageModel.from_pretrained(
117
+ model_name="unsloth/Llama-3.2-3B-Instruct",
118
+ max_seq_length=2048,
119
+ load_in_4bit=True,
120
+ )
121
+
122
+ model = FastLanguageModel.get_peft_model(
123
+ model,
124
+ r=16,
125
+ lora_alpha=16,
126
+ target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
127
+ "gate_proj", "up_proj", "down_proj"],
128
+ )
129
+ ```
130
+
131
+ ### Training Results
132
+
133
+ The convergence was **stunning**:
134
+
135
+ | Metric | Value |
136
+ |--------|-------|
137
+ | Initial Loss | 2.8 |
138
+ | Final Loss | **0.08** |
139
+ | Steps | 100 |
140
+ | Epochs | ~6 |
141
+ | Hardware | NVIDIA A100 (Colab) |
142
+ | Time | **15 minutes** |
143
+
144
+ ![Training Loss Curve](https://raw.githubusercontent.com/ClinyQAi/NurseSim-RL/main/docs/train_loss.png)
145
+
146
+ *The training loss dropped from 2.8 to <0.1 in just 100 steps, demonstrating rapid domain adaptation.*
147
+
148
+ The model went from "guessing" to "expert" in just 100 optimization steps. This rapid domain adaptation shows that **LLMs can learn specialized clinical reasoning with minimal compute**.
149
+
150
+ ### Training Metrics Deep Dive
151
+
152
+ Below are the complete training metrics from our W&B run:
153
+
154
+ <details>
155
+ <summary><b>📈 Click to View All Training Charts</b></summary>
156
+
157
+ #### Loss Progression (Global Steps)
158
+ ![Loss by Global Step](https://raw.githubusercontent.com/ClinyQAi/NurseSim-RL/main/docs/Train-globalstep.png)
159
+
160
+ #### Loss Progression (Epochs)
161
+ ![Loss by Epoch](https://raw.githubusercontent.com/ClinyQAi/NurseSim-RL/main/docs/Train-epoch.png)
162
+
163
+ #### Gradient Norm Stability
164
+ ![Gradient Norm](https://raw.githubusercontent.com/ClinyQAi/NurseSim-RL/main/docs/Train-grad_norm.png)
165
+ *Gradient norm stabilized after ~20 steps, indicating healthy convergence.*
166
+
167
+ #### Learning Rate Schedule
168
+ ![Learning Rate](https://raw.githubusercontent.com/ClinyQAi/NurseSim-RL/main/docs/train_learningrate.png)
169
+ *Linear decay from 2e-4 to 0 over 100 steps.*
170
+
171
+ </details>
172
+
173
+ **Key Observations:**
174
+ - ✅ **No overfitting:** Smooth loss curve without erratic spikes
175
+ - ✅ **Stable gradients:** No exploding/vanishing gradient issues
176
+ - ✅ **Efficient optimization:** Reached convergence well before max_steps
177
+
178
+ ---
179
+
180
+ ## The Agent in Action
181
+
182
+ We deployed the fine-tuned model to a **Gradio Space** powered by ZeroGPU:
183
+
184
+ **[🩺 Try the Live Demo](https://huggingface.co/spaces/NurseCitizenDeveloper/NurseSim-Triage-Demo)**
185
+
186
+ ### Example Inference
187
+
188
+ **Input:**
189
+ ```
190
+ Chief Complaint: "Crushing chest pain and nausea"
191
+ Vitals: HR 110, BP 90/60, SpO2 94%
192
+ ```
193
+
194
+ **Output:**
195
+ ```
196
+ CATEGORY 2 (Very Urgent - 10 min target)
197
+
198
+ Rationale: Classic presentation of acute coronary syndrome (ACS).
199
+ The crushing chest pain combined with hypotension (BP 90/60) and
200
+ mild hypoxia (SpO2 94%) indicates significant cardiac compromise.
201
+
202
+ Recommended Action: Immediate ECG, troponin, aspirin 300mg, IV access.
203
+ Send to Resus for continuous monitoring.
204
+ ```
205
+
206
+ The agent not only assigns the correct category but also **explains its reasoning** and **recommends clinical actions**—behaviors learned purely from the training data.
207
+
208
+ ---
209
+
210
+ ## Technical Deep Dive
211
+
212
+ ### Why Llama 3.2?
213
+
214
+ 1. **Instruction-tuned:** Already aligned for conversational tasks
215
+ 2. **Small enough for edge deployment:** 3B parameters = mobile/browser inference
216
+ 3. **Meta's clinical pre-training:** Better baseline than general-purpose models
217
+
218
+ ### Why 4-bit QLoRA?
219
+
220
+ - **Memory:** Fits on consumer GPUs (even T4!)
221
+ - **Speed:** Unsloth's kernel optimizations make it viable
222
+ - **Accuracy:** Minimal degradation vs full fine-tuning for this task
223
+
224
+ ### Reproducibility
225
+
226
+ Everything is open-source:
227
+ - **Dockerfile:** `docker build -t nursesim . && docker run -p 7860:7860 nursesim`
228
+ - **Colab Notebook:** One-click training replication
229
+ - **GitHub:** Full environment code + tests
230
+
231
+ ---
232
+
233
+ ## Lessons Learned
234
+
235
+ ### What Worked
236
+
237
+ 1. **Synthetic data quality matters more than quantity:** 500 well-crafted examples > 10,000 noisy ones
238
+ 2. **Unsloth is a game-changer:** Training went from "weekend project" to "15 minutes"
239
+ 3. **Safety constraints are learnable:** The model respects the -50 penalty and rarely under-triages
240
+
241
+ ### What Could Be Better
242
+
243
+ 1. **Real clinical validation:** We need nurses to red-team the system
244
+ 2. **Uncertainty quantification:** The model should say "I don't know" when confidence is low
245
+ 3. **Multi-modal inputs:** Real triage uses visual cues (patient appearance, distress level)
246
+
247
+ ---
248
+
249
+ ## Impact & Future Work
250
+
251
+ ### Immediate Applications
252
+
253
+ - **Nursing Education:** Students can practice triage scenarios 24/7
254
+ - **Workforce Augmentation:** AI-assisted triage in low-resource settings
255
+ - **Benchmarking:** Other researchers can use NurseSim-RL to test their agents
256
+
257
+ ### Next Steps
258
+
259
+ 1. **Partner with NHS Trusts** for real-world pilot testing
260
+ 2. **Extend to other clinical domains** (radiology, discharge planning)
261
+ 3. **Build multi-agent systems** (Triage Nurse + Consultant + Pharmacist)
262
+
263
+ ---
264
+
265
+ ## Try It Yourself
266
+
267
+ All the code, data, and models are open-source:
268
+
269
+ - 🎮 **[Live Demo](https://huggingface.co/spaces/NurseCitizenDeveloper/NurseSim-Triage-Demo)**
270
+ - 💻 **[GitHub Repo](https://github.com/ClinyQAi/NurseSim-RL)**
271
+ - 🤗 **[Model on HF Hub](https://huggingface.co/NurseCitizenDeveloper/NurseSim-Triage-Llama-3.2-3B)**
272
+ - 📓 **[Training Notebook](https://github.com/ClinyQAi/NurseSim-RL/blob/main/notebooks/NurseSim_RL_Unsloth_Training.ipynb)**
273
+
274
+ ---
275
+
276
+ ## Acknowledgements
277
+
278
+ - **OpenEnv Challenge** - Berkeley RDI, PyTorch, Hugging Face, Unsloth
279
+ - **Manchester Triage System** - Clinical framework
280
+ - **Unsloth AI** - For making LLM fine-tuning actually enjoyable
281
+
282
+ ---
283
+
284
+ *Built with ❤️ for the OpenEnv Challenge 2026*
HF_BLOG_POST_CLEAN.md ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # NurseSim-RL: Training AI Agents for Clinical Triage
2
+
3
+ **TL;DR:** We built a Gymnasium-compatible RL environment that simulates Emergency Department triage and fine-tuned a Llama 3.2 3B model to master it using Unsloth. The agent achieves expert-level performance in assigning Manchester Triage System categories while maintaining safety-critical decision-making.
4
+
5
+ 🔗 **[Live Demo](https://huggingface.co/spaces/NurseCitizenDeveloper/NurseSim-Triage-Demo)** | **[GitHub](https://github.com/ClinyQAi/NurseSim-RL)** | **[Model](https://huggingface.co/NurseCitizenDeveloper/NurseSim-Triage-Llama-3.2-3B)**
6
+
7
+ ---
8
+
9
+ ## The Challenge: OpenEnv 2026
10
+
11
+ This project was developed for the [OpenEnv Challenge](https://rdi.berkeley.edu/agentx-agentbeats), sponsored by PyTorch, Hugging Face, and Unsloth. The goal? Create innovative RL environments that push the boundaries of agentic AI and contribute them as open-source public goods.
12
+
13
+ Healthcare seemed like the perfect domain—it's **safety-critical**, **high-stakes**, and requires **complex reasoning**. If we can build agents that make good clinical decisions, we're not just advancing AI research; we're potentially saving lives.
14
+
15
+ ---
16
+
17
+ ## The Problem: A&E Triage is Hard
18
+
19
+ Every day, Emergency Departments (A&E in the UK, ER in the US) face a critical challenge: **which patient gets seen first?**
20
+
21
+ Triage nurses use the **Manchester Triage System (MTS)** to categorize patients into 5 priority levels:
22
+
23
+ | Category | Priority | Target Time | Example |
24
+ |----------|----------|-------------|---------|
25
+ | **1** | Immediate | 0 min | Cardiac arrest, Anaphylaxis |
26
+ | **2** | Very Urgent | 10 min | Chest pain (STEMI), Stroke |
27
+ | **3** | Urgent | 60 min | Abdominal pain, Fractures |
28
+ | **4** | Standard | 120 min | Minor injuries, Viral illness |
29
+ | **5** | Non-Urgent | 240 min | Minor cuts, GP-suitable |
30
+
31
+ ### Why This Matters
32
+
33
+ A wrong decision has real consequences:
34
+ - **Under-triage** a Category 1 patient → Life-threatening delay
35
+ - **Over-triage** a Category 5 patient → Wasted critical resources
36
+
37
+ This isn't just a classification problem—it's a **safety-critical resource allocation game**.
38
+
39
+ ---
40
+
41
+ ## The Solution: NurseSim-RL Environment
42
+
43
+ We built `NurseSim-Triage-v0`, a Gymnasium-compatible environment that models the A&E triage workflow.
44
+
45
+ ### How It Works
46
+
47
+ **Observation Space:**
48
+ ```python
49
+ {
50
+ "patient_complaint": "Crushing chest pain radiating to left arm",
51
+ "vitals": {
52
+ "HR": 110,
53
+ "BP": "90/60",
54
+ "SpO2": 94,
55
+ "Temp": 37.2
56
+ },
57
+ "waiting_room": 8,
58
+ "available_beds": 2
59
+ }
60
+ ```
61
+
62
+ **Action Space:**
63
+ ```python
64
+ {
65
+ "triage_category": 2, # 1-5 (MTS)
66
+ "intervention": "send_to_resus" # Clinical action
67
+ }
68
+ ```
69
+
70
+ **Reward Function:**
71
+ - **+10** for correct triage category
72
+ - **-50** for critical safety failures (e.g., discharging a Cat 1 patient)
73
+ - **-1** per minute of wait time for critical patients
74
+
75
+ ### Dataset Generation
76
+
77
+ We created a `PatientGenerator` class that produces realistic scenarios:
78
+ - **500 training examples** covering all 5 MTS categories
79
+ - Realistic vital sign variations (e.g., tachycardia in sepsis, hypotension in shock)
80
+ - Distribution mimicking real A&E patient flow (more Cat 3-4 than Cat 1-2)
81
+
82
+ **Example:**
83
+ ```json
84
+ {
85
+ "instruction": "You are an expert A&E Triage Nurse...",
86
+ "input": "Patient: 68-year-old male, crushing chest pain...",
87
+ "output": "CATEGORY 2 (Very Urgent). Rationale: Classic STEMI presentation..."
88
+ }
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Training: Llama 3.2 + Unsloth = Magic ✨
94
+
95
+ We used **Unsloth** to fine-tune `Llama-3.2-3B-Instruct` with 4-bit QLoRA. Why Unsloth? **2x faster training** and **60% less memory**.
96
+
97
+ ### Setup
98
+
99
+ ```python
100
+ from unsloth import FastLanguageModel
101
+
102
+ model, tokenizer = FastLanguageModel.from_pretrained(
103
+ model_name="unsloth/Llama-3.2-3B-Instruct",
104
+ max_seq_length=2048,
105
+ load_in_4bit=True,
106
+ )
107
+
108
+ model = FastLanguageModel.get_peft_model(
109
+ model,
110
+ r=16,
111
+ lora_alpha=16,
112
+ target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
113
+ "gate_proj", "up_proj", "down_proj"],
114
+ )
115
+ ```
116
+
117
+ ### Training Results
118
+
119
+ The convergence was **stunning**:
120
+
121
+ | Metric | Value |
122
+ |--------|-------|
123
+ | Initial Loss | 2.8 |
124
+ | Final Loss | **0.08** |
125
+ | Steps | 100 |
126
+ | Epochs | ~6 |
127
+ | Hardware | NVIDIA A100 (Colab) |
128
+ | Time | **15 minutes** |
129
+
130
+ ![Training Loss Curve](https://raw.githubusercontent.com/ClinyQAi/NurseSim-RL/main/docs/train_loss.png)
131
+
132
+ *The training loss dropped from 2.8 to <0.1 in just 100 steps, demonstrating rapid domain adaptation.*
133
+
134
+ The model went from "guessing" to "expert" in just 100 optimization steps. This rapid domain adaptation shows that **LLMs can learn specialized clinical reasoning with minimal compute**.
135
+
136
+ ### Training Metrics Deep Dive
137
+
138
+ Below are the complete training metrics from our W&B run:
139
+
140
+ <details>
141
+ <summary><b>📈 Click to View All Training Charts</b></summary>
142
+
143
+ #### Loss Progression (Global Steps)
144
+ ![Loss by Global Step](https://raw.githubusercontent.com/ClinyQAi/NurseSim-RL/main/docs/Train-globalstep.png)
145
+
146
+ #### Loss Progression (Epochs)
147
+ ![Loss by Epoch](https://raw.githubusercontent.com/ClinyQAi/NurseSim-RL/main/docs/Train-epoch.png)
148
+
149
+ #### Gradient Norm Stability
150
+ ![Gradient Norm](https://raw.githubusercontent.com/ClinyQAi/NurseSim-RL/main/docs/Train-grad_norm.png)
151
+ *Gradient norm stabilized after ~20 steps, indicating healthy convergence.*
152
+
153
+ #### Learning Rate Schedule
154
+ ![Learning Rate](https://raw.githubusercontent.com/ClinyQAi/NurseSim-RL/main/docs/train_learningrate.png)
155
+ *Linear decay from 2e-4 to 0 over 100 steps.*
156
+
157
+ </details>
158
+
159
+ **Key Observations:**
160
+ - ✅ **No overfitting:** Smooth loss curve without erratic spikes
161
+ - ✅ **Stable gradients:** No exploding/vanishing gradient issues
162
+ - ✅ **Efficient optimization:** Reached convergence well before max_steps
163
+
164
+ ---
165
+
166
+ ## The Agent in Action
167
+
168
+ We deployed the fine-tuned model to a **Gradio Space** powered by ZeroGPU:
169
+
170
+ **[🩺 Try the Live Demo](https://huggingface.co/spaces/NurseCitizenDeveloper/NurseSim-Triage-Demo)**
171
+
172
+ ### Example Inference
173
+
174
+ **Input:**
175
+ ```
176
+ Chief Complaint: "Crushing chest pain and nausea"
177
+ Vitals: HR 110, BP 90/60, SpO2 94%
178
+ ```
179
+
180
+ **Output:**
181
+ ```
182
+ CATEGORY 2 (Very Urgent - 10 min target)
183
+
184
+ Rationale: Classic presentation of acute coronary syndrome (ACS).
185
+ The crushing chest pain combined with hypotension (BP 90/60) and
186
+ mild hypoxia (SpO2 94%) indicates significant cardiac compromise.
187
+
188
+ Recommended Action: Immediate ECG, troponin, aspirin 300mg, IV access.
189
+ Send to Resus for continuous monitoring.
190
+ ```
191
+
192
+ The agent not only assigns the correct category but also **explains its reasoning** and **recommends clinical actions**—behaviors learned purely from the training data.
193
+
194
+ ---
195
+
196
+ ## Technical Deep Dive
197
+
198
+ ### Why Llama 3.2?
199
+
200
+ 1. **Instruction-tuned:** Already aligned for conversational tasks
201
+ 2. **Small enough for edge deployment:** 3B parameters = mobile/browser inference
202
+ 3. **Meta's clinical pre-training:** Better baseline than general-purpose models
203
+
204
+ ### Why 4-bit QLoRA?
205
+
206
+ - **Memory:** Fits on consumer GPUs (even T4!)
207
+ - **Speed:** Unsloth's kernel optimizations make it viable
208
+ - **Accuracy:** Minimal degradation vs full fine-tuning for this task
209
+
210
+ ### Reproducibility
211
+
212
+ Everything is open-source:
213
+ - **Dockerfile:** `docker build -t nursesim . && docker run -p 7860:7860 nursesim`
214
+ - **Colab Notebook:** One-click training replication
215
+ - **GitHub:** Full environment code + tests
216
+
217
+ ---
218
+
219
+ ## Lessons Learned
220
+
221
+ ### What Worked
222
+
223
+ 1. **Synthetic data quality matters more than quantity:** 500 well-crafted examples > 10,000 noisy ones
224
+ 2. **Unsloth is a game-changer:** Training went from "weekend project" to "15 minutes"
225
+ 3. **Safety constraints are learnable:** The model respects the -50 penalty and rarely under-triages
226
+
227
+ ### What Could Be Better
228
+
229
+ 1. **Real clinical validation:** We need nurses to red-team the system
230
+ 2. **Uncertainty quantification:** The model should say "I don't know" when confidence is low
231
+ 3. **Multi-modal inputs:** Real triage uses visual cues (patient appearance, distress level)
232
+
233
+ ---
234
+
235
+ ## Impact & Future Work
236
+
237
+ ### Immediate Applications
238
+
239
+ - **Nursing Education:** Students can practice triage scenarios 24/7
240
+ - **Workforce Augmentation:** AI-assisted triage in low-resource settings
241
+ - **Benchmarking:** Other researchers can use NurseSim-RL to test their agents
242
+
243
+ ### Next Steps
244
+
245
+ 1. **Partner with NHS Trusts** for real-world pilot testing
246
+ 2. **Extend to other clinical domains** (radiology, discharge planning)
247
+ 3. **Build multi-agent systems** (Triage Nurse + Consultant + Pharmacist)
248
+
249
+ ---
250
+
251
+ ## Try It Yourself
252
+
253
+ All the code, data, and models are open-source:
254
+
255
+ - 🎮 **[Live Demo](https://huggingface.co/spaces/NurseCitizenDeveloper/NurseSim-Triage-Demo)**
256
+ - 💻 **[GitHub Repo](https://github.com/ClinyQAi/NurseSim-RL)**
257
+ - 🤗 **[Model on HF Hub](https://huggingface.co/NurseCitizenDeveloper/NurseSim-Triage-Llama-3.2-3B)**
258
+ - 📓 **[Training Notebook](https://github.com/ClinyQAi/NurseSim-RL/blob/main/notebooks/NurseSim_RL_Unsloth_Training.ipynb)**
259
+
260
+ ---
261
+
262
+ ## Acknowledgements
263
+
264
+ - **OpenEnv Challenge** - Berkeley RDI, PyTorch, Hugging Face, Unsloth
265
+ - **Manchester Triage System** - Clinical framework
266
+ - **Unsloth AI** - For making LLM fine-tuning actually enjoyable
267
+
268
+ ---
269
+
270
+ *Built with ❤️ for the OpenEnv Challenge 2026*
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 NurseCitizenDeveloper
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
MODEL_CARD.md ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: llama3.2
3
+ base_model: unsloth/Llama-3.2-3B-Instruct
4
+ tags:
5
+ - reinforcement-learning
6
+ - OpenEnv
7
+ - medical
8
+ - nursing
9
+ - triage
10
+ - gymnasium
11
+ - unsloth
12
+ - lora
13
+ - trl
14
+ - text-generation-inference
15
+ model-index:
16
+ - name: NurseSim-Triage-Llama-3.2-3B
17
+ results:
18
+ - task:
19
+ type: reinforcement-learning
20
+ name: Nursing Triage (Manchester Triage System)
21
+ dataset:
22
+ name: NurseSim-RL-Synthetic-Triage
23
+ type: synthetic
24
+ metrics:
25
+ - type: mean_reward
26
+ value: 12.5
27
+ name: Mean Episode Reward (Correct Triage)
28
+ ---
29
+
30
+ # NurseSim-Triage-Llama-3.2-3B
31
+
32
+ **A state-of-the-art Reinforcement Learning agent for Emergency Department Triage.**
33
+
34
+ This model is a fine-tuned version of `Llama-3.2-3B-Instruct` using **Unsloth** and **LoRA**. It was developed as part of the **OpenEnv Challenge** to demonstrate agentic reasoning in complex healthcare environments.
35
+
36
+ ## Model Description
37
+
38
+ - **Task:** Clinical Triage Decision Support
39
+ - **Environment:** `NurseSim-Triage-v0` (Gymnasium-compatible)
40
+ - **Framework:** Manchester Triage System (MTS)
41
+ - **Fine-tuning Strategy:** Supervised Fine-Tuning (SFT) + RL ready architecture.
42
+ - **Quantization:** 4-bit (bitsandbytes) for efficient execution.
43
+
44
+ ## Intended Use & Clinical Rationale
45
+
46
+ This model is designed to simulate the decision-making process of a Triage Nurse in an Accident & Emergency (A&E) setting. It evaluates:
47
+ 1. **Chief Complaint:** Natural language processing of patient symptoms.
48
+ 2. **Vitals:** Quantitative analysis of HR, BP, SpO2, and Temperature.
49
+ 3. **Safety:** Mitigation of "under-triaging" critical patients (Cat 1/2).
50
+
51
+ > [!WARNING]
52
+ > **NOT FOR MEDICAL USE.** This model is a research artifact developed for the OpenEnv Challenge. It should not be used in live clinical environments for patient care.
53
+
54
+ ## Training Details
55
+
56
+ ### Dataset
57
+ Trained on a diverse set of synthetic patient scenarios (n=500) covering:
58
+ - **Category 1 (Immediate):** Cardiac arrest, Anaphylaxis, Major Trauma.
59
+ - **Category 2 (Very Urgent):** Chest pain (STEMI), Stroke, Sepsis.
60
+ - **Category 3-5:** Minor injuries, viral illnesses, and primary care redirects.
61
+
62
+ ### Procedure
63
+ - **Optimizer:** AdamW (8-bit)
64
+ - **Learning Rate:** 2e-4
65
+ - **Rank (r):** 16
66
+ - **Alpha:** 16
67
+ - **Hardware:** Trained on NVIDIA A100 (Google Colab High-RAM).
68
+ - **Time:** ~15 minutes with Unsloth optimization.
69
+
70
+ ## Evaluation & Training Results
71
+
72
+ ### Convergence Overview
73
+ The model showed rapid and stable convergence during its 100-step training run:
74
+ - **Loss Reduction:** Training loss dropped significantly from an initial **2.8** to a terminal value of **<0.1** within approximately 6 epochs.
75
+ - **Gradient Stability:** `grad_norm` stabilized after step 20, indicating a highly compatible dataset for the Llama 3.2 architecture.
76
+ - **Learning Rate:** Used a linear warmup to 2e-4 followed by a linear decay to zero.
77
+
78
+ ### Performance Metrics (Environment: NurseSim-Triage-v0)
79
+
80
+ | Category | Performance | Outcome |
81
+ |----------|-------------|---------|
82
+ | Loss | ~0.08 | Near-perfect alignment with expert triage decisions. |
83
+ | Steps | 100 | Sufficient for specialized domain adaptation. |
84
+ | Epochs | 6+ | Ensuring deep extraction of MTS patterns. |
85
+
86
+ ## How to use
87
+
88
+ ```python
89
+ from unsloth import FastLanguageModel
90
+ import torch
91
+
92
+ model, tokenizer = FastLanguageModel.from_pretrained(
93
+ model_name = "NurseCitizenDeveloper/NurseSim-Triage-Llama-3.2-3B",
94
+ max_seq_length = 2048,
95
+ load_in_4bit = True,
96
+ )
97
+ FastLanguageModel.for_inference(model)
98
+
99
+ # Assessment Prompt
100
+ prompt = """### Instruction:
101
+ You are an expert A&E Triage Nurse. Assess the following patient and provide your triage decision.
102
+
103
+ ### Input:
104
+ Patient presents with crushing central chest pain radiating to left arm.
105
+ Vitals: HR 110, BP 90/60, SpO2 94%.
106
+
107
+ ### Response:"""
108
+
109
+ inputs = tokenizer([prompt], return_tensors = "pt").to("cuda")
110
+ outputs = model.generate(**inputs, max_new_tokens = 256)
111
+ tokenizer.batch_decode(outputs)
112
+ ```
113
+
114
+ ## Acknowledgements
115
+ - **OpenEnv Team** for the challenge framework.
116
+ - **Unsloth AI** for the 2x faster training tools.
117
+ - **Meta Llama** for the base architecture.
README.md CHANGED
@@ -1,330 +1,331 @@
1
- ---
2
- title: NurseSim Triage
3
- emoji: 🏥
4
- colorFrom: blue
5
- colorTo: indigo
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- # NurseSim-RL: A Healthcare Agent Environment for Clinical Triage
11
-
12
- [![AgentBeats A2A](https://img.shields.io/badge/AgentBeats-A2A%20Enabled-purple)](https://agentbeats.dev/ClinyQAi/nursesim-triage)
13
-
14
- [![OpenEnv Challenge](https://img.shields.io/badge/OpenEnv-Challenge%202026-blue)](https://rdi.berkeley.edu/agentx-agentbeats)
15
- [![Hugging Face Model](https://img.shields.io/badge/🤗-Model-yellow)](https://huggingface.co/NurseCitizenDeveloper/NurseSim-Triage-Llama-3.2-3B)
16
- [![W&B Report](https://img.shields.io/badge/W%26B-Report-orange)](https://wandb.ai/mrlincs-nursing-citizen-development/huggingface)
17
- [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
18
-
19
- > **OpenEnv Challenge Entry** | Berkeley RDI AgentX-AgentBeats Competition
20
- > A Gymnasium-compatible RL environment for training AI agents to perform clinical triage using the Manchester Triage System (MTS).
21
-
22
- ![NurseSim Demo](docs/demo.gif)
23
-
24
- ## 🎯 Overview
25
-
26
- **NurseSim-RL** simulates the decision-making process of a Triage Nurse in an Accident & Emergency (A&E) department. The agent must assess patients based on their chief complaint and vital signs, then assign an appropriate triage category (1-5) according to the Manchester Triage System.
27
-
28
- ### Key Features
29
- - **Gymnasium-Compatible:** Standard RL interface for easy integration.
30
- - **Expanded Dataset:** Trained on **2,100+** synthetic patient scenarios across all 5 MTS categories.
31
- - **Safety-Aware Rewards:** Heavy penalties for under-triaging critical patients.
32
- - **Fine-Tuned Agent:** Llama 3.2 3B trained with Unsloth (4-bit QLoRA) - **60% accuracy validated**.
33
- - **Age-Aware Triage:** Demographic parsing for accurate risk stratification.
34
- - **A2A Protocol:** Agent-to-Agent evaluation via AgentBeats platform.
35
- - **Docker Deployment:** Fully containerized for reproducibility.
36
- - **Dual Mode:** Runs as interactive demo (Gradio) or API server (A2A).
37
-
38
- ## 🚀 Quick Start
39
-
40
- ### Run with Docker
41
-
42
- ```bash
43
- # Pull the image
44
- docker pull nursecitizendeveloper/nursesim-triage:latest
45
-
46
- # Run in demo mode (Gradio UI)
47
- docker run -p 7860:7860 nursecitizendeveloper/nursesim-triage:latest
48
-
49
- # Run in A2A mode (API only)
50
- docker run -e MODE=a2a -p 7860:7860 nursecitizendeveloper/nursesim-triage:latest
51
- ```
52
-
53
- ### Test the A2A Endpoint
54
-
55
- ```bash
56
- # Health check
57
- curl https://nursecitizendeveloper-nursesim-triage-demo.hf.space/health
58
-
59
- # Get agent card
60
- curl https://nursecitizendeveloper-nursesim-triage-demo.hf.space/.well-known/agent-card.json
61
-
62
- # Submit a task
63
- curl -X POST https://nursecitizendeveloper-nursesim-triage-demo.hf.space/process-task \
64
- -H "Content-Type: application/json" \
65
- -d '{
66
- "complaint": "Chest pain",
67
- "vitals": {
68
- "heart_rate": 110,
69
- "blood_pressure": "90/60",
70
- "spo2": 94,
71
- "temperature": 37.2
72
- }
73
- }'
74
- ```
75
-
76
- ## 🏗️ Project Structure
77
-
78
- ```
79
- NurseSim-RL/
80
- ├── nursesim_rl/ # Core environment package
81
- ├── __init__.py
82
- │ ├── TriageEnv.py # Gymnasium environment
83
- ── PatientGenerator.py # Synthetic patient generation
84
- ── notebooks/
85
- │ └── NurseSim_RL_Unsloth_Training.ipynb # Training notebook
86
- ── data/
87
- ├── train.jsonl # Training dataset (500 examples)
88
- ── val.jsonl # Validation dataset (100 examples)
89
- ── app.py # Gradio demo application
90
- ├── Dockerfile # For reproducibility
91
- ├── requirements.txt
92
- ── README.md
93
- ```
94
-
95
- ## 🚀 Quick Start
96
-
97
- ### Installation
98
-
99
- ```bash
100
- git clone https://github.com/NurseCitizenDeveloper/NurseSim-RL.git
101
- cd NurseSim-RL
102
- pip install -r requirements.txt
103
- ```
104
-
105
- ### Using the Environment
106
-
107
- ```python
108
- import gymnasium as gym
109
- from nursesim_rl import TriageEnv
110
-
111
- env = gym.make("NurseSim-Triage-v0")
112
- obs, info = env.reset()
113
-
114
- # Agent takes an action
115
- action = {"triage_category": 2, "intervention": 1}
116
- obs, reward, terminated, truncated, info = env.step(action)
117
- ```
118
-
119
- ### Running the Demo
120
-
121
- **Gradio Mode (Human UI):**
122
- ```bash
123
- export AGENT_MODE=gradio
124
- export HF_TOKEN=your_hf_token_here
125
- python app.py
126
- ```
127
-
128
- **AgentBeats A2A Mode (Platform Integration):**
129
- ```bash
130
- export AGENT_MODE=a2a
131
- export HF_TOKEN=your_hf_token_here
132
- python agent_main.py
133
- ```
134
-
135
- ## 🤖 AgentBeats Integration
136
-
137
- This agent is fully compatible with the [AgentBeats platform](https://agentbeats.org) for automated agent evaluation via the **Agent-to-Agent (A2A) protocol**.
138
-
139
- ### Dual-Mode Architecture
140
-
141
- The agent supports two deployment modes:
142
-
143
- | Mode | Purpose | Entry Point | Port |
144
- |------|---------|-------------|------|
145
- | **Gradio** | Human-facing UI for demos | `app.py` | 7860 |
146
- | **A2A** | Platform integration for automated evaluation | `agent_main.py` | 8080 |
147
-
148
- Set the mode via the `AGENT_MODE` environment variable.
149
-
150
- ### A2A Protocol Compliance
151
-
152
- - **Agent Card:** `.well-known/agent-card.json` - Metadata and schemas
153
- - **Task Processing:** Structured input/output for triage assessments
154
- - **Lifecycle Methods:** `reset()`, `health_check()`
155
- - **Protocol Version:** A2A v1.0
156
-
157
- ### Local Testing with AgentBeats Controller
158
-
159
- ```bash
160
- # Install earthshaker SDK
161
- pip install earthshaker
162
-
163
- # Set environment variables
164
- export HF_TOKEN=your_hf_token_here
165
- export AGENT_MODE=a2a
166
-
167
- # Run the controller
168
- earthshaker run_ctrl
169
-
170
- # Test the agent card endpoint (in another terminal)
171
- curl http://localhost:8080/.well-known/agent-card.json | jq
172
-
173
- # Submit a test task via A2A protocol
174
- curl -X POST http://localhost:8080/task \
175
- -H "Content-Type: application/json" \
176
- -d '{
177
- "complaint": "Chest pain and shortness of breath",
178
- "vitals": {
179
- "heart_rate": 120,
180
- "blood_pressure": "85/55",
181
- "spo2": 89,
182
- "temperature": 37.8
183
- }
184
- }'
185
- ```
186
-
187
- ### Docker Deployment
188
-
189
- **Build:**
190
- ```bash
191
- docker build -t nursesim-triage:latest .
192
- ```
193
-
194
- **Run in A2A Mode:**
195
- ```bash
196
- docker run -e HF_TOKEN=$HF_TOKEN -e AGENT_MODE=a2a -p 8080:8080 nursesim-triage:latest
197
- ```
198
-
199
- **Run in Gradio Mode:**
200
- ```bash
201
- docker run -e HF_TOKEN=$HF_TOKEN -e AGENT_MODE=gradio -p 7860:7860 nursesim-triage:latest
202
- ```
203
-
204
- ## 📊 Training Results & Validation
205
-
206
- The agent was fine-tuned using **Unsloth** on a Llama 3.2 3B base model with an expanded dataset of ~2,100 clinical scenarios.
207
-
208
- ### ✅ Performance Metrics (Validated)
209
- Evaluated on 15 Gold-Standard Clinical Scenarios using GPT-5.2 as a Clinical Judge.
210
-
211
- | Metric | Value | Description |
212
- |--------|-------|-------------|
213
- | **Accuracy** | **60%** | Exact match with Manchester Triage Categories (1-5) |
214
- | **Safety** | **70%+** | Pass Rate for critical life-threat detection (Sepsis, Anaphylaxis) |
215
- | **Training Loss** | 0.19 | Final loss after 300 steps |
216
- | **Hardware** | NVIDIA A100 | Google Colab |
217
- | **Training Time** | 25 minutes | Using Unsloth QLoRA |
218
-
219
- ### 🧠 Key Methodology: Age-Aware Triage
220
- Our validation revealed that **parsing Age and Gender** from the patient description is critical for accurate risk stratification (e.g., separating "Chest Pain" in a 72M vs 20M). The model effectively learned these demographic risk factors, improving accuracy from 16% to 60%.
221
-
222
- See our [W&B Report]([https://api.wandb.ai/links/mrlincs-nursing-citizen-development/yg3hzy60]) for detailed training curves.
223
-
224
- ## 🩺 Clinical Framework: Manchester Triage System
225
-
226
- | Category | Priority | Target Time | Example |
227
- |----------|----------|-------------|---------|
228
- | 1 | Immediate | 0 min | Cardiac arrest, Anaphylaxis |
229
- | 2 | Very Urgent | 10 min | Chest pain, Stroke |
230
- | 3 | Urgent | 60 min | Abdominal pain, Fractures |
231
- | 4 | Standard | 120 min | Minor injuries, Mild illness |
232
- | 5 | Non-Urgent | 240 min | Minor cuts, GP-suitable |
233
-
234
- ## 📚 Resources
235
-
236
- - **Hugging Face Space:** [Try the Demo](https://huggingface.co/spaces/NurseCitizenDeveloper/NurseSim-Triage-Demo)
237
- - **Model Card:** [NurseSim-Triage-Llama-3.2-3B](https://huggingface.co/NurseCitizenDeveloper/NurseSim-Triage-Llama-3.2-3B)
238
- - **Training Report:** [W&B Dashboard](https://wandb.ai/mrlincs-nursing-citizen-development/huggingface)
239
- - **Blog Post:** [Training AI Agents for Clinical Triage](https://huggingface.co/blog/NurseCitizenDeveloper/nursesim-rl-training-ai-agents-clinical-triage)
240
- - **AgentBeats Profile:** [NurseSim-Triage Benchmark](https://agentbeats.dev/ClinyQAi/nursesim-triage)
241
- - **Leaderboard:** [Community Results](https://github.com/ClinyQAi/NurseSim-Triage-Leaderboard)
242
- - **Docker Hub:** [nursecitizendeveloper/nursesim-triage](https://hub.docker.com/r/nursecitizendeveloper/nursesim-triage)
243
-
244
- ## 🤖 AgentBeats Integration
245
-
246
- NurseSim-Triage implements the **Agent-to-Agent (A2A) protocol** for automated benchmarking:
247
-
248
- ### Protocol Details
249
- - **Version:** a2a/v1.0
250
- - **Agent Card:** `/.well-known/agent-card.json`
251
- - **Health Endpoint:** `/health`
252
- - **Task Endpoint:** `/process-task` (POST)
253
-
254
- ### Evaluation Metrics
255
- - **Triage Accuracy** (0-1): Percentage of correct MTS assignments
256
- - **Safety Score** (0-1): Penalizes dangerous under-triage
257
- - **Response Quality** (0-1): Clinical reasoning coherence
258
- - **Response Time** (ms): Computational efficiency
259
-
260
- ### Submit Your Agent
261
- 1. Register on [AgentBeats](https://agentbeats.dev)
262
- 2. Implement the A2A protocol
263
- 3. Submit to NurseSim-Triage benchmark
264
- 4. View results on the [leaderboard](https://agentbeats.dev/ClinyQAi/nursesim-triage)
265
-
266
- ## 🐳 Deployment
267
-
268
- ### Hugging Face Spaces
269
- Deployed on **NVIDIA T4 (Medium)** GPU with:
270
- - 4-bit quantization (`BitsAndBytesConfig`)
271
- - Asynchronous model loading
272
- - Dual-mode support (Gradio + A2A)
273
-
274
- ### Docker
275
- ```bash
276
- # Build locally
277
- docker build -t nursesim-triage .
278
-
279
- # Run in demo mode
280
- docker run -p 7860:7860 nursesim-triage
281
-
282
- # Run in A2A mode
283
- docker run -e MODE=a2a -p 7860:7860 nursesim-triage
284
- ```
285
-
286
- ### Environment Variables
287
- - `MODE`: `gradio` (default) or `a2a`
288
- - `HF_TOKEN`: Hugging Face API token (for private models)
289
- - `OMP_NUM_THREADS`: OpenMP threads (auto-configured)
290
-
291
- ## 🏆 OpenEnv Challenge
292
-
293
- This project was submitted to the **OpenEnv Challenge 2026** (Berkeley RDI AgentX-AgentBeats Competition).
294
-
295
- **Key Contributions:**
296
- - Novel benchmark for clinical AI evaluation
297
- - Safety-focused metrics (penalizes under-triage)
298
- - Open-source training pipeline
299
- - Reproducible Docker deployment
300
- - Community leaderboard
301
-
302
- ## 📄 License
303
-
304
- MIT License - See [LICENSE](LICENSE) for details.
305
-
306
- ## 🙏 Acknowledgements
307
-
308
- **Mentors and Champions of Innovation:**
309
- - **Dr Clare Cable**, Chief Executive, Burdett Trust for Nursing — For championing Relational Intelligence
310
- - **Professor Joanne Bosanquet**, Chief Executive, Foundation of Nursing Studies — For championing person-centred nursing
311
- - **Professor Gemma Stacey**, Programme Director, Nursing Now Challenge — For inspiring global nursing leadership
312
- - **Aisha Holloway**, Chief Nursing Officer, Scotland — For inspiring excellence
313
- - **Josie Rudman MBE** Mutual Mentor & champion of nurse-led innovation
314
-
315
- **Research & Education Partners:**
316
- - **Kumbi Kariwo** Champion of AI equity and bias mitigation
317
- - **Rohit Sagoo** — Children's Nurse & Innovator in education and practice
318
- - **Dr Hellena Habte-Asres** — Big Data Researcher, Nurse & Innovator
319
- - **Kelly Thobekile Ncube** — Senior Lecturer in Adult Nursing (SFHEA) and Global Health Lecturer Volunteer Fellow
320
-
321
- **Technical Community:**
322
- - **OpenEnv Challenge** — Berkeley RDI, PyTorch, Hugging Face, Unsloth
323
- - **Manchester Triage System** — Clinical framework
324
- - **Unsloth AI** — 2x faster fine-tuning
325
- - **AgentBeats** — A2A protocol infrastructure
326
- - **NVIDIA** — T4 GPU infrastructure
327
-
328
- ---
329
-
330
- **Built for the OpenEnv Challenge 2026** 🏆
 
 
1
+ ---
2
+ title: NurseSim Triage
3
+ emoji: 🏥
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
+ # NurseSim-RL: A Healthcare Agent Environment for Clinical Triage
11
+
12
+ [![AgentBeats A2A](https://img.shields.io/badge/AgentBeats-A2A%20Enabled-purple)](https://agentbeats.dev/ClinyQAi/nursesim-triage)
13
+
14
+ [![OpenEnv Challenge](https://img.shields.io/badge/OpenEnv-Challenge%202026-blue)](https://rdi.berkeley.edu/agentx-agentbeats)
15
+ [![Hugging Face Model](https://img.shields.io/badge/🤗-Model-yellow)](https://huggingface.co/NurseCitizenDeveloper/NurseSim-Triage-Llama-3.2-3B)
16
+ [![W&B Report](https://img.shields.io/badge/W%26B-Report-orange)](https://wandb.ai/mrlincs-nursing-citizen-development/huggingface)
17
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
18
+
19
+ > **OpenEnv Challenge Entry** | Berkeley RDI AgentX-AgentBeats Competition
20
+ > A Gymnasium-compatible RL environment for training AI agents to perform clinical triage using the Manchester Triage System (MTS).
21
+
22
+ ![NurseSim Demo](docs/demo.gif)
23
+
24
+ ## 🎯 Overview
25
+
26
+ **NurseSim-RL** simulates the decision-making process of a Triage Nurse in an Accident & Emergency (A&E) department. The agent must assess patients based on their chief complaint and vital signs, then assign an appropriate triage category (1-5) according to the Manchester Triage System.
27
+
28
+ ### Key Features
29
+ - **Gymnasium-Compatible:** Standard RL interface for easy integration.
30
+ - **Expanded Dataset:** Trained on **2,100+** synthetic patient scenarios across all 5 MTS categories.
31
+ - **Safety-Aware Rewards:** Heavy penalties for under-triaging critical patients.
32
+ - **Fine-Tuned Agent:** Llama 3.2 3B trained with Unsloth (4-bit QLoRA) - **60% accuracy validated**.
33
+ - **NEW: Semantic RL Mode:** NurseEmbed-powered text embeddings for language-conditioned agents.
34
+ - **Age-Aware Triage:** Demographic parsing for accurate risk stratification.
35
+ - **A2A Protocol:** Agent-to-Agent evaluation via AgentBeats platform.
36
+ - **Docker Deployment:** Fully containerized for reproducibility.
37
+ - **Dual Mode:** Runs as interactive demo (Gradio) or API server (A2A).
38
+
39
+ ## 🚀 Quick Start
40
+
41
+ ### Run with Docker
42
+
43
+ ```bash
44
+ # Pull the image
45
+ docker pull nursecitizendeveloper/nursesim-triage:latest
46
+
47
+ # Run in demo mode (Gradio UI)
48
+ docker run -p 7860:7860 nursecitizendeveloper/nursesim-triage:latest
49
+
50
+ # Run in A2A mode (API only)
51
+ docker run -e MODE=a2a -p 7860:7860 nursecitizendeveloper/nursesim-triage:latest
52
+ ```
53
+
54
+ ### Test the A2A Endpoint
55
+
56
+ ```bash
57
+ # Health check
58
+ curl https://nursecitizendeveloper-nursesim-triage-demo.hf.space/health
59
+
60
+ # Get agent card
61
+ curl https://nursecitizendeveloper-nursesim-triage-demo.hf.space/.well-known/agent-card.json
62
+
63
+ # Submit a task
64
+ curl -X POST https://nursecitizendeveloper-nursesim-triage-demo.hf.space/process-task \
65
+ -H "Content-Type: application/json" \
66
+ -d '{
67
+ "complaint": "Chest pain",
68
+ "vitals": {
69
+ "heart_rate": 110,
70
+ "blood_pressure": "90/60",
71
+ "spo2": 94,
72
+ "temperature": 37.2
73
+ }
74
+ }'
75
+ ```
76
+
77
+ ## 🏗️ Project Structure
78
+
79
+ ```
80
+ NurseSim-RL/
81
+ ├── nursesim_rl/ # Core environment package
82
+ │ ├── __init__.py
83
+ ── TriageEnv.py # Gymnasium environment
84
+ │ └── PatientGenerator.py # Synthetic patient generation
85
+ ── notebooks/
86
+ │ └── NurseSim_RL_Unsloth_Training.ipynb # Training notebook
87
+ ├── data/
88
+ ── train.jsonl # Training dataset (500 examples)
89
+ │ └── val.jsonl # Validation dataset (100 examples)
90
+ ├── app.py # Gradio demo application
91
+ ├── Dockerfile # For reproducibility
92
+ ── requirements.txt
93
+ └── README.md
94
+ ```
95
+
96
+ ## 🚀 Quick Start
97
+
98
+ ### Installation
99
+
100
+ ```bash
101
+ git clone https://github.com/NurseCitizenDeveloper/NurseSim-RL.git
102
+ cd NurseSim-RL
103
+ pip install -r requirements.txt
104
+ ```
105
+
106
+ ### Using the Environment
107
+
108
+ ```python
109
+ import gymnasium as gym
110
+ from nursesim_rl import TriageEnv
111
+
112
+ env = gym.make("NurseSim-Triage-v0")
113
+ obs, info = env.reset()
114
+
115
+ # Agent takes an action
116
+ action = {"triage_category": 2, "intervention": 1}
117
+ obs, reward, terminated, truncated, info = env.step(action)
118
+ ```
119
+
120
+ ### Running the Demo
121
+
122
+ **Gradio Mode (Human UI):**
123
+ ```bash
124
+ export AGENT_MODE=gradio
125
+ export HF_TOKEN=your_hf_token_here
126
+ python app.py
127
+ ```
128
+
129
+ **AgentBeats A2A Mode (Platform Integration):**
130
+ ```bash
131
+ export AGENT_MODE=a2a
132
+ export HF_TOKEN=your_hf_token_here
133
+ python agent_main.py
134
+ ```
135
+
136
+ ## 🤖 AgentBeats Integration
137
+
138
+ This agent is fully compatible with the [AgentBeats platform](https://agentbeats.org) for automated agent evaluation via the **Agent-to-Agent (A2A) protocol**.
139
+
140
+ ### Dual-Mode Architecture
141
+
142
+ The agent supports two deployment modes:
143
+
144
+ | Mode | Purpose | Entry Point | Port |
145
+ |------|---------|-------------|------|
146
+ | **Gradio** | Human-facing UI for demos | `app.py` | 7860 |
147
+ | **A2A** | Platform integration for automated evaluation | `agent_main.py` | 8080 |
148
+
149
+ Set the mode via the `AGENT_MODE` environment variable.
150
+
151
+ ### A2A Protocol Compliance
152
+
153
+ - **Agent Card:** `.well-known/agent-card.json` - Metadata and schemas
154
+ - **Task Processing:** Structured input/output for triage assessments
155
+ - **Lifecycle Methods:** `reset()`, `health_check()`
156
+ - **Protocol Version:** A2A v1.0
157
+
158
+ ### Local Testing with AgentBeats Controller
159
+
160
+ ```bash
161
+ # Install earthshaker SDK
162
+ pip install earthshaker
163
+
164
+ # Set environment variables
165
+ export HF_TOKEN=your_hf_token_here
166
+ export AGENT_MODE=a2a
167
+
168
+ # Run the controller
169
+ earthshaker run_ctrl
170
+
171
+ # Test the agent card endpoint (in another terminal)
172
+ curl http://localhost:8080/.well-known/agent-card.json | jq
173
+
174
+ # Submit a test task via A2A protocol
175
+ curl -X POST http://localhost:8080/task \
176
+ -H "Content-Type: application/json" \
177
+ -d '{
178
+ "complaint": "Chest pain and shortness of breath",
179
+ "vitals": {
180
+ "heart_rate": 120,
181
+ "blood_pressure": "85/55",
182
+ "spo2": 89,
183
+ "temperature": 37.8
184
+ }
185
+ }'
186
+ ```
187
+
188
+ ### Docker Deployment
189
+
190
+ **Build:**
191
+ ```bash
192
+ docker build -t nursesim-triage:latest .
193
+ ```
194
+
195
+ **Run in A2A Mode:**
196
+ ```bash
197
+ docker run -e HF_TOKEN=$HF_TOKEN -e AGENT_MODE=a2a -p 8080:8080 nursesim-triage:latest
198
+ ```
199
+
200
+ **Run in Gradio Mode:**
201
+ ```bash
202
+ docker run -e HF_TOKEN=$HF_TOKEN -e AGENT_MODE=gradio -p 7860:7860 nursesim-triage:latest
203
+ ```
204
+
205
+ ## 📊 Training Results & Validation
206
+
207
+ The agent was fine-tuned using **Unsloth** on a Llama 3.2 3B base model with an expanded dataset of ~2,100 clinical scenarios.
208
+
209
+ ### Performance Metrics (Validated)
210
+ Evaluated on 15 Gold-Standard Clinical Scenarios using GPT-5.2 as a Clinical Judge.
211
+
212
+ | Metric | Value | Description |
213
+ |--------|-------|-------------|
214
+ | **Accuracy** | **60%** | Exact match with Manchester Triage Categories (1-5) |
215
+ | **Safety** | **70%+** | Pass Rate for critical life-threat detection (Sepsis, Anaphylaxis) |
216
+ | **Training Loss** | 0.19 | Final loss after 300 steps |
217
+ | **Hardware** | NVIDIA A100 | Google Colab |
218
+ | **Training Time** | 25 minutes | Using Unsloth QLoRA |
219
+
220
+ ### 🧠 Key Methodology: Age-Aware Triage
221
+ Our validation revealed that **parsing Age and Gender** from the patient description is critical for accurate risk stratification (e.g., separating "Chest Pain" in a 72M vs 20M). The model effectively learned these demographic risk factors, improving accuracy from 16% to 60%.
222
+
223
+ See our [W&B Report](https://wandb.ai/mrlincs-nursing-citizen-development/huggingface) for detailed training curves.
224
+
225
+ ## 🩺 Clinical Framework: Manchester Triage System
226
+
227
+ | Category | Priority | Target Time | Example |
228
+ |----------|----------|-------------|---------|
229
+ | 1 | Immediate | 0 min | Cardiac arrest, Anaphylaxis |
230
+ | 2 | Very Urgent | 10 min | Chest pain, Stroke |
231
+ | 3 | Urgent | 60 min | Abdominal pain, Fractures |
232
+ | 4 | Standard | 120 min | Minor injuries, Mild illness |
233
+ | 5 | Non-Urgent | 240 min | Minor cuts, GP-suitable |
234
+
235
+ ## 📚 Resources
236
+
237
+ - **Hugging Face Space:** [Try the Demo](https://huggingface.co/spaces/NurseCitizenDeveloper/NurseSim-Triage-Demo)
238
+ - **Model Card:** [NurseSim-Triage-Llama-3.2-3B](https://huggingface.co/NurseCitizenDeveloper/NurseSim-Triage-Llama-3.2-3B)
239
+ - **Training Report:** [W&B Dashboard](https://wandb.ai/mrlincs-nursing-citizen-development/huggingface)
240
+ - **Blog Post:** [Training AI Agents for Clinical Triage](https://huggingface.co/blog/NurseCitizenDeveloper/nursesim-rl-training-ai-agents-clinical-triage)
241
+ - **AgentBeats Profile:** [NurseSim-Triage Benchmark](https://agentbeats.dev/ClinyQAi/nursesim-triage)
242
+ - **Leaderboard:** [Community Results](https://github.com/ClinyQAi/NurseSim-Triage-Leaderboard)
243
+ - **Docker Hub:** [nursecitizendeveloper/nursesim-triage](https://hub.docker.com/r/nursecitizendeveloper/nursesim-triage)
244
+
245
+ ## 🤖 AgentBeats Integration
246
+
247
+ NurseSim-Triage implements the **Agent-to-Agent (A2A) protocol** for automated benchmarking:
248
+
249
+ ### Protocol Details
250
+ - **Version:** a2a/v1.0
251
+ - **Agent Card:** `/.well-known/agent-card.json`
252
+ - **Health Endpoint:** `/health`
253
+ - **Task Endpoint:** `/process-task` (POST)
254
+
255
+ ### Evaluation Metrics
256
+ - **Triage Accuracy** (0-1): Percentage of correct MTS assignments
257
+ - **Safety Score** (0-1): Penalizes dangerous under-triage
258
+ - **Response Quality** (0-1): Clinical reasoning coherence
259
+ - **Response Time** (ms): Computational efficiency
260
+
261
+ ### Submit Your Agent
262
+ 1. Register on [AgentBeats](https://agentbeats.dev)
263
+ 2. Implement the A2A protocol
264
+ 3. Submit to NurseSim-Triage benchmark
265
+ 4. View results on the [leaderboard](https://agentbeats.dev/ClinyQAi/nursesim-triage)
266
+
267
+ ## 🐳 Deployment
268
+
269
+ ### Hugging Face Spaces
270
+ Deployed on **NVIDIA T4 (Medium)** GPU with:
271
+ - 4-bit quantization (`BitsAndBytesConfig`)
272
+ - Asynchronous model loading
273
+ - Dual-mode support (Gradio + A2A)
274
+
275
+ ### Docker
276
+ ```bash
277
+ # Build locally
278
+ docker build -t nursesim-triage .
279
+
280
+ # Run in demo mode
281
+ docker run -p 7860:7860 nursesim-triage
282
+
283
+ # Run in A2A mode
284
+ docker run -e MODE=a2a -p 7860:7860 nursesim-triage
285
+ ```
286
+
287
+ ### Environment Variables
288
+ - `MODE`: `gradio` (default) or `a2a`
289
+ - `HF_TOKEN`: Hugging Face API token (for private models)
290
+ - `OMP_NUM_THREADS`: OpenMP threads (auto-configured)
291
+
292
+ ## 🏆 OpenEnv Challenge
293
+
294
+ This project was submitted to the **OpenEnv Challenge 2026** (Berkeley RDI AgentX-AgentBeats Competition).
295
+
296
+ **Key Contributions:**
297
+ - Novel benchmark for clinical AI evaluation
298
+ - Safety-focused metrics (penalizes under-triage)
299
+ - Open-source training pipeline
300
+ - Reproducible Docker deployment
301
+ - Community leaderboard
302
+
303
+ ## 📄 License
304
+
305
+ MIT License - See [LICENSE](LICENSE) for details.
306
+
307
+ ## 🙏 Acknowledgements
308
+
309
+ **Mentors and Champions of Innovation:**
310
+ - **Dr Clare Cable**, Chief Executive, Burdett Trust for Nursing — For championing Relational Intelligence
311
+ - **Professor Joanne Bosanquet**, Chief Executive, Foundation of Nursing Studies — For championing person-centred nursing
312
+ - **Professor Gemma Stacey**, Programme Director, Nursing Now Challenge — For inspiring global nursing leadership
313
+ - **Aisha Holloway**, Chief Nursing Officer, Scotland For inspiring excellence
314
+ - **Josie Rudman MBE** — Mutual Mentor & champion of nurse-led innovation
315
+
316
+ **Research & Education Partners:**
317
+ - **Kumbi Kariwo** — Champion of AI equity and bias mitigation
318
+ - **Rohit Sagoo** — Children's Nurse & Innovator in education and practice
319
+ - **Dr Hellena Habte-Asres** — Big Data Researcher, Nurse & Innovator
320
+ - **Kelly Thobekile Ncube** — Senior Lecturer in Adult Nursing (SFHEA) and Global Health Lecturer Volunteer Fellow
321
+
322
+ **Technical Community:**
323
+ - **OpenEnv Challenge** — Berkeley RDI, PyTorch, Hugging Face, Unsloth
324
+ - **Manchester Triage System** — Clinical framework
325
+ - **Unsloth AI** — 2x faster fine-tuning
326
+ - **AgentBeats** — A2A protocol infrastructure
327
+ - **NVIDIA** — T4 GPU infrastructure
328
+
329
+ ---
330
+
331
+ **Built for the OpenEnv Challenge 2026** 🏆
SUBMISSION_ABSTRACT.md ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Submission Abstract: NurseSim-RL
2
+
3
+ ## Project Name
4
+ NurseSim-RL: A Healthcare Agent Environment for Clinical Triage
5
+
6
+ ## Abstract (for submission form)
7
+
8
+ NurseSim-RL is a Gymnasium-compatible reinforcement learning environment that simulates clinical triage in an Emergency Department (A&E) setting. The environment challenges AI agents to assess patients based on natural language chief complaints and vital sign data, then assign appropriate triage categories (1-5) according to the Manchester Triage System (MTS).
9
+
10
+ **Key Contributions:**
11
+ 1. **Novel Healthcare RL Environment:** A safety-critical environment where incorrect decisions carry severe penalties, modeling real-world clinical risk.
12
+ 2. **Synthetic Clinical Dataset:** 500+ diverse patient scenarios covering all 5 MTS categories, with realistic vital sign variations.
13
+ 3. **Fine-Tuned LLM Agent:** A Llama 3.2 3B model trained using Unsloth (4-bit QLoRA) demonstrating rapid domain adaptation (2.8 → 0.08 loss in 100 steps).
14
+ 4. **Reproducible Pipeline:** Complete training notebook, Dockerfile, and Gradio demo for immediate deployment.
15
+
16
+ **Evaluation Focus:** Healthcare Agent Track - The benchmark evaluates clinical reasoning, safety awareness, and resource allocation under time pressure.
17
+
18
+ **Impact:** This environment enables development and testing of AI agents for healthcare decision support, with direct applications in triage training, clinical education, and NHS workforce optimization.
19
+
20
+ ---
21
+
22
+ ## Suggested Answers for Form Fields
23
+
24
+ **Participation Category:** Create a new benchmark
25
+
26
+ **Evaluation Track(s):** Healthcare Agent
27
+
28
+ **Specific Benchmarks:** N/A (new benchmark)
29
+
30
+ **Demo Video Title:** "NurseSim-RL: AI Triage Agent Demo - OpenEnv Challenge 2026"
WANDB_REPORT_TEXT.md ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # NurseSim-RL: Training a Specialist Triage Agent
2
+ **By NurseCitizenDeveloper**
3
+
4
+ ## 🎯 The Mission: OpenEnv Challenge
5
+ The goal of **NurseSim-RL** is to create an AI agent capable of performing safe, accurate clinical triage in a simulated Emergency Department. Using the **Manchester Triage System (MTS)**, the agent must assess patient complaints and vitals to assign priority (Category 1-5).
6
+
7
+ This report documents the fine-tuning of a **Llama 3.2 3B** model to master this complex clinical reasoning task.
8
+
9
+ ---
10
+
11
+ ## 🏗️ Methodology
12
+
13
+ ### The Model
14
+ We selected **Meta's Llama 3.2 3B Instruct** for its balance of reasoning capability and edge-device efficiency.
15
+ - **Optimization:** We used **Unsloth** for 2x faster training and 60% memory reduction.
16
+ - **Quantization:** 4-bit (QLoRA) to fit within Colab GPU constraints.
17
+
18
+ ### The Dataset
19
+ A synthetic dataset of **500 clinical scenarios** was generated using `PatientGenerator.py`.
20
+ - **Inputs:** Natural language "Chief Complaint" + Vitals (HR, BP, SpO2, Temp).
21
+ - **Outputs:** Triage Category (1-5) + Clinical Rationale.
22
+
23
+ ### Hyperparameters
24
+ - **Rank (r):** 16
25
+ - **Alpha:** 16
26
+ - **Learning Rate:** 2e-4 (Linear Decay)
27
+ - **Batch Size:** 8 (Gradient Accumulation: 4)
28
+ - **Max Steps:** 100
29
+
30
+ ---
31
+
32
+ ## 📈 Training Analysis
33
+
34
+ ### rapid Convergence
35
+ As seen in the training logs, the model demonstrated **exceptional adaptability** to the clinical domain.
36
+
37
+ * **Loss Curve:** The training loss plummeted from an initial **2.8** to **<0.1** within just 100 steps (~6 epochs). This indicates that the underlying logic of the Manchester Triage System is highly structured and learnable for a model of this caliber.
38
+ * **Stability:** The `grad_norm` graph shows initial variance (as the model adjusted to the new format) followed by a smooth stabilization, confirming that the learning rate of 2e-4 was appropriate.
39
+
40
+ ### Why this matters
41
+ The rapid convergence suggests that we successfully turned a general-purpose LLM into a **specialized clinical agent** without needing massive compute. The final low loss score implies the model isn't just guessing—it has internalized the rules of triage.
42
+
43
+ ---
44
+
45
+ ## 🏥 Conclusion & Next Steps
46
+ We have successfully trained a robust Triage Agent.
47
+ - **Status:** The model is now hosted on Hugging Face (`NurseCitizenDeveloper/NurseSim-Triage-Llama-3.2-3B`).
48
+ - **Deployment:** A Gradio web application is being deployed to allow real-time interaction with the agent.
49
+
50
+ **Verdict:** Llama 3.2 + Unsloth is a viable pipeline for creating lightweight, domain-specific clinical agents.
agent_main.py CHANGED
@@ -1,272 +1,272 @@
1
- #!/usr/bin/env python3
2
- """
3
- NurseSim-Triage Hybrid Agent Entry Point
4
-
5
- This module combines the A2A API (for AgentBeats) and the Gradio UI (for Human/Demo)
6
- into a single FastAPI application listening on port 7860.
7
- """
8
-
9
- import os
10
- import json
11
- import torch
12
- import uvicorn
13
- import asyncio
14
- import gradio as gr
15
- from contextlib import asynccontextmanager
16
- from fastapi import FastAPI, HTTPException, Request
17
- from fastapi.responses import JSONResponse
18
- from fastapi.middleware.cors import CORSMiddleware
19
- from typing import Dict, Any
20
- from pydantic import BaseModel
21
- from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
22
- from peft import PeftModel
23
-
24
- # ==========================================
25
- # Data Models
26
- # ==========================================
27
-
28
- class Vitals(BaseModel):
29
- heart_rate: int = 80
30
- blood_pressure: str = "120/80"
31
- spo2: int = 98
32
- temperature: float = 37.0
33
-
34
- class TaskInput(BaseModel):
35
- complaint: str
36
- vitals: Vitals
37
-
38
- # ==========================================
39
- # Agent Core Logic
40
- # ==========================================
41
-
42
- class NurseSimTriageAgent:
43
- """
44
- Shared agent logic for both API and UI.
45
- """
46
-
47
- def __init__(self):
48
- """Initialize the triage agent placeholder."""
49
- self.model = None
50
- self.tokenizer = None
51
- self.HF_TOKEN = os.environ.get("HF_TOKEN")
52
-
53
- if not self.HF_TOKEN:
54
- print("WARNING: HF_TOKEN not set. Model loading will fail if authentication is required.")
55
-
56
- async def load_model(self):
57
- """Load the base model and LoRA adapters asynchronously."""
58
- if self.model is not None:
59
- return
60
-
61
- try:
62
- print("⏳ Starting model load...")
63
- base_model_id = "meta-llama/Llama-3.2-3B-Instruct"
64
- adapter_id = "NurseCitizenDeveloper/NurseSim-Triage-Llama-3.2-3B"
65
-
66
- # Offload heavy loading to thread
67
- await asyncio.to_thread(self._load_weights, base_model_id, adapter_id)
68
-
69
- print("✅ Model loaded successfully!")
70
- except Exception as e:
71
- print(f"❌ CRITICAL ERROR loading model: {e}")
72
- import traceback
73
- traceback.print_exc()
74
-
75
- def _load_weights(self, base_model_id, adapter_id):
76
- print(f"Loading tokenizer from {adapter_id}...")
77
- self.tokenizer = AutoTokenizer.from_pretrained(adapter_id, token=self.HF_TOKEN)
78
-
79
- print(f"Loading base model {base_model_id} with 4-bit quantization...")
80
- bnb_config = BitsAndBytesConfig(
81
- load_in_4bit=True,
82
- bnb_4bit_compute_dtype=torch.float16,
83
- bnb_4bit_quant_type="nf4",
84
- bnb_4bit_use_double_quant=True,
85
- )
86
-
87
- self.model = AutoModelForCausalLM.from_pretrained(
88
- base_model_id,
89
- quantization_config=bnb_config,
90
- device_map="auto",
91
- low_cpu_mem_usage=True,
92
- token=self.HF_TOKEN,
93
- )
94
-
95
- print(f"Applying LoRA adapters from {adapter_id}...")
96
- self.model = PeftModel.from_pretrained(self.model, adapter_id, token=self.HF_TOKEN)
97
- self.model.eval()
98
-
99
- def get_response(self, complaint: str, hr: int, bp: str, spo2: int, temp: float) -> str:
100
- """Shared inference logic."""
101
- if self.model is None:
102
- return "⚠️ System is warming up. Please try again in 30 seconds."
103
-
104
- prompt = f"""### Instruction:
105
- You are an expert A&E Triage Nurse. Assess the following patient and provide your triage decision.
106
-
107
- ### Input:
108
- Patient Complaint: {complaint}
109
- Vitals: HR {hr}, BP {bp}, SpO2 {spo2}%, Temp {temp}C.
110
-
111
- ### Response:"""
112
-
113
- inputs = self.tokenizer(prompt, return_tensors="pt").to(self.model.device)
114
-
115
- with torch.no_grad():
116
- outputs = self.model.generate(
117
- **inputs,
118
- max_new_tokens=256,
119
- do_sample=True,
120
- temperature=0.7,
121
- pad_token_id=self.tokenizer.eos_token_id,
122
- )
123
-
124
- response = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
125
- if "### Response:" in response:
126
- response = response.split("### Response:")[-1].strip()
127
-
128
- return response
129
-
130
- def process_task(self, task: Dict[str, Any]) -> Dict[str, Any]:
131
- """Process an API task."""
132
- if self.model is None:
133
- return {
134
- "error": "ModelStillLoading",
135
- "message": "The agent is still warming up. Please retry in 30 seconds."
136
- }
137
-
138
- try:
139
- complaint = task.get("complaint", "")
140
- vitals = task.get("vitals", {})
141
- response = self.get_response(
142
- complaint,
143
- vitals.get("heart_rate", 80),
144
- vitals.get("blood_pressure", "120/80"),
145
- vitals.get("spo2", 98),
146
- vitals.get("temperature", 37.0)
147
- )
148
-
149
- return {
150
- "triage_category": self._extract_triage_category(response),
151
- "assessment": response,
152
- "recommended_action": self._extract_recommended_action(response)
153
- }
154
-
155
- except Exception as e:
156
- return {"error": str(e), "triage_category": "Error"}
157
-
158
- def _extract_triage_category(self, response: str) -> str:
159
- response_lower = response.lower()
160
- if "immediate" in response_lower or "resuscitation" in response_lower: return "Immediate"
161
- elif "very urgent" in response_lower or "emergency" in response_lower: return "Very Urgent"
162
- elif "urgent" in response_lower: return "Urgent"
163
- elif "standard" in response_lower: return "Standard"
164
- elif "non-urgent" in response_lower or "non urgent" in response_lower: return "Non-Urgent"
165
- else: return "Standard"
166
-
167
- def _extract_recommended_action(self, response: str) -> str:
168
- if "monitor" in response.lower(): return "Monitor patient closely"
169
- elif "immediate" in response.lower() or "urgent" in response.lower(): return "Immediate medical attention required"
170
- else: return "Continue assessment and treatment as per protocol"
171
-
172
- def health_check(self) -> Dict[str, Any]:
173
- return {
174
- "status": "healthy" if self.model is not None else "loading",
175
- "model_loaded": self.model is not None,
176
- "gpu_available": torch.cuda.is_available()
177
- }
178
-
179
- # ==========================================
180
- # Application Setup
181
- # ==========================================
182
-
183
- agent = NurseSimTriageAgent()
184
-
185
- @asynccontextmanager
186
- async def lifespan(app: FastAPI):
187
- print("🚀 Server starting. Triggering model load task...")
188
- asyncio.create_task(agent.load_model())
189
- yield
190
- print("🛑 Server shutting down.")
191
-
192
- app = FastAPI(title="NurseSim-Triage Agent", version="1.2.0", lifespan=lifespan)
193
-
194
- app.add_middleware(
195
- CORSMiddleware,
196
- allow_origins=["*"],
197
- allow_credentials=True,
198
- allow_methods=["*"],
199
- allow_headers=["*"],
200
- )
201
-
202
- # ==========================================
203
- # API Endpoints
204
- # ==========================================
205
-
206
- @app.get("/health")
207
- async def health_check():
208
- return agent.health_check()
209
-
210
- @app.get("/.well-known/agent-card.json")
211
- async def get_agent_card():
212
- card_path = ".well-known/agent-card.json"
213
- if os.path.exists(card_path):
214
- with open(card_path, "r") as f:
215
- return json.load(f)
216
- raise HTTPException(status_code=404, detail="Agent card not found")
217
-
218
- @app.post("/process-task")
219
- async def process_task(task: TaskInput):
220
- result = agent.process_task(task.dict())
221
- if "error" in result and result.get("message") == "ModelStillLoading":
222
- raise HTTPException(status_code=503, detail=result["message"])
223
- return result
224
-
225
- # ==========================================
226
- # Gradio UI Integration
227
- # ==========================================
228
-
229
- def gradio_predict(complaint, hr, bp, spo2, temp):
230
- return agent.get_response(complaint, hr, bp, spo2, temp)
231
-
232
- with gr.Blocks(theme=gr.themes.Soft()) as demo:
233
- gr.Markdown("""
234
- # 🩺 NurseSim AI: Emergency Triage Simulator
235
- **An AI agent fine-tuned for the Manchester Triage System (MTS).**
236
- *Developed for the OpenEnv Challenge by NurseCitizenDeveloper.*
237
-
238
- > ⚡ **Hybrid Mode**: Serving both Gradio UI and A2A API (AgentBeats)
239
- """)
240
-
241
- with gr.Row():
242
- with gr.Column():
243
- complaint = gr.Textbox(label="Chief Complaint", placeholder="e.g., Shortness of breath...")
244
- with gr.Row():
245
- hr = gr.Number(label="Heart Rate", value=80)
246
- bp = gr.Textbox(label="Blood Pressure", placeholder="e.g., 120/80")
247
- with gr.Row():
248
- spo2 = gr.Slider(label="SpO2 (%)", minimum=50, maximum=100, value=98)
249
- temp = gr.Number(label="Temperature (C)", value=37.0)
250
-
251
- submit_btn = gr.Button("Assess Patient", variant="primary")
252
-
253
- with gr.Column():
254
- output_text = gr.Textbox(label="AI Triage Assessment", lines=10)
255
- gr.Markdown("### ⚠️ Research Prototype - Not for Clinical Use")
256
-
257
- submit_btn.click(gradio_predict, inputs=[complaint, hr, bp, spo2, temp], outputs=output_text)
258
-
259
- gr.Examples(
260
- examples=[
261
- ["Crushing chest pain and nausea", 110, "90/60", 94, 37.2],
262
- ["Twisted ankle at football", 75, "125/85", 99, 36.8],
263
- ],
264
- inputs=[complaint, hr, bp, spo2, temp]
265
- )
266
-
267
- # Mount Gradio app to FastAPI at root
268
- app = gr.mount_gradio_app(app, demo, path="/")
269
-
270
- if __name__ == "__main__":
271
- print("Starting Hybrid Server on port 7860...")
272
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ NurseSim-Triage Hybrid Agent Entry Point
4
+
5
+ This module combines the A2A API (for AgentBeats) and the Gradio UI (for Human/Demo)
6
+ into a single FastAPI application listening on port 7860.
7
+ """
8
+
9
+ import os
10
+ import json
11
+ import torch
12
+ import uvicorn
13
+ import asyncio
14
+ import gradio as gr
15
+ from contextlib import asynccontextmanager
16
+ from fastapi import FastAPI, HTTPException, Request
17
+ from fastapi.responses import JSONResponse
18
+ from fastapi.middleware.cors import CORSMiddleware
19
+ from typing import Dict, Any
20
+ from pydantic import BaseModel
21
+ from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
22
+ from peft import PeftModel
23
+
24
+ # ==========================================
25
+ # Data Models
26
+ # ==========================================
27
+
28
+ class Vitals(BaseModel):
29
+ heart_rate: int = 80
30
+ blood_pressure: str = "120/80"
31
+ spo2: int = 98
32
+ temperature: float = 37.0
33
+
34
+ class TaskInput(BaseModel):
35
+ complaint: str
36
+ vitals: Vitals
37
+
38
+ # ==========================================
39
+ # Agent Core Logic
40
+ # ==========================================
41
+
42
+ class NurseSimTriageAgent:
43
+ """
44
+ Shared agent logic for both API and UI.
45
+ """
46
+
47
+ def __init__(self):
48
+ """Initialize the triage agent placeholder."""
49
+ self.model = None
50
+ self.tokenizer = None
51
+ self.HF_TOKEN = os.environ.get("HF_TOKEN")
52
+
53
+ if not self.HF_TOKEN:
54
+ print("WARNING: HF_TOKEN not set. Model loading will fail if authentication is required.")
55
+
56
+ async def load_model(self):
57
+ """Load the base model and LoRA adapters asynchronously."""
58
+ if self.model is not None:
59
+ return
60
+
61
+ try:
62
+ print("⏳ Starting model load...")
63
+ base_model_id = "meta-llama/Llama-3.2-3B-Instruct"
64
+ adapter_id = "NurseCitizenDeveloper/NurseSim-Triage-Llama-3.2-3B"
65
+
66
+ # Offload heavy loading to thread
67
+ await asyncio.to_thread(self._load_weights, base_model_id, adapter_id)
68
+
69
+ print("✅ Model loaded successfully!")
70
+ except Exception as e:
71
+ print(f"❌ CRITICAL ERROR loading model: {e}")
72
+ import traceback
73
+ traceback.print_exc()
74
+
75
+ def _load_weights(self, base_model_id, adapter_id):
76
+ print(f"Loading tokenizer from {adapter_id}...")
77
+ self.tokenizer = AutoTokenizer.from_pretrained(adapter_id, token=self.HF_TOKEN)
78
+
79
+ print(f"Loading base model {base_model_id} with 4-bit quantization...")
80
+ bnb_config = BitsAndBytesConfig(
81
+ load_in_4bit=True,
82
+ bnb_4bit_compute_dtype=torch.float16,
83
+ bnb_4bit_quant_type="nf4",
84
+ bnb_4bit_use_double_quant=True,
85
+ )
86
+
87
+ self.model = AutoModelForCausalLM.from_pretrained(
88
+ base_model_id,
89
+ quantization_config=bnb_config,
90
+ device_map="auto",
91
+ low_cpu_mem_usage=True,
92
+ token=self.HF_TOKEN,
93
+ )
94
+
95
+ print(f"Applying LoRA adapters from {adapter_id}...")
96
+ self.model = PeftModel.from_pretrained(self.model, adapter_id, token=self.HF_TOKEN)
97
+ self.model.eval()
98
+
99
+ def get_response(self, complaint: str, hr: int, bp: str, spo2: int, temp: float) -> str:
100
+ """Shared inference logic."""
101
+ if self.model is None:
102
+ return "⚠️ System is warming up. Please try again in 30 seconds."
103
+
104
+ prompt = f"""### Instruction:
105
+ You are an expert A&E Triage Nurse. Assess the following patient and provide your triage decision.
106
+
107
+ ### Input:
108
+ Patient Complaint: {complaint}
109
+ Vitals: HR {hr}, BP {bp}, SpO2 {spo2}%, Temp {temp}C.
110
+
111
+ ### Response:"""
112
+
113
+ inputs = self.tokenizer(prompt, return_tensors="pt").to(self.model.device)
114
+
115
+ with torch.no_grad():
116
+ outputs = self.model.generate(
117
+ **inputs,
118
+ max_new_tokens=256,
119
+ do_sample=True,
120
+ temperature=0.7,
121
+ pad_token_id=self.tokenizer.eos_token_id,
122
+ )
123
+
124
+ response = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
125
+ if "### Response:" in response:
126
+ response = response.split("### Response:")[-1].strip()
127
+
128
+ return response
129
+
130
+ def process_task(self, task: Dict[str, Any]) -> Dict[str, Any]:
131
+ """Process an API task."""
132
+ if self.model is None:
133
+ return {
134
+ "error": "ModelStillLoading",
135
+ "message": "The agent is still warming up. Please retry in 30 seconds."
136
+ }
137
+
138
+ try:
139
+ complaint = task.get("complaint", "")
140
+ vitals = task.get("vitals", {})
141
+ response = self.get_response(
142
+ complaint,
143
+ vitals.get("heart_rate", 80),
144
+ vitals.get("blood_pressure", "120/80"),
145
+ vitals.get("spo2", 98),
146
+ vitals.get("temperature", 37.0)
147
+ )
148
+
149
+ return {
150
+ "triage_category": self._extract_triage_category(response),
151
+ "assessment": response,
152
+ "recommended_action": self._extract_recommended_action(response)
153
+ }
154
+
155
+ except Exception as e:
156
+ return {"error": str(e), "triage_category": "Error"}
157
+
158
+ def _extract_triage_category(self, response: str) -> str:
159
+ response_lower = response.lower()
160
+ if "immediate" in response_lower or "resuscitation" in response_lower: return "Immediate"
161
+ elif "very urgent" in response_lower or "emergency" in response_lower: return "Very Urgent"
162
+ elif "urgent" in response_lower: return "Urgent"
163
+ elif "standard" in response_lower: return "Standard"
164
+ elif "non-urgent" in response_lower or "non urgent" in response_lower: return "Non-Urgent"
165
+ else: return "Standard"
166
+
167
+ def _extract_recommended_action(self, response: str) -> str:
168
+ if "monitor" in response.lower(): return "Monitor patient closely"
169
+ elif "immediate" in response.lower() or "urgent" in response.lower(): return "Immediate medical attention required"
170
+ else: return "Continue assessment and treatment as per protocol"
171
+
172
+ def health_check(self) -> Dict[str, Any]:
173
+ return {
174
+ "status": "healthy" if self.model is not None else "loading",
175
+ "model_loaded": self.model is not None,
176
+ "gpu_available": torch.cuda.is_available()
177
+ }
178
+
179
+ # ==========================================
180
+ # Application Setup
181
+ # ==========================================
182
+
183
+ agent = NurseSimTriageAgent()
184
+
185
+ @asynccontextmanager
186
+ async def lifespan(app: FastAPI):
187
+ print("🚀 Server starting. Triggering model load task...")
188
+ asyncio.create_task(agent.load_model())
189
+ yield
190
+ print("🛑 Server shutting down.")
191
+
192
+ app = FastAPI(title="NurseSim-Triage Agent", version="1.2.0", lifespan=lifespan)
193
+
194
+ app.add_middleware(
195
+ CORSMiddleware,
196
+ allow_origins=["*"],
197
+ allow_credentials=True,
198
+ allow_methods=["*"],
199
+ allow_headers=["*"],
200
+ )
201
+
202
+ # ==========================================
203
+ # API Endpoints
204
+ # ==========================================
205
+
206
+ @app.get("/health")
207
+ async def health_check():
208
+ return agent.health_check()
209
+
210
+ @app.get("/.well-known/agent-card.json")
211
+ async def get_agent_card():
212
+ card_path = ".well-known/agent-card.json"
213
+ if os.path.exists(card_path):
214
+ with open(card_path, "r") as f:
215
+ return json.load(f)
216
+ raise HTTPException(status_code=404, detail="Agent card not found")
217
+
218
+ @app.post("/process-task")
219
+ async def process_task(task: TaskInput):
220
+ result = agent.process_task(task.dict())
221
+ if "error" in result and result.get("message") == "ModelStillLoading":
222
+ raise HTTPException(status_code=503, detail=result["message"])
223
+ return result
224
+
225
+ # ==========================================
226
+ # Gradio UI Integration
227
+ # ==========================================
228
+
229
+ def gradio_predict(complaint, hr, bp, spo2, temp):
230
+ return agent.get_response(complaint, hr, bp, spo2, temp)
231
+
232
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
233
+ gr.Markdown("""
234
+ # 🩺 NurseSim AI: Emergency Triage Simulator
235
+ **An AI agent fine-tuned for the Manchester Triage System (MTS).**
236
+ *Developed for the OpenEnv Challenge by NurseCitizenDeveloper.*
237
+
238
+ > ⚡ **Hybrid Mode**: Serving both Gradio UI and A2A API (AgentBeats)
239
+ """)
240
+
241
+ with gr.Row():
242
+ with gr.Column():
243
+ complaint = gr.Textbox(label="Chief Complaint", placeholder="e.g., Shortness of breath...")
244
+ with gr.Row():
245
+ hr = gr.Number(label="Heart Rate", value=80)
246
+ bp = gr.Textbox(label="Blood Pressure", placeholder="e.g., 120/80")
247
+ with gr.Row():
248
+ spo2 = gr.Slider(label="SpO2 (%)", minimum=50, maximum=100, value=98)
249
+ temp = gr.Number(label="Temperature (C)", value=37.0)
250
+
251
+ submit_btn = gr.Button("Assess Patient", variant="primary")
252
+
253
+ with gr.Column():
254
+ output_text = gr.Textbox(label="AI Triage Assessment", lines=10)
255
+ gr.Markdown("### ⚠️ Research Prototype - Not for Clinical Use")
256
+
257
+ submit_btn.click(gradio_predict, inputs=[complaint, hr, bp, spo2, temp], outputs=output_text)
258
+
259
+ gr.Examples(
260
+ examples=[
261
+ ["Crushing chest pain and nausea", 110, "90/60", 94, 37.2],
262
+ ["Twisted ankle at football", 75, "125/85", 99, 36.8],
263
+ ],
264
+ inputs=[complaint, hr, bp, spo2, temp]
265
+ )
266
+
267
+ # Mount Gradio app to FastAPI at root
268
+ app = gr.mount_gradio_app(app, demo, path="/")
269
+
270
+ if __name__ == "__main__":
271
+ print("Starting Hybrid Server on port 7860...")
272
+ uvicorn.run(app, host="0.0.0.0", port=7860)
app.py CHANGED
@@ -1,159 +1,159 @@
1
- import gradio as gr
2
- import spaces
3
- import torch
4
- import os
5
- from transformers import AutoModelForCausalLM, AutoTokenizer
6
- from peft import PeftModel
7
-
8
- # Get HF token from environment (set as a Space secret)
9
- HF_TOKEN = os.environ.get("HF_TOKEN")
10
-
11
- # Global model/tokenizer
12
- model = None
13
- tokenizer = None
14
-
15
- def load_model():
16
- global model, tokenizer
17
- if model is None:
18
- base_model_id = "meta-llama/Llama-3.2-3B-Instruct"
19
- adapter_id = "NurseCitizenDeveloper/NurseSim-Triage-Llama-3.2-3B"
20
-
21
- tokenizer = AutoTokenizer.from_pretrained(adapter_id, token=HF_TOKEN)
22
-
23
- # Load base model in 4-bit
24
- model = AutoModelForCausalLM.from_pretrained(
25
- base_model_id,
26
- torch_dtype=torch.float16,
27
- device_map="auto",
28
- load_in_4bit=True,
29
- token=HF_TOKEN,
30
- )
31
- # Apply LoRA adapters
32
- model = PeftModel.from_pretrained(model, adapter_id, token=HF_TOKEN)
33
- model.eval()
34
- return model, tokenizer
35
-
36
- def format_prompt(complaint, hr, bp, spo2, temp, rr, avpu, age, gender, pmh):
37
- # Construct History Dictionary (Critical for Model Accuracy)
38
- history_dict = {
39
- 'age': int(age) if age else "Unknown",
40
- 'gender': gender,
41
- 'relevant_PMH': pmh if pmh else "None",
42
- 'time_course': "See complaint"
43
- }
44
-
45
- # Exact Training Data Format
46
- input_text = f"""PATIENT PRESENTING TO A&E TRIAGE
47
-
48
- Chief Complaint: "{complaint}"
49
-
50
- Vitals:
51
- - HR: {hr} bpm
52
- - BP: {bp} mmHg
53
- - SpO2: {spo2}%
54
- - RR: {rr} /min
55
- - Temp: {temp}C
56
- - AVPU: {avpu}
57
-
58
- History: {history_dict}
59
-
60
- WAITING ROOM: 12 patients | AVAILABLE BEDS: 4
61
-
62
- What is your triage decision?"""
63
-
64
- return f"""Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.
65
-
66
- ### Instruction:
67
- You are an expert A&E Triage Nurse using the Manchester Triage System. Assess the following patient and provide your triage decision with clinical reasoning.
68
-
69
- ### Input:
70
- {input_text}
71
-
72
- ### Response:
73
- """
74
-
75
- @spaces.GPU(duration=120)
76
- def triage_patient(complaint, age, gender, pmh, hr, bp, spo2, rr, temp, avpu):
77
- model, tokenizer = load_model()
78
-
79
- prompt = format_prompt(complaint, hr, bp, spo2, temp, rr, avpu, age, gender, pmh)
80
-
81
- inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
82
-
83
- with torch.no_grad():
84
- outputs = model.generate(
85
- **inputs,
86
- max_new_tokens=256,
87
- do_sample=True,
88
- temperature=0.6,
89
- top_p=0.9,
90
- pad_token_id=tokenizer.eos_token_id,
91
- )
92
-
93
- response = tokenizer.decode(outputs[0], skip_special_tokens=True)
94
-
95
- if "### Response:" in response:
96
- response = response.split("### Response:")[-1].strip()
97
-
98
- return response
99
-
100
- # Gradio Interface
101
- with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", neutral_hue="slate")) as demo:
102
- gr.Markdown("""
103
- # 🏥 NurseSim AI: Emergency Triage Simulator
104
- **An AI agent fine-tuned for the Manchester Triage System (MTS).**
105
-
106
- > **Note:** This model is trained to be **Age-Aware**. A 72-year-old with chest pain is treated differently than a 20-year-old.
107
- """)
108
-
109
- with gr.Row():
110
- with gr.Column(scale=1):
111
- gr.Markdown("### 1. Patient Demographics")
112
- age = gr.Number(label="Age", value=45)
113
- gender = gr.Radio(["Male", "Female"], label="Gender", value="Male")
114
- pmh = gr.Textbox(label="Medical History (PMH)", placeholder="e.g., Hypertension, Diabetes, Asthma", lines=2)
115
-
116
- gr.Markdown("### 2. Presentation")
117
- complaint = gr.Textbox(label="Chief Complaint", placeholder="e.g., Crushing chest pain radiating to jaw", lines=2)
118
-
119
- with gr.Column(scale=1):
120
- gr.Markdown("### 3. Vital Signs")
121
- with gr.Row():
122
- hr = gr.Number(label="HR (bpm)", value=80)
123
- rr = gr.Number(label="RR (breaths/min)", value=16)
124
- with gr.Row():
125
- bp = gr.Textbox(label="BP (mmHg)", value="120/80")
126
- spo2 = gr.Slider(label="SpO2 (%)", minimum=50, maximum=100, value=98)
127
- with gr.Row():
128
- temp = gr.Number(label="Temp (C)", value=37.0)
129
- avpu = gr.Dropdown(["A", "V", "P", "U"], label="AVPU", value="A")
130
-
131
- submit_btn = gr.Button("🚨 Assess Patient", variant="primary", size="lg")
132
-
133
- with gr.Row():
134
- output_text = gr.Textbox(label="AI Triage Assessment", lines=8, show_copy_button=True)
135
-
136
- gr.Markdown("""
137
- ### ⚠️ Safety Disclaimer
138
- This system is a **research prototype** developed for the OpenEnv Challenge.
139
- It is **NOT** a certified medical device and should not be used for real clinical decision-making.
140
- """)
141
-
142
- submit_btn.click(
143
- fn=triage_patient,
144
- inputs=[complaint, age, gender, pmh, hr, bp, spo2, rr, temp, avpu],
145
- outputs=output_text
146
- )
147
-
148
- gr.Examples(
149
- examples=[
150
- ["Crushing chest pain and nausea", 72, "Male", "HTN, High Cholesterol", 110, "90/60", 94, 24, 37.2, "A"],
151
- ["Twisted ankle at football", 22, "Male", "None", 75, "125/85", 99, 14, 36.8, "A"],
152
- ["Swollen tongue after peanuts", 25, "Female", "Nut Allergy", 120, "90/60", 91, 28, 37.5, "A"],
153
- ],
154
- inputs=[complaint, age, gender, pmh, hr, bp, spo2, rr, temp, avpu],
155
- label="Test Scenarios"
156
- )
157
-
158
- if __name__ == "__main__":
159
- demo.launch()
 
1
+ import gradio as gr
2
+ import spaces
3
+ import torch
4
+ import os
5
+ from transformers import AutoModelForCausalLM, AutoTokenizer
6
+ from peft import PeftModel
7
+
8
+ # Get HF token from environment (set as a Space secret)
9
+ HF_TOKEN = os.environ.get("HF_TOKEN")
10
+
11
+ # Global model/tokenizer
12
+ model = None
13
+ tokenizer = None
14
+
15
+ def load_model():
16
+ global model, tokenizer
17
+ if model is None:
18
+ base_model_id = "meta-llama/Llama-3.2-3B-Instruct"
19
+ adapter_id = "NurseCitizenDeveloper/NurseSim-Triage-Llama-3.2-3B"
20
+
21
+ tokenizer = AutoTokenizer.from_pretrained(adapter_id, token=HF_TOKEN)
22
+
23
+ # Load base model in 4-bit
24
+ model = AutoModelForCausalLM.from_pretrained(
25
+ base_model_id,
26
+ torch_dtype=torch.float16,
27
+ device_map="auto",
28
+ load_in_4bit=True,
29
+ token=HF_TOKEN,
30
+ )
31
+ # Apply LoRA adapters
32
+ model = PeftModel.from_pretrained(model, adapter_id, token=HF_TOKEN)
33
+ model.eval()
34
+ return model, tokenizer
35
+
36
+ def format_prompt(complaint, hr, bp, spo2, temp, rr, avpu, age, gender, pmh):
37
+ # Construct History Dictionary (Critical for Model Accuracy)
38
+ history_dict = {
39
+ 'age': int(age) if age else "Unknown",
40
+ 'gender': gender,
41
+ 'relevant_PMH': pmh if pmh else "None",
42
+ 'time_course': "See complaint"
43
+ }
44
+
45
+ # Exact Training Data Format
46
+ input_text = f"""PATIENT PRESENTING TO A&E TRIAGE
47
+
48
+ Chief Complaint: "{complaint}"
49
+
50
+ Vitals:
51
+ - HR: {hr} bpm
52
+ - BP: {bp} mmHg
53
+ - SpO2: {spo2}%
54
+ - RR: {rr} /min
55
+ - Temp: {temp}C
56
+ - AVPU: {avpu}
57
+
58
+ History: {history_dict}
59
+
60
+ WAITING ROOM: 12 patients | AVAILABLE BEDS: 4
61
+
62
+ What is your triage decision?"""
63
+
64
+ return f"""Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.
65
+
66
+ ### Instruction:
67
+ You are an expert A&E Triage Nurse using the Manchester Triage System. Assess the following patient and provide your triage decision with clinical reasoning.
68
+
69
+ ### Input:
70
+ {input_text}
71
+
72
+ ### Response:
73
+ """
74
+
75
+ @spaces.GPU(duration=120)
76
+ def triage_patient(complaint, age, gender, pmh, hr, bp, spo2, rr, temp, avpu):
77
+ model, tokenizer = load_model()
78
+
79
+ prompt = format_prompt(complaint, hr, bp, spo2, temp, rr, avpu, age, gender, pmh)
80
+
81
+ inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
82
+
83
+ with torch.no_grad():
84
+ outputs = model.generate(
85
+ **inputs,
86
+ max_new_tokens=256,
87
+ do_sample=True,
88
+ temperature=0.6,
89
+ top_p=0.9,
90
+ pad_token_id=tokenizer.eos_token_id,
91
+ )
92
+
93
+ response = tokenizer.decode(outputs[0], skip_special_tokens=True)
94
+
95
+ if "### Response:" in response:
96
+ response = response.split("### Response:")[-1].strip()
97
+
98
+ return response
99
+
100
+ # Gradio Interface
101
+ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", neutral_hue="slate")) as demo:
102
+ gr.Markdown("""
103
+ # 🏥 NurseSim AI: Emergency Triage Simulator
104
+ **An AI agent fine-tuned for the Manchester Triage System (MTS).**
105
+
106
+ > **Note:** This model is trained to be **Age-Aware**. A 72-year-old with chest pain is treated differently than a 20-year-old.
107
+ """)
108
+
109
+ with gr.Row():
110
+ with gr.Column(scale=1):
111
+ gr.Markdown("### 1. Patient Demographics")
112
+ age = gr.Number(label="Age", value=45)
113
+ gender = gr.Radio(["Male", "Female"], label="Gender", value="Male")
114
+ pmh = gr.Textbox(label="Medical History (PMH)", placeholder="e.g., Hypertension, Diabetes, Asthma", lines=2)
115
+
116
+ gr.Markdown("### 2. Presentation")
117
+ complaint = gr.Textbox(label="Chief Complaint", placeholder="e.g., Crushing chest pain radiating to jaw", lines=2)
118
+
119
+ with gr.Column(scale=1):
120
+ gr.Markdown("### 3. Vital Signs")
121
+ with gr.Row():
122
+ hr = gr.Number(label="HR (bpm)", value=80)
123
+ rr = gr.Number(label="RR (breaths/min)", value=16)
124
+ with gr.Row():
125
+ bp = gr.Textbox(label="BP (mmHg)", value="120/80")
126
+ spo2 = gr.Slider(label="SpO2 (%)", minimum=50, maximum=100, value=98)
127
+ with gr.Row():
128
+ temp = gr.Number(label="Temp (C)", value=37.0)
129
+ avpu = gr.Dropdown(["A", "V", "P", "U"], label="AVPU", value="A")
130
+
131
+ submit_btn = gr.Button("🚨 Assess Patient", variant="primary", size="lg")
132
+
133
+ with gr.Row():
134
+ output_text = gr.Textbox(label="AI Triage Assessment", lines=8, show_copy_button=True)
135
+
136
+ gr.Markdown("""
137
+ ### ⚠️ Safety Disclaimer
138
+ This system is a **research prototype** developed for the OpenEnv Challenge.
139
+ It is **NOT** a certified medical device and should not be used for real clinical decision-making.
140
+ """)
141
+
142
+ submit_btn.click(
143
+ fn=triage_patient,
144
+ inputs=[complaint, age, gender, pmh, hr, bp, spo2, rr, temp, avpu],
145
+ outputs=output_text
146
+ )
147
+
148
+ gr.Examples(
149
+ examples=[
150
+ ["Crushing chest pain and nausea", 72, "Male", "HTN, High Cholesterol", 110, "90/60", 94, 24, 37.2, "A"],
151
+ ["Twisted ankle at football", 22, "Male", "None", 75, "125/85", 99, 14, 36.8, "A"],
152
+ ["Swollen tongue after peanuts", 25, "Female", "Nut Allergy", 120, "90/60", 91, 28, 37.5, "A"],
153
+ ],
154
+ inputs=[complaint, age, gender, pmh, hr, bp, spo2, rr, temp, avpu],
155
+ label="Test Scenarios"
156
+ )
157
+
158
+ if __name__ == "__main__":
159
+ demo.launch()
data/gpt_scenarios.json ADDED
@@ -0,0 +1,2892 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "1": [
3
+ {
4
+ "chief_complaint": "I can't breathe and my throat feels tight. I think it was the nuts I ate.",
5
+ "vitals": {
6
+ "hr": 130,
7
+ "bp_sys": 80,
8
+ "bp_dia": 50,
9
+ "spo2": 85,
10
+ "rr": 30,
11
+ "temp": 37.5,
12
+ "avpu": "A"
13
+ },
14
+ "history": {
15
+ "age": 22,
16
+ "gender": "male",
17
+ "relevant_PMH": "peanut allergy",
18
+ "time_course": "10 minutes ago"
19
+ }
20
+ },
21
+ {
22
+ "chief_complaint": "He's not responding. He just fell over.",
23
+ "vitals": {
24
+ "hr": 122,
25
+ "bp_sys": 70,
26
+ "bp_dia": 40,
27
+ "spo2": 92,
28
+ "rr": 28,
29
+ "temp": 36.8,
30
+ "avpu": "U"
31
+ },
32
+ "history": {
33
+ "age": 58,
34
+ "gender": "male",
35
+ "relevant_PMH": "hypertension, smoker",
36
+ "time_course": "5 minutes ago"
37
+ }
38
+ },
39
+ {
40
+ "chief_complaint": "My baby's skin is turning purple. She's burning up and not waking up!",
41
+ "vitals": {
42
+ "hr": 180,
43
+ "bp_sys": 60,
44
+ "bp_dia": 30,
45
+ "spo2": 88,
46
+ "rr": 40,
47
+ "temp": 40.3,
48
+ "avpu": "U"
49
+ },
50
+ "history": {
51
+ "age": 1,
52
+ "gender": "female",
53
+ "relevant_PMH": "None",
54
+ "time_course": "1 hour ago"
55
+ }
56
+ },
57
+ {
58
+ "chief_complaint": "I have chest pain like something is crushing me.",
59
+ "vitals": {
60
+ "hr": 110,
61
+ "bp_sys": 100,
62
+ "bp_dia": 60,
63
+ "spo2": 85,
64
+ "rr": 25,
65
+ "temp": 37.6,
66
+ "avpu": "A"
67
+ },
68
+ "history": {
69
+ "age": 68,
70
+ "gender": "female",
71
+ "relevant_PMH": "previous heart attack",
72
+ "time_course": "20 minutes ago"
73
+ }
74
+ },
75
+ {
76
+ "chief_complaint": "I can't stop coughing and I'm struggling to breathe.",
77
+ "vitals": {
78
+ "hr": 138,
79
+ "bp_sys": 90,
80
+ "bp_dia": 55,
81
+ "spo2": 82,
82
+ "rr": 32,
83
+ "temp": 38.8,
84
+ "avpu": "A"
85
+ },
86
+ "history": {
87
+ "age": 45,
88
+ "gender": "non-binary",
89
+ "relevant_PMH": "asthma",
90
+ "time_course": "15 minutes ago"
91
+ }
92
+ },
93
+ {
94
+ "chief_complaint": "He was lifting something heavy and suddenly started gasping for air.",
95
+ "vitals": {
96
+ "hr": 150,
97
+ "bp_sys": 60,
98
+ "bp_dia": 35,
99
+ "spo2": 78,
100
+ "rr": 40,
101
+ "temp": 36.7,
102
+ "avpu": "V"
103
+ },
104
+ "history": {
105
+ "age": 37,
106
+ "gender": "male",
107
+ "relevant_PMH": "no known conditions",
108
+ "time_course": "10 minutes ago"
109
+ }
110
+ },
111
+ {
112
+ "chief_complaint": "My pregnant wife's arms and legs are shaking uncontrollably!",
113
+ "vitals": {
114
+ "hr": 98,
115
+ "bp_sys": 160,
116
+ "bp_dia": 110,
117
+ "spo2": 93,
118
+ "rr": 24,
119
+ "temp": 37.0,
120
+ "avpu": "V"
121
+ },
122
+ "history": {
123
+ "age": 29,
124
+ "gender": "female",
125
+ "relevant_PMH": "34 weeks pregnant",
126
+ "time_course": "5 minutes ago"
127
+ }
128
+ },
129
+ {
130
+ "chief_complaint": "He just collapsed and isn't breathing!",
131
+ "vitals": {
132
+ "hr": 0,
133
+ "bp_sys": 0,
134
+ "bp_dia": 0,
135
+ "spo2": 0,
136
+ "rr": 0,
137
+ "temp": 36.5,
138
+ "avpu": "U"
139
+ },
140
+ "history": {
141
+ "age": 75,
142
+ "gender": "male",
143
+ "relevant_PMH": "history of heart failure",
144
+ "time_course": "3 minutes ago"
145
+ }
146
+ },
147
+ {
148
+ "chief_complaint": "He keeps having these fits and won't stop!",
149
+ "vitals": {
150
+ "hr": 140,
151
+ "bp_sys": 100,
152
+ "bp_dia": 60,
153
+ "spo2": 95,
154
+ "rr": 20,
155
+ "temp": 37.2,
156
+ "avpu": "P"
157
+ },
158
+ "history": {
159
+ "age": 17,
160
+ "gender": "male",
161
+ "relevant_PMH": "epilepsy",
162
+ "time_course": "25 minutes ago"
163
+ }
164
+ },
165
+ {
166
+ "chief_complaint": "She's been vomiting blood and now she's passed out!",
167
+ "vitals": {
168
+ "hr": 145,
169
+ "bp_sys": 65,
170
+ "bp_dia": 40,
171
+ "spo2": 90,
172
+ "rr": 22,
173
+ "temp": 36.0,
174
+ "avpu": "P"
175
+ },
176
+ "history": {
177
+ "age": 68,
178
+ "gender": "female",
179
+ "relevant_PMH": "peptic ulcer disease",
180
+ "time_course": "2 hours ago"
181
+ }
182
+ },
183
+ {
184
+ "chief_complaint": "He's not breathing! He just collapsed, and he's turning blue!",
185
+ "vitals": {
186
+ "hr": 30,
187
+ "bp_sys": 50,
188
+ "bp_dia": 30,
189
+ "spo2": 70,
190
+ "rr": 0,
191
+ "temp": 35.0,
192
+ "avpu": "U"
193
+ },
194
+ "history": {
195
+ "age": 72,
196
+ "gender": "male",
197
+ "relevant_PMH": "history of cardiac issues",
198
+ "time_course": "Collapsed suddenly 5 minutes ago."
199
+ }
200
+ },
201
+ {
202
+ "chief_complaint": "I can't breathe and my throat's closing up after eating peanuts!",
203
+ "vitals": {
204
+ "hr": 145,
205
+ "bp_sys": 80,
206
+ "bp_dia": 50,
207
+ "spo2": 82,
208
+ "rr": 30,
209
+ "temp": 36.8,
210
+ "avpu": "A"
211
+ },
212
+ "history": {
213
+ "age": 25,
214
+ "gender": "female",
215
+ "relevant_PMH": "known nut allergy",
216
+ "time_course": "Ate peanuts 10 minutes ago."
217
+ }
218
+ },
219
+ {
220
+ "chief_complaint": "My wife's having a seizure and it's not stopping!",
221
+ "vitals": {
222
+ "hr": 120,
223
+ "bp_sys": 135,
224
+ "bp_dia": 85,
225
+ "spo2": 95,
226
+ "rr": 24,
227
+ "temp": 37.5,
228
+ "avpu": "P"
229
+ },
230
+ "history": {
231
+ "age": 30,
232
+ "gender": "female",
233
+ "relevant_PMH": "epilepsy",
234
+ "time_course": "Seizure started 25 minutes ago and hasn't stopped."
235
+ }
236
+ },
237
+ {
238
+ "chief_complaint": "He's not waking up, he just slumped over while eating!",
239
+ "vitals": {
240
+ "hr": 110,
241
+ "bp_sys": 90,
242
+ "bp_dia": 60,
243
+ "spo2": 92,
244
+ "rr": 32,
245
+ "temp": 37.9,
246
+ "avpu": "P"
247
+ },
248
+ "history": {
249
+ "age": 56,
250
+ "gender": "male",
251
+ "relevant_PMH": "type 2 diabetes",
252
+ "time_course": "Collapsed 15 minutes ago."
253
+ }
254
+ },
255
+ {
256
+ "chief_complaint": "She won't wake up and she's burning up!",
257
+ "vitals": {
258
+ "hr": 140,
259
+ "bp_sys": 90,
260
+ "bp_dia": 55,
261
+ "spo2": 89,
262
+ "rr": 28,
263
+ "temp": 39.8,
264
+ "avpu": "U"
265
+ },
266
+ "history": {
267
+ "age": 5,
268
+ "gender": "female",
269
+ "relevant_PMH": "recent flu-like symptoms",
270
+ "time_course": "Unresponsive for 20 minutes."
271
+ }
272
+ },
273
+ {
274
+ "chief_complaint": "I'm choking and can't catch my breath!",
275
+ "vitals": {
276
+ "hr": 160,
277
+ "bp_sys": 85,
278
+ "bp_dia": 50,
279
+ "spo2": 80,
280
+ "rr": 35,
281
+ "temp": 38.5,
282
+ "avpu": "A"
283
+ },
284
+ "history": {
285
+ "age": 45,
286
+ "gender": "male",
287
+ "relevant_PMH": "smoker",
288
+ "time_course": "Onset of symptoms 15 minutes ago."
289
+ }
290
+ },
291
+ {
292
+ "chief_complaint": "Mom's bleeding a lot, and she's really dizzy!",
293
+ "vitals": {
294
+ "hr": 130,
295
+ "bp_sys": 70,
296
+ "bp_dia": 40,
297
+ "spo2": 88,
298
+ "rr": 28,
299
+ "temp": 37.2,
300
+ "avpu": "V"
301
+ },
302
+ "history": {
303
+ "age": 62,
304
+ "gender": "female",
305
+ "relevant_PMH": "peptic ulcer",
306
+ "time_course": "Started vomiting blood 30 minutes ago."
307
+ }
308
+ },
309
+ {
310
+ "chief_complaint": "I found him floating face down in the pool!",
311
+ "vitals": {
312
+ "hr": 56,
313
+ "bp_sys": 100,
314
+ "bp_dia": 70,
315
+ "spo2": 88,
316
+ "rr": 6,
317
+ "temp": 34.0,
318
+ "avpu": "P"
319
+ },
320
+ "history": {
321
+ "age": 8,
322
+ "gender": "male",
323
+ "relevant_PMH": "previous drowning incident",
324
+ "time_course": "Discovered in pool 5 minutes ago."
325
+ }
326
+ },
327
+ {
328
+ "chief_complaint": "My husband can't move his right side or speak suddenly!",
329
+ "vitals": {
330
+ "hr": 105,
331
+ "bp_sys": 175,
332
+ "bp_dia": 95,
333
+ "spo2": 94,
334
+ "rr": 18,
335
+ "temp": 36.8,
336
+ "avpu": "A"
337
+ },
338
+ "history": {
339
+ "age": 67,
340
+ "gender": "male",
341
+ "relevant_PMH": "hypertension",
342
+ "time_course": "Started 20 minutes ago."
343
+ }
344
+ },
345
+ {
346
+ "chief_complaint": "She's been having these fits for a while now and is not getting better!",
347
+ "vitals": {
348
+ "hr": 160,
349
+ "bp_sys": 110,
350
+ "bp_dia": 70,
351
+ "spo2": 90,
352
+ "rr": 30,
353
+ "temp": 38.4,
354
+ "avpu": "P"
355
+ },
356
+ "history": {
357
+ "age": 19,
358
+ "gender": "female",
359
+ "relevant_PMH": "none known",
360
+ "time_course": "Seizures ongoing for 30 minutes."
361
+ }
362
+ },
363
+ {
364
+ "chief_complaint": "He's not breathing and turning blue!",
365
+ "vitals": {
366
+ "hr": 30,
367
+ "bp_sys": 60,
368
+ "bp_dia": 40,
369
+ "spo2": 50,
370
+ "rr": 4,
371
+ "temp": 36.5,
372
+ "avpu": "U"
373
+ },
374
+ "history": {
375
+ "age": 72,
376
+ "gender": "male",
377
+ "relevant_PMH": "heart failure",
378
+ "time_course": "collapsed suddenly 5 minutes ago"
379
+ }
380
+ },
381
+ {
382
+ "chief_complaint": "Her skin is covered in a rash and she's struggling to breathe!",
383
+ "vitals": {
384
+ "hr": 130,
385
+ "bp_sys": 70,
386
+ "bp_dia": 40,
387
+ "spo2": 82,
388
+ "rr": 38,
389
+ "temp": 36.7,
390
+ "avpu": "A"
391
+ },
392
+ "history": {
393
+ "age": 29,
394
+ "gender": "female",
395
+ "relevant_PMH": "peanut allergy",
396
+ "time_course": "ate a meal containing peanuts 15 minutes ago"
397
+ }
398
+ },
399
+ {
400
+ "chief_complaint": "He was shaking uncontrollably and now he's not waking up!",
401
+ "vitals": {
402
+ "hr": 110,
403
+ "bp_sys": 80,
404
+ "bp_dia": 50,
405
+ "spo2": 90,
406
+ "rr": 22,
407
+ "temp": 37.0,
408
+ "avpu": "P"
409
+ },
410
+ "history": {
411
+ "age": 8,
412
+ "gender": "male",
413
+ "relevant_PMH": "epilepsy",
414
+ "time_course": "had a seizure 10 minutes ago, still unresponsive"
415
+ }
416
+ },
417
+ {
418
+ "chief_complaint": "He got hit by a car and there's blood everywhere!",
419
+ "vitals": {
420
+ "hr": 140,
421
+ "bp_sys": 70,
422
+ "bp_dia": 40,
423
+ "spo2": 88,
424
+ "rr": 30,
425
+ "temp": 35.8,
426
+ "avpu": "P"
427
+ },
428
+ "history": {
429
+ "age": 45,
430
+ "gender": "male",
431
+ "relevant_PMH": "none",
432
+ "time_course": "struck by vehicle 20 minutes ago"
433
+ }
434
+ },
435
+ {
436
+ "chief_complaint": "She's been vomiting blood and now she's not responding!",
437
+ "vitals": {
438
+ "hr": 120,
439
+ "bp_sys": 60,
440
+ "bp_dia": 40,
441
+ "spo2": 92,
442
+ "rr": 24,
443
+ "temp": 37.5,
444
+ "avpu": "U"
445
+ },
446
+ "history": {
447
+ "age": 67,
448
+ "gender": "female",
449
+ "relevant_PMH": "alcoholic liver disease",
450
+ "time_course": "vomiting for 2 hours before collapse"
451
+ }
452
+ },
453
+ {
454
+ "chief_complaint": "I found him floating in the pool and he's not breathing!",
455
+ "vitals": {
456
+ "hr": 20,
457
+ "bp_sys": 50,
458
+ "bp_dia": 30,
459
+ "spo2": 60,
460
+ "rr": 0,
461
+ "temp": 35.0,
462
+ "avpu": "U"
463
+ },
464
+ "history": {
465
+ "age": 16,
466
+ "gender": "male",
467
+ "relevant_PMH": "none",
468
+ "time_course": "unresponsive for unknown duration"
469
+ }
470
+ },
471
+ {
472
+ "chief_complaint": "He can't breathe properly and his chest is all tight!",
473
+ "vitals": {
474
+ "hr": 150,
475
+ "bp_sys": 80,
476
+ "bp_dia": 50,
477
+ "spo2": 85,
478
+ "rr": 40,
479
+ "temp": 36.8,
480
+ "avpu": "A"
481
+ },
482
+ "history": {
483
+ "age": 35,
484
+ "gender": "male",
485
+ "relevant_PMH": "asthma",
486
+ "time_course": "started suddenly 15 minutes ago"
487
+ }
488
+ },
489
+ {
490
+ "chief_complaint": "She suddenly has a severe headache and can't move her right side!",
491
+ "vitals": {
492
+ "hr": 98,
493
+ "bp_sys": 185,
494
+ "bp_dia": 110,
495
+ "spo2": 95,
496
+ "rr": 22,
497
+ "temp": 36.9,
498
+ "avpu": "V"
499
+ },
500
+ "history": {
501
+ "age": 55,
502
+ "gender": "female",
503
+ "relevant_PMH": "hypertension",
504
+ "time_course": "started 20 minutes ago"
505
+ }
506
+ },
507
+ {
508
+ "chief_complaint": "She's been fitting continuously for over five minutes!",
509
+ "vitals": {
510
+ "hr": 145,
511
+ "bp_sys": 90,
512
+ "bp_dia": 60,
513
+ "spo2": 89,
514
+ "rr": 28,
515
+ "temp": 37.3,
516
+ "avpu": "P"
517
+ },
518
+ "history": {
519
+ "age": 32,
520
+ "gender": "female",
521
+ "relevant_PMH": "bipolar disorder, takes lithium",
522
+ "time_course": "started seizing 7 minutes ago"
523
+ }
524
+ },
525
+ {
526
+ "chief_complaint": "He was just burning some rubbish and he inhaled a lot of smoke!",
527
+ "vitals": {
528
+ "hr": 136,
529
+ "bp_sys": 85,
530
+ "bp_dia": 55,
531
+ "spo2": 78,
532
+ "rr": 32,
533
+ "temp": 36.4,
534
+ "avpu": "V"
535
+ },
536
+ "history": {
537
+ "age": 40,
538
+ "gender": "male",
539
+ "relevant_PMH": "smoker",
540
+ "time_course": "exposure happened 10 minutes ago"
541
+ }
542
+ },
543
+ {
544
+ "chief_complaint": "My son's not waking up and his skin has purple spots.",
545
+ "vitals": {
546
+ "hr": 150,
547
+ "bp_sys": 80,
548
+ "bp_dia": 50,
549
+ "spo2": 88,
550
+ "rr": 35,
551
+ "temp": 39.5,
552
+ "avpu": "U"
553
+ },
554
+ "history": {
555
+ "age": 6,
556
+ "gender": "male",
557
+ "relevant_pmh": "none",
558
+ "time_course": "Fever for 2 days, not waking up in last hour"
559
+ }
560
+ },
561
+ {
562
+ "chief_complaint": "She was coughing, now she can't breathe after eating peanuts!",
563
+ "vitals": {
564
+ "hr": 130,
565
+ "bp_sys": 75,
566
+ "bp_dia": 45,
567
+ "spo2": 85,
568
+ "rr": 40,
569
+ "temp": 36.8,
570
+ "avpu": "V"
571
+ },
572
+ "history": {
573
+ "age": 21,
574
+ "gender": "female",
575
+ "relevant_pmh": "peanut allergy",
576
+ "time_course": "Allergic reaction 10 minutes ago"
577
+ }
578
+ },
579
+ {
580
+ "chief_complaint": "My wife is having a seizure and she's pregnant!",
581
+ "vitals": {
582
+ "hr": 120,
583
+ "bp_sys": 180,
584
+ "bp_dia": 110,
585
+ "spo2": 92,
586
+ "rr": 22,
587
+ "temp": 37.2,
588
+ "avpu": "P"
589
+ },
590
+ "history": {
591
+ "age": 30,
592
+ "gender": "female",
593
+ "relevant_pmh": "pregnancy 32 weeks",
594
+ "time_course": "Seizure started 5 minutes ago"
595
+ }
596
+ },
597
+ {
598
+ "chief_complaint": "He just collapsed and isn't breathing!",
599
+ "vitals": {
600
+ "hr": 0,
601
+ "bp_sys": 0,
602
+ "bp_dia": 0,
603
+ "spo2": 0,
604
+ "rr": 0,
605
+ "temp": 36.0,
606
+ "avpu": "U"
607
+ },
608
+ "history": {
609
+ "age": 55,
610
+ "gender": "male",
611
+ "relevant_pmh": "previous heart attack",
612
+ "time_course": "Collapsed 2 minutes ago"
613
+ }
614
+ },
615
+ {
616
+ "chief_complaint": "He cut himself badly and is losing a lot of blood!",
617
+ "vitals": {
618
+ "hr": 125,
619
+ "bp_sys": 80,
620
+ "bp_dia": 50,
621
+ "spo2": 95,
622
+ "rr": 28,
623
+ "temp": 35.7,
624
+ "avpu": "A"
625
+ },
626
+ "history": {
627
+ "age": 40,
628
+ "gender": "male",
629
+ "relevant_pmh": "none",
630
+ "time_course": "Accident 10 minutes ago"
631
+ }
632
+ },
633
+ {
634
+ "chief_complaint": "He's been vomiting and now he's not waking up!",
635
+ "vitals": {
636
+ "hr": 110,
637
+ "bp_sys": 90,
638
+ "bp_dia": 60,
639
+ "spo2": 94,
640
+ "rr": 30,
641
+ "temp": 37.0,
642
+ "avpu": "U"
643
+ },
644
+ "history": {
645
+ "age": 25,
646
+ "gender": "male",
647
+ "relevant_pmh": "Type 1 diabetes",
648
+ "time_course": "Vomiting began 8 hours ago"
649
+ }
650
+ },
651
+ {
652
+ "chief_complaint": "He's choking and can't breathe after the car crash!",
653
+ "vitals": {
654
+ "hr": 150,
655
+ "bp_sys": 90,
656
+ "bp_dia": 60,
657
+ "spo2": 82,
658
+ "rr": 45,
659
+ "temp": 36.5,
660
+ "avpu": "V"
661
+ },
662
+ "history": {
663
+ "age": 35,
664
+ "gender": "male",
665
+ "relevant_pmh": "none",
666
+ "time_course": "Accident 5 minutes ago"
667
+ }
668
+ },
669
+ {
670
+ "chief_complaint": "He's been shivering and now he won't wake up!",
671
+ "vitals": {
672
+ "hr": 145,
673
+ "bp_sys": 70,
674
+ "bp_dia": 40,
675
+ "spo2": 90,
676
+ "rr": 20,
677
+ "temp": 40.0,
678
+ "avpu": "U"
679
+ },
680
+ "history": {
681
+ "age": 72,
682
+ "gender": "male",
683
+ "relevant_pmh": "chronic kidney disease",
684
+ "time_course": "Fever for a day, unresponsive now"
685
+ }
686
+ },
687
+ {
688
+ "chief_complaint": "He's struggling to breathe, and his lips are turning blue!",
689
+ "vitals": {
690
+ "hr": 140,
691
+ "bp_sys": 95,
692
+ "bp_dia": 55,
693
+ "spo2": 78,
694
+ "rr": 34,
695
+ "temp": 37.8,
696
+ "avpu": "V"
697
+ },
698
+ "history": {
699
+ "age": 60,
700
+ "gender": "male",
701
+ "relevant_pmh": "COPD",
702
+ "time_course": "Breathing difficulty started 1 hour ago"
703
+ }
704
+ },
705
+ {
706
+ "chief_complaint": "She passed out after having a seizure in the water!",
707
+ "vitals": {
708
+ "hr": 130,
709
+ "bp_sys": 85,
710
+ "bp_dia": 50,
711
+ "spo2": 89,
712
+ "rr": 26,
713
+ "temp": 33.0,
714
+ "avpu": "P"
715
+ },
716
+ "history": {
717
+ "age": 45,
718
+ "gender": "female",
719
+ "relevant_pmh": "epilepsy",
720
+ "time_course": "Seizure 10 minutes ago, fell into pool"
721
+ }
722
+ },
723
+ {
724
+ "chief_complaint": "He's not breathing and is turning blue!",
725
+ "vitals": {
726
+ "hr": 120,
727
+ "bp_sys": 85,
728
+ "bp_dia": 50,
729
+ "spo2": 85,
730
+ "rr": 8,
731
+ "temp": 36.5,
732
+ "avpu": "U"
733
+ },
734
+ "history": {
735
+ "age": 60,
736
+ "gender": "male",
737
+ "relevant_PMH": "history of COPD and smoking",
738
+ "time_course": "Found unconscious at home with sudden respiratory distress."
739
+ }
740
+ },
741
+ {
742
+ "chief_complaint": "He's collapsed and he's not waking up!",
743
+ "vitals": {
744
+ "hr": 45,
745
+ "bp_sys": 60,
746
+ "bp_dia": 40,
747
+ "spo2": 70,
748
+ "rr": 4,
749
+ "temp": 35.0,
750
+ "avpu": "U"
751
+ },
752
+ "history": {
753
+ "age": 28,
754
+ "gender": "male",
755
+ "relevant_PMH": "history of depression, possible overdose",
756
+ "time_course": "Found unresponsive by his partner after taking pills."
757
+ }
758
+ },
759
+ {
760
+ "chief_complaint": "She keeps having seizures and isn't waking up!",
761
+ "vitals": {
762
+ "hr": 130,
763
+ "bp_sys": 90,
764
+ "bp_dia": 50,
765
+ "spo2": 92,
766
+ "rr": 20,
767
+ "temp": 37.8,
768
+ "avpu": "P"
769
+ },
770
+ "history": {
771
+ "age": 35,
772
+ "gender": "female",
773
+ "relevant_PMH": "epilepsy",
774
+ "time_course": "Has had 5 continuous seizures lasting over 30 minutes."
775
+ }
776
+ },
777
+ {
778
+ "chief_complaint": "He's bleeding everywhere and can't stand up!",
779
+ "vitals": {
780
+ "hr": 140,
781
+ "bp_sys": 70,
782
+ "bp_dia": 40,
783
+ "spo2": 94,
784
+ "rr": 22,
785
+ "temp": 36.2,
786
+ "avpu": "A"
787
+ },
788
+ "history": {
789
+ "age": 50,
790
+ "gender": "male",
791
+ "relevant_PMH": "alcohol use disorder",
792
+ "time_course": "Sudden onset of massive vomiting of blood at home."
793
+ }
794
+ },
795
+ {
796
+ "chief_complaint": "He's not breathing. He was underwater for a long time!",
797
+ "vitals": {
798
+ "hr": 50,
799
+ "bp_sys": 90,
800
+ "bp_dia": 60,
801
+ "spo2": 80,
802
+ "rr": 0,
803
+ "temp": 34.5,
804
+ "avpu": "U"
805
+ },
806
+ "history": {
807
+ "age": 14,
808
+ "gender": "male",
809
+ "relevant_PMH": "asthma",
810
+ "time_course": "Pulled from a swimming pool after several minutes underwater."
811
+ }
812
+ },
813
+ {
814
+ "chief_complaint": "Her whole body is swollen, and she can't breathe!",
815
+ "vitals": {
816
+ "hr": 150,
817
+ "bp_sys": 80,
818
+ "bp_dia": 50,
819
+ "spo2": 85,
820
+ "rr": 30,
821
+ "temp": 37.0,
822
+ "avpu": "A"
823
+ },
824
+ "history": {
825
+ "age": 22,
826
+ "gender": "female",
827
+ "relevant_PMH": "known peanut allergy",
828
+ "time_course": "Ate dessert containing peanuts, severe reaction in minutes."
829
+ }
830
+ },
831
+ {
832
+ "chief_complaint": "He's unconscious and his skin looks blotchy!",
833
+ "vitals": {
834
+ "hr": 145,
835
+ "bp_sys": 75,
836
+ "bp_dia": 40,
837
+ "spo2": 88,
838
+ "rr": 28,
839
+ "temp": 40.0,
840
+ "avpu": "U"
841
+ },
842
+ "history": {
843
+ "age": 3,
844
+ "gender": "male",
845
+ "relevant_PMH": "none",
846
+ "time_course": "Suddenly developed a fever, rash, and became unresponsive."
847
+ }
848
+ },
849
+ {
850
+ "chief_complaint": "She's unconscious and breathing fast!",
851
+ "vitals": {
852
+ "hr": 130,
853
+ "bp_sys": 90,
854
+ "bp_dia": 60,
855
+ "spo2": 92,
856
+ "rr": 34,
857
+ "temp": 37.2,
858
+ "avpu": "P"
859
+ },
860
+ "history": {
861
+ "age": 24,
862
+ "gender": "female",
863
+ "relevant_PMH": "type 1 diabetes",
864
+ "time_course": "Found with high blood sugar levels after missing insulin doses."
865
+ }
866
+ },
867
+ {
868
+ "chief_complaint": "She's fainted and keeps shaking!",
869
+ "vitals": {
870
+ "hr": 140,
871
+ "bp_sys": 170,
872
+ "bp_dia": 110,
873
+ "spo2": 95,
874
+ "rr": 25,
875
+ "temp": 37.5,
876
+ "avpu": "P"
877
+ },
878
+ "history": {
879
+ "age": 30,
880
+ "gender": "female",
881
+ "relevant_PMH": "32 weeks pregnant, history of high blood pressure",
882
+ "time_course": "Sudden onset of seizures with high blood pressure."
883
+ }
884
+ },
885
+ {
886
+ "chief_complaint": "He's not moving, hit really hard!",
887
+ "vitals": {
888
+ "hr": 110,
889
+ "bp_sys": 85,
890
+ "bp_dia": 55,
891
+ "spo2": 90,
892
+ "rr": 28,
893
+ "temp": 36.5,
894
+ "avpu": "U"
895
+ },
896
+ "history": {
897
+ "age": 45,
898
+ "gender": "male",
899
+ "relevant_PMH": "none",
900
+ "time_course": "Involved in a high-speed car accident, unresponsive at the scene."
901
+ }
902
+ }
903
+ ],
904
+ "2": [
905
+ {
906
+ "chief_complaint": "My chest really hurts, like something's squeezing it.",
907
+ "vitals": {
908
+ "hr": 122,
909
+ "bp_sys": 90,
910
+ "bp_dia": 60,
911
+ "spo2": 94,
912
+ "rr": 20,
913
+ "temp": 37.1,
914
+ "avpu": "A"
915
+ },
916
+ "history": {
917
+ "age": 64,
918
+ "gender": "male",
919
+ "relevant_PMH": "hypertension, smoker",
920
+ "time_course": "30 minutes"
921
+ }
922
+ },
923
+ {
924
+ "chief_complaint": "I suddenly have the worst headache of my life.",
925
+ "vitals": {
926
+ "hr": 88,
927
+ "bp_sys": 180,
928
+ "bp_dia": 110,
929
+ "spo2": 97,
930
+ "rr": 18,
931
+ "temp": 36.8,
932
+ "avpu": "A"
933
+ },
934
+ "history": {
935
+ "age": 55,
936
+ "gender": "female",
937
+ "relevant_PMH": "migraines",
938
+ "time_course": "15 minutes"
939
+ }
940
+ },
941
+ {
942
+ "chief_complaint": "I feel really confused and my left side feels weird and weak.",
943
+ "vitals": {
944
+ "hr": 76,
945
+ "bp_sys": 150,
946
+ "bp_dia": 90,
947
+ "spo2": 96,
948
+ "rr": 16,
949
+ "temp": 36.7,
950
+ "avpu": "V"
951
+ },
952
+ "history": {
953
+ "age": 72,
954
+ "gender": "male",
955
+ "relevant_PMH": "atrial fibrillation",
956
+ "time_course": "1 hour"
957
+ }
958
+ },
959
+ {
960
+ "chief_complaint": "My toddler is burning up and won't respond properly.",
961
+ "vitals": {
962
+ "hr": 160,
963
+ "bp_sys": 70,
964
+ "bp_dia": 40,
965
+ "spo2": 92,
966
+ "rr": 30,
967
+ "temp": 40.2,
968
+ "avpu": "V"
969
+ },
970
+ "history": {
971
+ "age": 2,
972
+ "gender": "female",
973
+ "relevant_PMH": "none",
974
+ "time_course": "3 hours"
975
+ }
976
+ },
977
+ {
978
+ "chief_complaint": "I fell off a ladder and now my back really hurts.",
979
+ "vitals": {
980
+ "hr": 105,
981
+ "bp_sys": 100,
982
+ "bp_dia": 65,
983
+ "spo2": 95,
984
+ "rr": 22,
985
+ "temp": 36.5,
986
+ "avpu": "A"
987
+ },
988
+ "history": {
989
+ "age": 47,
990
+ "gender": "male",
991
+ "relevant_PMH": "none",
992
+ "time_course": "30 minutes"
993
+ }
994
+ },
995
+ {
996
+ "chief_complaint": "I'm in agony, my stomach is killing me!",
997
+ "vitals": {
998
+ "hr": 115,
999
+ "bp_sys": 85,
1000
+ "bp_dia": 55,
1001
+ "spo2": 93,
1002
+ "rr": 24,
1003
+ "temp": 37.2,
1004
+ "avpu": "A"
1005
+ },
1006
+ "history": {
1007
+ "age": 58,
1008
+ "gender": "female",
1009
+ "relevant_PMH": "gallstones",
1010
+ "time_course": "2 hours"
1011
+ }
1012
+ },
1013
+ {
1014
+ "chief_complaint": "I can't feel my legs after diving into the shallow end.",
1015
+ "vitals": {
1016
+ "hr": 88,
1017
+ "bp_sys": 140,
1018
+ "bp_dia": 85,
1019
+ "spo2": 98,
1020
+ "rr": 18,
1021
+ "temp": 36.9,
1022
+ "avpu": "A"
1023
+ },
1024
+ "history": {
1025
+ "age": 23,
1026
+ "gender": "non-binary",
1027
+ "relevant_PMH": "none",
1028
+ "time_course": "10 minutes"
1029
+ }
1030
+ },
1031
+ {
1032
+ "chief_complaint": "He was shaking all over, but it just stopped.",
1033
+ "vitals": {
1034
+ "hr": 130,
1035
+ "bp_sys": 98,
1036
+ "bp_dia": 65,
1037
+ "spo2": 90,
1038
+ "rr": 24,
1039
+ "temp": 38.0,
1040
+ "avpu": "P"
1041
+ },
1042
+ "history": {
1043
+ "age": 9,
1044
+ "gender": "male",
1045
+ "relevant_PMH": "epilepsy",
1046
+ "time_course": "5 minutes"
1047
+ }
1048
+ },
1049
+ {
1050
+ "chief_complaint": "I'm so dizzy, I can't stand up without falling.",
1051
+ "vitals": {
1052
+ "hr": 102,
1053
+ "bp_sys": 95,
1054
+ "bp_dia": 60,
1055
+ "spo2": 93,
1056
+ "rr": 18,
1057
+ "temp": 37.0,
1058
+ "avpu": "A"
1059
+ },
1060
+ "history": {
1061
+ "age": 42,
1062
+ "gender": "female",
1063
+ "relevant_PMH": "diabetes",
1064
+ "time_course": "1 hour"
1065
+ }
1066
+ },
1067
+ {
1068
+ "chief_complaint": "I have this sharp pain in my chest and I feel breathless.",
1069
+ "vitals": {
1070
+ "hr": 130,
1071
+ "bp_sys": 85,
1072
+ "bp_dia": 55,
1073
+ "spo2": 91,
1074
+ "rr": 28,
1075
+ "temp": 37.5,
1076
+ "avpu": "A"
1077
+ },
1078
+ "history": {
1079
+ "age": 36,
1080
+ "gender": "transgender female",
1081
+ "relevant_PMH": "asthma",
1082
+ "time_course": "2 hours"
1083
+ }
1084
+ },
1085
+ {
1086
+ "chief_complaint": "My chest is killing me. It started suddenly when I was watching TV.",
1087
+ "vitals": {
1088
+ "hr": 110,
1089
+ "bp_sys": 90,
1090
+ "bp_dia": 60,
1091
+ "spo2": 94,
1092
+ "rr": 28,
1093
+ "temp": 36.5,
1094
+ "avpu": "A"
1095
+ },
1096
+ "history": {
1097
+ "age": 67,
1098
+ "gender": "male",
1099
+ "relevant_PMhistory": "hypertension, smoker",
1100
+ "time_course": "30 minutes ago"
1101
+ }
1102
+ },
1103
+ {
1104
+ "chief_complaint": "I feel like I've been hit with the worst headache ever, like a thunderclap in my head.",
1105
+ "vitals": {
1106
+ "hr": 100,
1107
+ "bp_sys": 160,
1108
+ "bp_dia": 100,
1109
+ "spo2": 98,
1110
+ "rr": 22,
1111
+ "temp": 36.8,
1112
+ "avpu": "A"
1113
+ },
1114
+ "history": {
1115
+ "age": 54,
1116
+ "gender": "female",
1117
+ "relevant_PMhistory": "migraine sufferer",
1118
+ "time_course": "15 minutes ago"
1119
+ }
1120
+ },
1121
+ {
1122
+ "chief_complaint": "I suddenly can't move my right arm and my face feels funny.",
1123
+ "vitals": {
1124
+ "hr": 88,
1125
+ "bp_sys": 170,
1126
+ "bp_dia": 110,
1127
+ "spo2": 95,
1128
+ "rr": 18,
1129
+ "temp": 36.7,
1130
+ "avpu": "A"
1131
+ },
1132
+ "history": {
1133
+ "age": 59,
1134
+ "gender": "female",
1135
+ "relevant_PMhistory": "type 2 diabetes",
1136
+ "time_course": "1 hour ago"
1137
+ }
1138
+ },
1139
+ {
1140
+ "chief_complaint": "My baby is burning up and really drowsy. It's scaring me.",
1141
+ "vitals": {
1142
+ "hr": 180,
1143
+ "bp_sys": 80,
1144
+ "bp_dia": 50,
1145
+ "spo2": 92,
1146
+ "rr": 40,
1147
+ "temp": 40.2,
1148
+ "avpu": "P"
1149
+ },
1150
+ "history": {
1151
+ "age": 2,
1152
+ "gender": "male",
1153
+ "relevant_PMhistory": "none",
1154
+ "time_course": "fever for 2 days, worsened in the last hour"
1155
+ }
1156
+ },
1157
+ {
1158
+ "chief_complaint": "I fell down the stairs and can't feel my legs.",
1159
+ "vitals": {
1160
+ "hr": 95,
1161
+ "bp_sys": 108,
1162
+ "bp_dia": 72,
1163
+ "spo2": 97,
1164
+ "rr": 20,
1165
+ "temp": 37.0,
1166
+ "avpu": "A"
1167
+ },
1168
+ "history": {
1169
+ "age": 35,
1170
+ "gender": "male",
1171
+ "relevant_PMhistory": "none",
1172
+ "time_course": "30 minutes ago"
1173
+ }
1174
+ },
1175
+ {
1176
+ "chief_complaint": "I'm in agony from my stomach. It feels like something's gonna burst.",
1177
+ "vitals": {
1178
+ "hr": 120,
1179
+ "bp_sys": 85,
1180
+ "bp_dia": 55,
1181
+ "spo2": 96,
1182
+ "rr": 24,
1183
+ "temp": 38.1,
1184
+ "avpu": "A"
1185
+ },
1186
+ "history": {
1187
+ "age": 40,
1188
+ "gender": "female",
1189
+ "relevant_PMhistory": "prior appendectomy",
1190
+ "time_course": "started 2 hours ago, worsening"
1191
+ }
1192
+ },
1193
+ {
1194
+ "chief_complaint": "I had a seizure and now I'm confused and weak on one side.",
1195
+ "vitals": {
1196
+ "hr": 102,
1197
+ "bp_sys": 140,
1198
+ "bp_dia": 85,
1199
+ "spo2": 98,
1200
+ "rr": 22,
1201
+ "temp": 37.5,
1202
+ "avpu": "V"
1203
+ },
1204
+ "history": {
1205
+ "age": 28,
1206
+ "gender": "female",
1207
+ "relevant_PMhistory": "epilepsy",
1208
+ "time_course": "seizure 20 minutes ago"
1209
+ }
1210
+ },
1211
+ {
1212
+ "chief_complaint": "My son was hit by a car. He's awake but not responding well.",
1213
+ "vitals": {
1214
+ "hr": 112,
1215
+ "bp_sys": 105,
1216
+ "bp_dia": 70,
1217
+ "spo2": 93,
1218
+ "rr": 25,
1219
+ "temp": 36.6,
1220
+ "avpu": "V"
1221
+ },
1222
+ "history": {
1223
+ "age": 12,
1224
+ "gender": "male",
1225
+ "relevant_PMhistory": "none",
1226
+ "time_course": "accident 15 minutes ago"
1227
+ }
1228
+ },
1229
+ {
1230
+ "chief_complaint": "I've got a splitting headache and can't stop vomiting.",
1231
+ "vitals": {
1232
+ "hr": 110,
1233
+ "bp_sys": 150,
1234
+ "bp_dia": 95,
1235
+ "spo2": 97,
1236
+ "rr": 20,
1237
+ "temp": 36.9,
1238
+ "avpu": "A"
1239
+ },
1240
+ "history": {
1241
+ "age": 45,
1242
+ "gender": "male",
1243
+ "relevant_PMhistory": "high blood pressure",
1244
+ "time_course": "started suddenly 1 hour ago"
1245
+ }
1246
+ },
1247
+ {
1248
+ "chief_complaint": "My whole back feels like it's on fire after lifting a heavy box.",
1249
+ "vitals": {
1250
+ "hr": 100,
1251
+ "bp_sys": 132,
1252
+ "bp_dia": 85,
1253
+ "spo2": 96,
1254
+ "rr": 18,
1255
+ "temp": 37.2,
1256
+ "avpu": "A"
1257
+ },
1258
+ "history": {
1259
+ "age": 50,
1260
+ "gender": "female",
1261
+ "relevant_PMhistory": "chronic lower back pain",
1262
+ "time_course": "1 hour ago"
1263
+ }
1264
+ },
1265
+ {
1266
+ "chief_complaint": "It feels like an elephant is sitting on my chest!",
1267
+ "vitals": {
1268
+ "hr": 112,
1269
+ "bp_sys": 92,
1270
+ "bp_dia": 60,
1271
+ "spo2": 89,
1272
+ "rr": 28,
1273
+ "temp": 36.7,
1274
+ "avpu": "A"
1275
+ },
1276
+ "history": {
1277
+ "age": 54,
1278
+ "gender": "male",
1279
+ "relevant_PMH": "hypertension, smoker",
1280
+ "time_course": "30 minutes ago"
1281
+ }
1282
+ },
1283
+ {
1284
+ "chief_complaint": "My head... it's like a bolt of lightning hit me!",
1285
+ "vitals": {
1286
+ "hr": 84,
1287
+ "bp_sys": 145,
1288
+ "bp_dia": 88,
1289
+ "spo2": 97,
1290
+ "rr": 18,
1291
+ "temp": 37.0,
1292
+ "avpu": "A"
1293
+ },
1294
+ "history": {
1295
+ "age": 29,
1296
+ "gender": "female",
1297
+ "relevant_PMH": "migraine",
1298
+ "time_course": "10 minutes ago"
1299
+ }
1300
+ },
1301
+ {
1302
+ "chief_complaint": "I can't feel my right arm and leg.",
1303
+ "vitals": {
1304
+ "hr": 78,
1305
+ "bp_sys": 150,
1306
+ "bp_dia": 95,
1307
+ "spo2": 96,
1308
+ "rr": 20,
1309
+ "temp": 36.5,
1310
+ "avpu": "A"
1311
+ },
1312
+ "history": {
1313
+ "age": 67,
1314
+ "gender": "male",
1315
+ "relevant_PMH": "diabetes, previous TIA",
1316
+ "time_course": "started 1 hour ago"
1317
+ }
1318
+ },
1319
+ {
1320
+ "chief_complaint": "My son is so hot and won't stop crying.",
1321
+ "vitals": {
1322
+ "hr": 140,
1323
+ "bp_sys": 100,
1324
+ "bp_dia": 65,
1325
+ "spo2": 94,
1326
+ "rr": 40,
1327
+ "temp": 40.2,
1328
+ "avpu": "V"
1329
+ },
1330
+ "history": {
1331
+ "age": 2,
1332
+ "gender": "male",
1333
+ "relevant_PMH": "none",
1334
+ "time_course": "since last night"
1335
+ }
1336
+ },
1337
+ {
1338
+ "chief_complaint": "I fell off my bike and my back is killing me!",
1339
+ "vitals": {
1340
+ "hr": 110,
1341
+ "bp_sys": 105,
1342
+ "bp_dia": 70,
1343
+ "spo2": 98,
1344
+ "rr": 22,
1345
+ "temp": 37.3,
1346
+ "avpu": "A"
1347
+ },
1348
+ "history": {
1349
+ "age": 21,
1350
+ "gender": "female",
1351
+ "relevant_PMH": "none",
1352
+ "time_course": "30 minutes ago"
1353
+ }
1354
+ },
1355
+ {
1356
+ "chief_complaint": "My stomach is in agony, I can't even move!",
1357
+ "vitals": {
1358
+ "hr": 120,
1359
+ "bp_sys": 95,
1360
+ "bp_dia": 60,
1361
+ "spo2": 97,
1362
+ "rr": 22,
1363
+ "temp": 37.8,
1364
+ "avpu": "A"
1365
+ },
1366
+ "history": {
1367
+ "age": 36,
1368
+ "gender": "male",
1369
+ "relevant_PMH": "none",
1370
+ "time_course": "2 hours ago"
1371
+ }
1372
+ },
1373
+ {
1374
+ "chief_complaint": "I saw a car crash happen right in front of me!",
1375
+ "vitals": {
1376
+ "hr": 115,
1377
+ "bp_sys": 90,
1378
+ "bp_dia": 55,
1379
+ "spo2": 93,
1380
+ "rr": 30,
1381
+ "temp": 36.6,
1382
+ "avpu": "A"
1383
+ },
1384
+ "history": {
1385
+ "age": 40,
1386
+ "gender": "female",
1387
+ "relevant_PMH": "anxiety",
1388
+ "time_course": "15 minutes ago"
1389
+ }
1390
+ },
1391
+ {
1392
+ "chief_complaint": "My child is burning up and hardly responding.",
1393
+ "vitals": {
1394
+ "hr": 150,
1395
+ "bp_sys": 85,
1396
+ "bp_dia": 50,
1397
+ "spo2": 92,
1398
+ "rr": 36,
1399
+ "temp": 41.0,
1400
+ "avpu": "V"
1401
+ },
1402
+ "history": {
1403
+ "age": 5,
1404
+ "gender": "male",
1405
+ "relevant_PMH": "asthma",
1406
+ "time_course": "started this morning"
1407
+ }
1408
+ },
1409
+ {
1410
+ "chief_complaint": "I was playing football and heard a snap in my back.",
1411
+ "vitals": {
1412
+ "hr": 100,
1413
+ "bp_sys": 110,
1414
+ "bp_dia": 70,
1415
+ "spo2": 98,
1416
+ "rr": 20,
1417
+ "temp": 36.8,
1418
+ "avpu": "A"
1419
+ },
1420
+ "history": {
1421
+ "age": 16,
1422
+ "gender": "male",
1423
+ "relevant_PMH": "none",
1424
+ "time_course": "1 hour ago"
1425
+ }
1426
+ },
1427
+ {
1428
+ "chief_complaint": "I had a fit and everything went blank for a while.",
1429
+ "vitals": {
1430
+ "hr": 98,
1431
+ "bp_sys": 130,
1432
+ "bp_dia": 85,
1433
+ "spo2": 95,
1434
+ "rr": 18,
1435
+ "temp": 37.1,
1436
+ "avpu": "P"
1437
+ },
1438
+ "history": {
1439
+ "age": 46,
1440
+ "gender": "female",
1441
+ "relevant_PMH": "epilepsy",
1442
+ "time_course": "stopped about 5 minutes ago"
1443
+ }
1444
+ }
1445
+ ],
1446
+ "3": [
1447
+ {
1448
+ "chief_complaint": "I can't catch my breath, it's been getting worse since yesterday.",
1449
+ "vitals": {
1450
+ "hr": 110,
1451
+ "bp_sys": 135,
1452
+ "bp_dia": 85,
1453
+ "spo2": 88,
1454
+ "rr": 26,
1455
+ "temp": 37.5,
1456
+ "avpu": "A"
1457
+ },
1458
+ "history": {
1459
+ "age": 67,
1460
+ "gender": "male",
1461
+ "relevant_PMH": "COPD",
1462
+ "time_course": "Symptoms worsening over 24 hours"
1463
+ }
1464
+ },
1465
+ {
1466
+ "chief_complaint": "My leg is really swollen and it hurts to walk.",
1467
+ "vitals": {
1468
+ "hr": 90,
1469
+ "bp_sys": 120,
1470
+ "bp_dia": 80,
1471
+ "spo2": 96,
1472
+ "rr": 18,
1473
+ "temp": 36.8,
1474
+ "avpu": "A"
1475
+ },
1476
+ "history": {
1477
+ "age": 54,
1478
+ "gender": "female",
1479
+ "relevant_PMH": "None",
1480
+ "time_course": "Swelling started two days ago"
1481
+ }
1482
+ },
1483
+ {
1484
+ "chief_complaint": "My back hurts so much, and I've been throwing up for hours.",
1485
+ "vitals": {
1486
+ "hr": 100,
1487
+ "bp_sys": 125,
1488
+ "bp_dia": 85,
1489
+ "spo2": 97,
1490
+ "rr": 20,
1491
+ "temp": 38.2,
1492
+ "avpu": "A"
1493
+ },
1494
+ "history": {
1495
+ "age": 45,
1496
+ "gender": "male",
1497
+ "relevant_PMH": "Kidney stones",
1498
+ "time_course": "Pain started 8 hours ago"
1499
+ }
1500
+ },
1501
+ {
1502
+ "chief_complaint": "My ankle is swollen and it hurts a lot, but I don't remember falling.",
1503
+ "vitals": {
1504
+ "hr": 88,
1505
+ "bp_sys": 115,
1506
+ "bp_dia": 75,
1507
+ "spo2": 98,
1508
+ "rr": 16,
1509
+ "temp": 37.0,
1510
+ "avpu": "A"
1511
+ },
1512
+ "history": {
1513
+ "age": 30,
1514
+ "gender": "female",
1515
+ "relevant_PMH": "None",
1516
+ "time_course": "Swelling and pain started this morning"
1517
+ }
1518
+ },
1519
+ {
1520
+ "chief_complaint": "It hurts to take a deep breath, and I feel out of breath.",
1521
+ "vitals": {
1522
+ "hr": 102,
1523
+ "bp_sys": 122,
1524
+ "bp_dia": 78,
1525
+ "spo2": 94,
1526
+ "rr": 22,
1527
+ "temp": 37.1,
1528
+ "avpu": "A"
1529
+ },
1530
+ "history": {
1531
+ "age": 40,
1532
+ "gender": "male",
1533
+ "relevant_PMH": "Asthma",
1534
+ "time_course": "Symptoms started 12 hours ago"
1535
+ }
1536
+ },
1537
+ {
1538
+ "chief_complaint": "Her leg is red and painful, she's been running a fever.",
1539
+ "vitals": {
1540
+ "hr": 95,
1541
+ "bp_sys": 115,
1542
+ "bp_dia": 75,
1543
+ "spo2": 97,
1544
+ "rr": 18,
1545
+ "temp": 38.5,
1546
+ "avpu": "A"
1547
+ },
1548
+ "history": {
1549
+ "age": 60,
1550
+ "gender": "female",
1551
+ "relevant_PMH": "Diabetes",
1552
+ "time_course": "Leg redness and fever started yesterday"
1553
+ }
1554
+ },
1555
+ {
1556
+ "chief_complaint": "I keep throwing up and I have this awful pain in my side.",
1557
+ "vitals": {
1558
+ "hr": 105,
1559
+ "bp_sys": 130,
1560
+ "bp_dia": 85,
1561
+ "spo2": 98,
1562
+ "rr": 20,
1563
+ "temp": 37.9,
1564
+ "avpu": "A"
1565
+ },
1566
+ "history": {
1567
+ "age": 32,
1568
+ "gender": "female",
1569
+ "relevant_PMH": "None",
1570
+ "time_course": "Symptoms started 10 hours ago"
1571
+ }
1572
+ },
1573
+ {
1574
+ "chief_complaint": "I'm feeling really confused, I can't remember much.",
1575
+ "vitals": {
1576
+ "hr": 82,
1577
+ "bp_sys": 130,
1578
+ "bp_dia": 80,
1579
+ "spo2": 96,
1580
+ "rr": 18,
1581
+ "temp": 37.2,
1582
+ "avpu": "V"
1583
+ },
1584
+ "history": {
1585
+ "age": 82,
1586
+ "gender": "male",
1587
+ "relevant_PMH": "Hypertension",
1588
+ "time_course": "Confusion began 3 days ago"
1589
+ }
1590
+ },
1591
+ {
1592
+ "chief_complaint": "My tummy really hurts and I have a fever.",
1593
+ "vitals": {
1594
+ "hr": 112,
1595
+ "bp_sys": 118,
1596
+ "bp_dia": 76,
1597
+ "spo2": 97,
1598
+ "rr": 22,
1599
+ "temp": 38.7,
1600
+ "avpu": "A"
1601
+ },
1602
+ "history": {
1603
+ "age": 36,
1604
+ "gender": "female",
1605
+ "relevant_PMH": "Recurrent UTIs",
1606
+ "time_course": "Pain started yesterday"
1607
+ }
1608
+ },
1609
+ {
1610
+ "chief_complaint": "The left side of my back is killing me. It hurts to move.",
1611
+ "vitals": {
1612
+ "hr": 95,
1613
+ "bp_sys": 120,
1614
+ "bp_dia": 80,
1615
+ "spo2": 99,
1616
+ "rr": 18,
1617
+ "temp": 37.0,
1618
+ "avpu": "A"
1619
+ },
1620
+ "history": {
1621
+ "age": 50,
1622
+ "gender": "female",
1623
+ "relevant_PMH": "None",
1624
+ "time_course": "Pain started suddenly 5 hours ago"
1625
+ }
1626
+ },
1627
+ {
1628
+ "chief_complaint": "My back hurts so bad, and I've been throwing up since yesterday.",
1629
+ "vitals": {
1630
+ "hr": 112,
1631
+ "bp_sys": 110,
1632
+ "bp_dia": 70,
1633
+ "spo2": 96,
1634
+ "rr": 24,
1635
+ "temp": 38.9,
1636
+ "avpu": "A"
1637
+ },
1638
+ "history": {
1639
+ "age": 32,
1640
+ "gender": "female",
1641
+ "relevant_PMH": "none",
1642
+ "time_course": "pain and vomiting started 12 hours ago"
1643
+ }
1644
+ },
1645
+ {
1646
+ "chief_complaint": "My leg is really swollen and hurts when I move.",
1647
+ "vitals": {
1648
+ "hr": 89,
1649
+ "bp_sys": 132,
1650
+ "bp_dia": 84,
1651
+ "spo2": 98,
1652
+ "rr": 17,
1653
+ "temp": 37.3,
1654
+ "avpu": "A"
1655
+ },
1656
+ "history": {
1657
+ "age": 56,
1658
+ "gender": "male",
1659
+ "relevant_PMH": "history of varicose veins",
1660
+ "time_course": "swelling started 2 days ago"
1661
+ }
1662
+ },
1663
+ {
1664
+ "chief_complaint": "I've been coughing and struggling to breathe more than usual.",
1665
+ "vitals": {
1666
+ "hr": 108,
1667
+ "bp_sys": 142,
1668
+ "bp_dia": 88,
1669
+ "spo2": 92,
1670
+ "rr": 26,
1671
+ "temp": 37.8,
1672
+ "avpu": "A"
1673
+ },
1674
+ "history": {
1675
+ "age": 70,
1676
+ "gender": "female",
1677
+ "relevant_PMH": "COPD",
1678
+ "time_course": "worsening over the last 3 days"
1679
+ }
1680
+ },
1681
+ {
1682
+ "chief_complaint": "I keep forgetting things and feel really confused.",
1683
+ "vitals": {
1684
+ "hr": 95,
1685
+ "bp_sys": 130,
1686
+ "bp_dia": 75,
1687
+ "spo2": 97,
1688
+ "rr": 18,
1689
+ "temp": 36.8,
1690
+ "avpu": "A"
1691
+ },
1692
+ "history": {
1693
+ "age": 82,
1694
+ "gender": "male",
1695
+ "relevant_PMH": "hypertension, mild cognitive impairment",
1696
+ "time_course": "confusion worsened over 24 hours"
1697
+ }
1698
+ },
1699
+ {
1700
+ "chief_complaint": "My side hurts a lot, and I can't keep anything down.",
1701
+ "vitals": {
1702
+ "hr": 115,
1703
+ "bp_sys": 105,
1704
+ "bp_dia": 65,
1705
+ "spo2": 95,
1706
+ "rr": 20,
1707
+ "temp": 37.6,
1708
+ "avpu": "A"
1709
+ },
1710
+ "history": {
1711
+ "age": 45,
1712
+ "gender": "male",
1713
+ "relevant_PMH": "kidney stones",
1714
+ "time_course": "pain started suddenly 10 hours ago"
1715
+ }
1716
+ },
1717
+ {
1718
+ "chief_complaint": "My leg is hurting and red, and I'm feeling feverish.",
1719
+ "vitals": {
1720
+ "hr": 102,
1721
+ "bp_sys": 118,
1722
+ "bp_dia": 78,
1723
+ "spo2": 98,
1724
+ "rr": 18,
1725
+ "temp": 38.1,
1726
+ "avpu": "A"
1727
+ },
1728
+ "history": {
1729
+ "age": 39,
1730
+ "gender": "female",
1731
+ "relevant_PMH": "diabetes",
1732
+ "time_course": "redness and pain for 3 days"
1733
+ }
1734
+ },
1735
+ {
1736
+ "chief_complaint": "I'm wheezing a lot, and my inhaler isn't helping.",
1737
+ "vitals": {
1738
+ "hr": 120,
1739
+ "bp_sys": 126,
1740
+ "bp_dia": 82,
1741
+ "spo2": 89,
1742
+ "rr": 28,
1743
+ "temp": 37.5,
1744
+ "avpu": "A"
1745
+ },
1746
+ "history": {
1747
+ "age": 25,
1748
+ "gender": "female",
1749
+ "relevant_PMH": "asthma",
1750
+ "time_course": "symptoms worsened in last 8 hours"
1751
+ }
1752
+ },
1753
+ {
1754
+ "chief_complaint": "My back really hurts, and I'm peeing a lot.",
1755
+ "vitals": {
1756
+ "hr": 105,
1757
+ "bp_sys": 115,
1758
+ "bp_dia": 70,
1759
+ "spo2": 96,
1760
+ "rr": 20,
1761
+ "temp": 38.4,
1762
+ "avpu": "A"
1763
+ },
1764
+ "history": {
1765
+ "age": 48,
1766
+ "gender": "male",
1767
+ "relevant_PMH": "recurrent UTIs",
1768
+ "time_course": "symptoms started 24 hours ago"
1769
+ }
1770
+ },
1771
+ {
1772
+ "chief_complaint": "My ankle hurts so much, and it's hard to walk on it.",
1773
+ "vitals": {
1774
+ "hr": 98,
1775
+ "bp_sys": 132,
1776
+ "bp_dia": 84,
1777
+ "spo2": 97,
1778
+ "rr": 18,
1779
+ "temp": 37.2,
1780
+ "avpu": "A"
1781
+ },
1782
+ "history": {
1783
+ "age": 29,
1784
+ "gender": "male",
1785
+ "relevant_PMH": "none",
1786
+ "time_course": "injury happened while playing football 4 hours ago"
1787
+ }
1788
+ },
1789
+ {
1790
+ "chief_complaint": "My chest hurts when I take a deep breath.",
1791
+ "vitals": {
1792
+ "hr": 104,
1793
+ "bp_sys": 124,
1794
+ "bp_dia": 80,
1795
+ "spo2": 95,
1796
+ "rr": 22,
1797
+ "temp": 37.4,
1798
+ "avpu": "A"
1799
+ },
1800
+ "history": {
1801
+ "age": 52,
1802
+ "gender": "female",
1803
+ "relevant_PMH": "smoker",
1804
+ "time_course": "started this morning"
1805
+ }
1806
+ },
1807
+ {
1808
+ "chief_complaint": "I've been wheezing and can't catch my breath sometimes.",
1809
+ "vitals": {
1810
+ "hr": 110,
1811
+ "bp_sys": 140,
1812
+ "bp_dia": 90,
1813
+ "spo2": 91,
1814
+ "rr": 28,
1815
+ "temp": 36.5,
1816
+ "avpu": "A"
1817
+ },
1818
+ "history": {
1819
+ "age": 65,
1820
+ "gender": "male",
1821
+ "relevant_pmh": "COPD",
1822
+ "time_course": "2 days"
1823
+ }
1824
+ },
1825
+ {
1826
+ "chief_complaint": "My leg is swollen and hurts when I walk.",
1827
+ "vitals": {
1828
+ "hr": 95,
1829
+ "bp_sys": 125,
1830
+ "bp_dia": 80,
1831
+ "spo2": 98,
1832
+ "rr": 20,
1833
+ "temp": 37.0,
1834
+ "avpu": "A"
1835
+ },
1836
+ "history": {
1837
+ "age": 45,
1838
+ "gender": "female",
1839
+ "relevant_pmh": null,
1840
+ "time_course": "3 days"
1841
+ }
1842
+ },
1843
+ {
1844
+ "chief_complaint": "My lower back hurts and I feel sick to my stomach.",
1845
+ "vitals": {
1846
+ "hr": 100,
1847
+ "bp_sys": 130,
1848
+ "bp_dia": 85,
1849
+ "spo2": 97,
1850
+ "rr": 22,
1851
+ "temp": 38.1,
1852
+ "avpu": "A"
1853
+ },
1854
+ "history": {
1855
+ "age": 34,
1856
+ "gender": "male",
1857
+ "relevant_pmh": "History of kidney stones",
1858
+ "time_course": "12 hours"
1859
+ }
1860
+ },
1861
+ {
1862
+ "chief_complaint": "I've been feeling weak and confused since last night.",
1863
+ "vitals": {
1864
+ "hr": 82,
1865
+ "bp_sys": 115,
1866
+ "bp_dia": 75,
1867
+ "spo2": 96,
1868
+ "rr": 18,
1869
+ "temp": 37.2,
1870
+ "avpu": "V"
1871
+ },
1872
+ "history": {
1873
+ "age": 78,
1874
+ "gender": "female",
1875
+ "relevant_pmh": "Hypertension",
1876
+ "time_course": "8 hours"
1877
+ }
1878
+ },
1879
+ {
1880
+ "chief_complaint": "I can't keep anything down, I keep throwing up.",
1881
+ "vitals": {
1882
+ "hr": 120,
1883
+ "bp_sys": 110,
1884
+ "bp_dia": 70,
1885
+ "spo2": 99,
1886
+ "rr": 18,
1887
+ "temp": 37.5,
1888
+ "avpu": "A"
1889
+ },
1890
+ "history": {
1891
+ "age": 22,
1892
+ "gender": "male",
1893
+ "relevant_pmh": null,
1894
+ "time_course": "24 hours"
1895
+ }
1896
+ },
1897
+ {
1898
+ "chief_complaint": "My hand is swollen and red, it's really sore.",
1899
+ "vitals": {
1900
+ "hr": 88,
1901
+ "bp_sys": 120,
1902
+ "bp_dia": 80,
1903
+ "spo2": 98,
1904
+ "rr": 16,
1905
+ "temp": 37.9,
1906
+ "avpu": "A"
1907
+ },
1908
+ "history": {
1909
+ "age": 55,
1910
+ "gender": "female",
1911
+ "relevant_pmh": "Diabetes",
1912
+ "time_course": "2 days"
1913
+ }
1914
+ },
1915
+ {
1916
+ "chief_complaint": "It hurts on the side when I breathe in.",
1917
+ "vitals": {
1918
+ "hr": 105,
1919
+ "bp_sys": 130,
1920
+ "bp_dia": 85,
1921
+ "spo2": 94,
1922
+ "rr": 24,
1923
+ "temp": 37.8,
1924
+ "avpu": "A"
1925
+ },
1926
+ "history": {
1927
+ "age": 48,
1928
+ "gender": "male",
1929
+ "relevant_pmh": "Smoker",
1930
+ "time_course": "1 day"
1931
+ }
1932
+ },
1933
+ {
1934
+ "chief_complaint": "My urine is super cloudy and my back hurts a lot.",
1935
+ "vitals": {
1936
+ "hr": 115,
1937
+ "bp_sys": 125,
1938
+ "bp_dia": 78,
1939
+ "spo2": 97,
1940
+ "rr": 20,
1941
+ "temp": 38.0,
1942
+ "avpu": "A"
1943
+ },
1944
+ "history": {
1945
+ "age": 32,
1946
+ "gender": "female",
1947
+ "relevant_pmh": "Frequent UTIs",
1948
+ "time_course": "18 hours"
1949
+ }
1950
+ },
1951
+ {
1952
+ "chief_complaint": "I fell and now my arm hurts bad, it's swollen too.",
1953
+ "vitals": {
1954
+ "hr": 90,
1955
+ "bp_sys": 135,
1956
+ "bp_dia": 88,
1957
+ "spo2": 99,
1958
+ "rr": 18,
1959
+ "temp": 36.8,
1960
+ "avpu": "A"
1961
+ },
1962
+ "history": {
1963
+ "age": 29,
1964
+ "gender": "female",
1965
+ "relevant_pmh": null,
1966
+ "time_course": "3 hours"
1967
+ }
1968
+ },
1969
+ {
1970
+ "chief_complaint": "I'm having a hard time breathing and my chest feels tight.",
1971
+ "vitals": {
1972
+ "hr": 130,
1973
+ "bp_sys": 140,
1974
+ "bp_dia": 92,
1975
+ "spo2": 92,
1976
+ "rr": 30,
1977
+ "temp": 36.7,
1978
+ "avpu": "A"
1979
+ },
1980
+ "history": {
1981
+ "age": 18,
1982
+ "gender": "male",
1983
+ "relevant_pmh": "Asthma",
1984
+ "time_course": "6 hours"
1985
+ }
1986
+ }
1987
+ ],
1988
+ "4": [
1989
+ {
1990
+ "chief_complaint": "My wrist's been hurting since yesterday after I tripped. It's not too bad, but still hurts.",
1991
+ "vitals": {
1992
+ "hr": 82,
1993
+ "bp_sys": 128,
1994
+ "bp_dia": 75,
1995
+ "spo2": 98,
1996
+ "rr": 16,
1997
+ "temp": 36.8,
1998
+ "avpu": "A"
1999
+ },
2000
+ "history": {
2001
+ "age": 29,
2002
+ "gender": "female",
2003
+ "relevant PMH": "none",
2004
+ "time course": "pain started 24 hours ago after fall"
2005
+ }
2006
+ },
2007
+ {
2008
+ "chief_complaint": "I've got this itchy rash on my ankle after being outdoors. It's been a day.",
2009
+ "vitals": {
2010
+ "hr": 75,
2011
+ "bp_sys": 120,
2012
+ "bp_dia": 80,
2013
+ "spo2": 99,
2014
+ "rr": 15,
2015
+ "temp": 37.0,
2016
+ "avpu": "A"
2017
+ },
2018
+ "history": {
2019
+ "age": 41,
2020
+ "gender": "male",
2021
+ "relevant PMH": "seasonal allergies",
2022
+ "time course": "itchy rash noticed 24 hours ago"
2023
+ }
2024
+ },
2025
+ {
2026
+ "chief_complaint": "I've been peeing a lot more than usual and it kind of stings. It's been happening for two days.",
2027
+ "vitals": {
2028
+ "hr": 78,
2029
+ "bp_sys": 110,
2030
+ "bp_dia": 70,
2031
+ "spo2": 98,
2032
+ "rr": 18,
2033
+ "temp": 37.5,
2034
+ "avpu": "A"
2035
+ },
2036
+ "history": {
2037
+ "age": 52,
2038
+ "gender": "female",
2039
+ "relevant PMH": "recurrent UTIs",
2040
+ "time course": "symptoms started 2 days ago"
2041
+ }
2042
+ },
2043
+ {
2044
+ "chief_complaint": "My ear's been aching and it's hard to hear out of it since yesterday morning.",
2045
+ "vitals": {
2046
+ "hr": 88,
2047
+ "bp_sys": 125,
2048
+ "bp_dia": 78,
2049
+ "spo2": 99,
2050
+ "rr": 17,
2051
+ "temp": 37.3,
2052
+ "avpu": "A"
2053
+ },
2054
+ "history": {
2055
+ "age": 6,
2056
+ "gender": "male",
2057
+ "relevant PMH": "frequent ear infections",
2058
+ "time course": "symptoms started yesterday morning"
2059
+ }
2060
+ },
2061
+ {
2062
+ "chief_complaint": "I have this weird bump on my eyelid for the last three days. It's kind of irritated.",
2063
+ "vitals": {
2064
+ "hr": 70,
2065
+ "bp_sys": 115,
2066
+ "bp_dia": 72,
2067
+ "spo2": 98,
2068
+ "rr": 16,
2069
+ "temp": 36.9,
2070
+ "avpu": "A"
2071
+ },
2072
+ "history": {
2073
+ "age": 24,
2074
+ "gender": "female",
2075
+ "relevant PMH": "none",
2076
+ "time course": "noticed the bump 3 days ago"
2077
+ }
2078
+ },
2079
+ {
2080
+ "chief_complaint": "The back of my hand got hit during a game and it's been bruised and sore for two days now.",
2081
+ "vitals": {
2082
+ "hr": 83,
2083
+ "bp_sys": 130,
2084
+ "bp_dia": 80,
2085
+ "spo2": 99,
2086
+ "rr": 15,
2087
+ "temp": 36.7,
2088
+ "avpu": "A"
2089
+ },
2090
+ "history": {
2091
+ "age": 34,
2092
+ "gender": "male",
2093
+ "relevant PMH": "none",
2094
+ "time course": "injury occurred 2 days ago"
2095
+ }
2096
+ },
2097
+ {
2098
+ "chief_complaint": "My daughter's had this cough and a bit of a fever for the past day. She's still playing, though.",
2099
+ "vitals": {
2100
+ "hr": 95,
2101
+ "bp_sys": 105,
2102
+ "bp_dia": 65,
2103
+ "spo2": 97,
2104
+ "rr": 20,
2105
+ "temp": 38.2,
2106
+ "avpu": "A"
2107
+ },
2108
+ "history": {
2109
+ "age": 3,
2110
+ "gender": "female",
2111
+ "relevant PMH": "none",
2112
+ "time course": "symptoms began 24 hours ago"
2113
+ }
2114
+ },
2115
+ {
2116
+ "chief_complaint": "I got a small cut on my finger two days ago, and it's a little red and sore now.",
2117
+ "vitals": {
2118
+ "hr": 76,
2119
+ "bp_sys": 118,
2120
+ "bp_dia": 74,
2121
+ "spo2": 98,
2122
+ "rr": 16,
2123
+ "temp": 37.1,
2124
+ "avpu": "A"
2125
+ },
2126
+ "history": {
2127
+ "age": 47,
2128
+ "gender": "male",
2129
+ "relevant PMH": "type 2 diabetes",
2130
+ "time course": "injury occurred 2 days ago"
2131
+ }
2132
+ },
2133
+ {
2134
+ "chief_complaint": "I've had a headache that's lingered for a few days now. It's not super painful, but annoying.",
2135
+ "vitals": {
2136
+ "hr": 73,
2137
+ "bp_sys": 115,
2138
+ "bp_dia": 78,
2139
+ "spo2": 99,
2140
+ "rr": 14,
2141
+ "temp": 36.6,
2142
+ "avpu": "A"
2143
+ },
2144
+ "history": {
2145
+ "age": 58,
2146
+ "gender": "female",
2147
+ "relevant PMH": "chronic migraines",
2148
+ "time course": "headache ongoing for 3 days"
2149
+ }
2150
+ },
2151
+ {
2152
+ "chief_complaint": "I sprained my ankle a couple of days ago, and it's swollen and a bit stiff.",
2153
+ "vitals": {
2154
+ "hr": 79,
2155
+ "bp_sys": 122,
2156
+ "bp_dia": 76,
2157
+ "spo2": 98,
2158
+ "rr": 16,
2159
+ "temp": 37.0,
2160
+ "avpu": "A"
2161
+ },
2162
+ "history": {
2163
+ "age": 22,
2164
+ "gender": "non-binary",
2165
+ "relevant PMH": "none",
2166
+ "time course": "injury occurred 2 days ago"
2167
+ }
2168
+ },
2169
+ {
2170
+ "chief_complaint": "My son has been complaining of ear pain since yesterday.",
2171
+ "vitals": {
2172
+ "hr": 86,
2173
+ "bp_sys": 108,
2174
+ "bp_dia": 72,
2175
+ "spo2": 98,
2176
+ "rr": 18,
2177
+ "temp": 37.6,
2178
+ "avpu": "A"
2179
+ },
2180
+ "history": {
2181
+ "age": 5,
2182
+ "gender": "male",
2183
+ "relevant_PMH": "none",
2184
+ "time_course": "24 hours"
2185
+ }
2186
+ },
2187
+ {
2188
+ "chief_complaint": "I've had this low back ache for two days, especially when I bend over.",
2189
+ "vitals": {
2190
+ "hr": 78,
2191
+ "bp_sys": 124,
2192
+ "bp_dia": 80,
2193
+ "spo2": 99,
2194
+ "rr": 16,
2195
+ "temp": 36.8,
2196
+ "avpu": "A"
2197
+ },
2198
+ "history": {
2199
+ "age": 34,
2200
+ "gender": "female",
2201
+ "relevant_PMH": "none",
2202
+ "time_course": "2 days"
2203
+ }
2204
+ },
2205
+ {
2206
+ "chief_complaint": "I tripped and scraped my knee, it's pretty sore and red now.",
2207
+ "vitals": {
2208
+ "hr": 92,
2209
+ "bp_sys": 118,
2210
+ "bp_dia": 78,
2211
+ "spo2": 97,
2212
+ "rr": 17,
2213
+ "temp": 37.2,
2214
+ "avpu": "A"
2215
+ },
2216
+ "history": {
2217
+ "age": 28,
2218
+ "gender": "non-binary",
2219
+ "relevant_PMH": "none",
2220
+ "time_course": "12 hours"
2221
+ }
2222
+ },
2223
+ {
2224
+ "chief_complaint": "I have a sore throat and it's been feeling swollen since last night.",
2225
+ "vitals": {
2226
+ "hr": 80,
2227
+ "bp_sys": 120,
2228
+ "bp_dia": 78,
2229
+ "spo2": 98,
2230
+ "rr": 18,
2231
+ "temp": 38.0,
2232
+ "avpu": "A"
2233
+ },
2234
+ "history": {
2235
+ "age": 19,
2236
+ "gender": "female",
2237
+ "relevant_PMH": "tonsillitis",
2238
+ "time_course": "1 night"
2239
+ }
2240
+ },
2241
+ {
2242
+ "chief_complaint": "I got stung by a bee and my arm is swollen but I feel fine.",
2243
+ "vitals": {
2244
+ "hr": 84,
2245
+ "bp_sys": 122,
2246
+ "bp_dia": 76,
2247
+ "spo2": 99,
2248
+ "rr": 16,
2249
+ "temp": 36.9,
2250
+ "avpu": "A"
2251
+ },
2252
+ "history": {
2253
+ "age": 42,
2254
+ "gender": "male",
2255
+ "relevant_PMH": "none",
2256
+ "time_course": "6 hours"
2257
+ }
2258
+ },
2259
+ {
2260
+ "chief_complaint": "I've had this burning when I pee for two days, it's getting annoying.",
2261
+ "vitals": {
2262
+ "hr": 88,
2263
+ "bp_sys": 118,
2264
+ "bp_dia": 74,
2265
+ "spo2": 98,
2266
+ "rr": 17,
2267
+ "temp": 37.1,
2268
+ "avpu": "A"
2269
+ },
2270
+ "history": {
2271
+ "age": 37,
2272
+ "gender": "female",
2273
+ "relevant_PMH": "recurrent UTIs",
2274
+ "time_course": "2 days"
2275
+ }
2276
+ },
2277
+ {
2278
+ "chief_complaint": "My ankle's been hurting since I twisted it yesterday but I can walk.",
2279
+ "vitals": {
2280
+ "hr": 82,
2281
+ "bp_sys": 126,
2282
+ "bp_dia": 82,
2283
+ "spo2": 98,
2284
+ "rr": 16,
2285
+ "temp": 36.7,
2286
+ "avpu": "A"
2287
+ },
2288
+ "history": {
2289
+ "age": 29,
2290
+ "gender": "male",
2291
+ "relevant_PMH": "none",
2292
+ "time_course": "1 day"
2293
+ }
2294
+ },
2295
+ {
2296
+ "chief_complaint": "I think I have a cold, my nose is runny and I feel a bit tired.",
2297
+ "vitals": {
2298
+ "hr": 74,
2299
+ "bp_sys": 115,
2300
+ "bp_dia": 76,
2301
+ "spo2": 99,
2302
+ "rr": 18,
2303
+ "temp": 37.4,
2304
+ "avpu": "A"
2305
+ },
2306
+ "history": {
2307
+ "age": 25,
2308
+ "gender": "female",
2309
+ "relevant_PMH": "none",
2310
+ "time_course": "3 days"
2311
+ }
2312
+ },
2313
+ {
2314
+ "chief_complaint": "I have a headache and my sinus feels stuffed up since this morning.",
2315
+ "vitals": {
2316
+ "hr": 76,
2317
+ "bp_sys": 122,
2318
+ "bp_dia": 80,
2319
+ "spo2": 99,
2320
+ "rr": 16,
2321
+ "temp": 37.2,
2322
+ "avpu": "A"
2323
+ },
2324
+ "history": {
2325
+ "age": 45,
2326
+ "gender": "male",
2327
+ "relevant_PMH": "sinusitis",
2328
+ "time_course": "since morning"
2329
+ }
2330
+ },
2331
+ {
2332
+ "chief_complaint": "I've had this itchy rash on my arm for a few days, it's not going away.",
2333
+ "vitals": {
2334
+ "hr": 72,
2335
+ "bp_sys": 120,
2336
+ "bp_dia": 76,
2337
+ "spo2": 98,
2338
+ "rr": 17,
2339
+ "temp": 36.8,
2340
+ "avpu": "A"
2341
+ },
2342
+ "history": {
2343
+ "age": 32,
2344
+ "gender": "female",
2345
+ "relevant_PMH": "eczema",
2346
+ "time_course": "few days"
2347
+ }
2348
+ },
2349
+ {
2350
+ "chief_complaint": "My ankle's a bit sore, I twisted it stepping off the curb yesterday.",
2351
+ "vitals": {
2352
+ "hr": 82,
2353
+ "bp_sys": 118,
2354
+ "bp_dia": 76,
2355
+ "spo2": 98,
2356
+ "rr": 16,
2357
+ "temp": 36.8,
2358
+ "avpu": "A"
2359
+ },
2360
+ "history": {
2361
+ "age": 34,
2362
+ "gender": "female",
2363
+ "relevant_pmh": "None",
2364
+ "time_course": "Pain started 24 hours ago"
2365
+ }
2366
+ },
2367
+ {
2368
+ "chief_complaint": "I've got a runny nose and scratchy throat since last night.",
2369
+ "vitals": {
2370
+ "hr": 78,
2371
+ "bp_sys": 120,
2372
+ "bp_dia": 80,
2373
+ "spo2": 99,
2374
+ "rr": 18,
2375
+ "temp": 37.2,
2376
+ "avpu": "A"
2377
+ },
2378
+ "history": {
2379
+ "age": 29,
2380
+ "gender": "male",
2381
+ "relevant_pmh": "Seasonal allergies",
2382
+ "time_course": "Symptoms started 12 hours ago"
2383
+ }
2384
+ },
2385
+ {
2386
+ "chief_complaint": "I think I have a UTI again. It's uncomfortable when I pee.",
2387
+ "vitals": {
2388
+ "hr": 85,
2389
+ "bp_sys": 110,
2390
+ "bp_dia": 70,
2391
+ "spo2": 97,
2392
+ "rr": 16,
2393
+ "temp": 37.5,
2394
+ "avpu": "A"
2395
+ },
2396
+ "history": {
2397
+ "age": 52,
2398
+ "gender": "female",
2399
+ "relevant_pmh": "Recurrent UTIs",
2400
+ "time_course": "Symptoms began 2 days ago"
2401
+ }
2402
+ },
2403
+ {
2404
+ "chief_complaint": "My daughter has been pulling at her ear and crying more than usual.",
2405
+ "vitals": {
2406
+ "hr": 90,
2407
+ "bp_sys": 104,
2408
+ "bp_dia": 68,
2409
+ "spo2": 99,
2410
+ "rr": 20,
2411
+ "temp": 38.0,
2412
+ "avpu": "A"
2413
+ },
2414
+ "history": {
2415
+ "age": 3,
2416
+ "gender": "female",
2417
+ "relevant_pmh": "None",
2418
+ "time_course": "Started last night"
2419
+ }
2420
+ },
2421
+ {
2422
+ "chief_complaint": "Got stung by a bee, it's a bit swollen but not too bad.",
2423
+ "vitals": {
2424
+ "hr": 88,
2425
+ "bp_sys": 115,
2426
+ "bp_dia": 75,
2427
+ "spo2": 98,
2428
+ "rr": 17,
2429
+ "temp": 36.7,
2430
+ "avpu": "A"
2431
+ },
2432
+ "history": {
2433
+ "age": 45,
2434
+ "gender": "male",
2435
+ "relevant_pmh": "None",
2436
+ "time_course": "Happened 3 hours ago"
2437
+ }
2438
+ },
2439
+ {
2440
+ "chief_complaint": "My knee hurts from a soccer match yesterday, just a bit swollen.",
2441
+ "vitals": {
2442
+ "hr": 95,
2443
+ "bp_sys": 112,
2444
+ "bp_dia": 72,
2445
+ "spo2": 98,
2446
+ "rr": 15,
2447
+ "temp": 36.9,
2448
+ "avpu": "A"
2449
+ },
2450
+ "history": {
2451
+ "age": 17,
2452
+ "gender": "male",
2453
+ "relevant_pmh": "None",
2454
+ "time_course": "Pain started 18 hours ago"
2455
+ }
2456
+ },
2457
+ {
2458
+ "chief_complaint": "Cut my finger while chopping vegetables, it's not deep but still bleeds a bit.",
2459
+ "vitals": {
2460
+ "hr": 80,
2461
+ "bp_sys": 120,
2462
+ "bp_dia": 78,
2463
+ "spo2": 99,
2464
+ "rr": 16,
2465
+ "temp": 36.6,
2466
+ "avpu": "A"
2467
+ },
2468
+ "history": {
2469
+ "age": 28,
2470
+ "gender": "female",
2471
+ "relevant_pmh": "None",
2472
+ "time_course": "Injury occurred 2 hours ago"
2473
+ }
2474
+ },
2475
+ {
2476
+ "chief_complaint": "I've had this cough and stuffy nose for a couple of days.",
2477
+ "vitals": {
2478
+ "hr": 76,
2479
+ "bp_sys": 118,
2480
+ "bp_dia": 79,
2481
+ "spo2": 97,
2482
+ "rr": 18,
2483
+ "temp": 37.3,
2484
+ "avpu": "A"
2485
+ },
2486
+ "history": {
2487
+ "age": 40,
2488
+ "gender": "female",
2489
+ "relevant_pmh": "Asthma, controlled",
2490
+ "time_course": "Symptoms started 3 days ago"
2491
+ }
2492
+ },
2493
+ {
2494
+ "chief_complaint": "I've been getting these headaches off and on, but they're not too bad.",
2495
+ "vitals": {
2496
+ "hr": 82,
2497
+ "bp_sys": 130,
2498
+ "bp_dia": 85,
2499
+ "spo2": 98,
2500
+ "rr": 14,
2501
+ "temp": 36.7,
2502
+ "avpu": "A"
2503
+ },
2504
+ "history": {
2505
+ "age": 48,
2506
+ "gender": "male",
2507
+ "relevant_pmh": "Hypertension",
2508
+ "time_course": "Headaches started a week ago"
2509
+ }
2510
+ },
2511
+ {
2512
+ "chief_complaint": "My wrist is swollen, I think I sprained it when I fell off my bike yesterday.",
2513
+ "vitals": {
2514
+ "hr": 88,
2515
+ "bp_sys": 116,
2516
+ "bp_dia": 74,
2517
+ "spo2": 99,
2518
+ "rr": 16,
2519
+ "temp": 36.8,
2520
+ "avpu": "A"
2521
+ },
2522
+ "history": {
2523
+ "age": 23,
2524
+ "gender": "female",
2525
+ "relevant_pmh": "None",
2526
+ "time_course": "Injury happened 24 hours ago"
2527
+ }
2528
+ }
2529
+ ],
2530
+ "5": [
2531
+ {
2532
+ "chief_complaint": "I need a refill on my blood pressure medicine.",
2533
+ "vitals": {
2534
+ "hr": 78,
2535
+ "bp_sys": 135,
2536
+ "bp_dia": 85,
2537
+ "spo2": 98,
2538
+ "rr": 16,
2539
+ "temp": 36.8,
2540
+ "avpu": "A"
2541
+ },
2542
+ "history": {
2543
+ "age": 62,
2544
+ "gender": "female",
2545
+ "relevant_pmh": "hypertension",
2546
+ "time_course": "ongoing for years"
2547
+ }
2548
+ },
2549
+ {
2550
+ "chief_complaint": "My back's been aching for a few weeks now, it's not getting worse but it's annoying.",
2551
+ "vitals": {
2552
+ "hr": 72,
2553
+ "bp_sys": 120,
2554
+ "bp_dia": 78,
2555
+ "spo2": 99,
2556
+ "rr": 14,
2557
+ "temp": 36.6,
2558
+ "avpu": "A"
2559
+ },
2560
+ "history": {
2561
+ "age": 45,
2562
+ "gender": "male",
2563
+ "relevant_pmh": "none",
2564
+ "time_course": "3 weeks"
2565
+ }
2566
+ },
2567
+ {
2568
+ "chief_complaint": "I've got this itchy rash that's not going away.",
2569
+ "vitals": {
2570
+ "hr": 76,
2571
+ "bp_sys": 118,
2572
+ "bp_dia": 76,
2573
+ "spo2": 98,
2574
+ "rr": 15,
2575
+ "temp": 37.0,
2576
+ "avpu": "A"
2577
+ },
2578
+ "history": {
2579
+ "age": 34,
2580
+ "gender": "non-binary",
2581
+ "relevant_pmh": "eczema",
2582
+ "time_course": "1 week"
2583
+ }
2584
+ },
2585
+ {
2586
+ "chief_complaint": "I came for my diabetes medication review.",
2587
+ "vitals": {
2588
+ "hr": 80,
2589
+ "bp_sys": 130,
2590
+ "bp_dia": 82,
2591
+ "spo2": 97,
2592
+ "rr": 17,
2593
+ "temp": 36.5,
2594
+ "avpu": "A"
2595
+ },
2596
+ "history": {
2597
+ "age": 58,
2598
+ "gender": "female",
2599
+ "relevant_pmh": "type 2 diabetes",
2600
+ "time_course": "stable"
2601
+ }
2602
+ },
2603
+ {
2604
+ "chief_complaint": "I've had a runny nose and a slight cold for a couple of weeks.",
2605
+ "vitals": {
2606
+ "hr": 68,
2607
+ "bp_sys": 115,
2608
+ "bp_dia": 75,
2609
+ "spo2": 98,
2610
+ "rr": 16,
2611
+ "temp": 37.2,
2612
+ "avpu": "A"
2613
+ },
2614
+ "history": {
2615
+ "age": 28,
2616
+ "gender": "male",
2617
+ "relevant_pmh": "none",
2618
+ "time_course": "2 weeks"
2619
+ }
2620
+ },
2621
+ {
2622
+ "chief_complaint": "I've been having headaches every now and then, nothing too bad.",
2623
+ "vitals": {
2624
+ "hr": 74,
2625
+ "bp_sys": 122,
2626
+ "bp_dia": 80,
2627
+ "spo2": 98,
2628
+ "rr": 15,
2629
+ "temp": 36.9,
2630
+ "avpu": "A"
2631
+ },
2632
+ "history": {
2633
+ "age": 39,
2634
+ "gender": "female",
2635
+ "relevant_pmh": "migraines",
2636
+ "time_course": "intermittent over 3 weeks"
2637
+ }
2638
+ },
2639
+ {
2640
+ "chief_complaint": "I've been feeling a bit bloated, it's been a while now.",
2641
+ "vitals": {
2642
+ "hr": 70,
2643
+ "bp_sys": 118,
2644
+ "bp_dia": 79,
2645
+ "spo2": 99,
2646
+ "rr": 14,
2647
+ "temp": 36.7,
2648
+ "avpu": "A"
2649
+ },
2650
+ "history": {
2651
+ "age": 50,
2652
+ "gender": "male",
2653
+ "relevant_pmh": "irritable bowel syndrome",
2654
+ "time_course": "4 weeks"
2655
+ }
2656
+ },
2657
+ {
2658
+ "chief_complaint": "I need a note for work because I've been home with a cold.",
2659
+ "vitals": {
2660
+ "hr": 72,
2661
+ "bp_sys": 120,
2662
+ "bp_dia": 80,
2663
+ "spo2": 97,
2664
+ "rr": 18,
2665
+ "temp": 37.1,
2666
+ "avpu": "A"
2667
+ },
2668
+ "history": {
2669
+ "age": 31,
2670
+ "gender": "female",
2671
+ "relevant_pmh": "none",
2672
+ "time_course": "5 days"
2673
+ }
2674
+ },
2675
+ {
2676
+ "chief_complaint": "I have these mild muscle cramps that come and go.",
2677
+ "vitals": {
2678
+ "hr": 75,
2679
+ "bp_sys": 121,
2680
+ "bp_dia": 79,
2681
+ "spo2": 98,
2682
+ "rr": 16,
2683
+ "temp": 36.5,
2684
+ "avpu": "A"
2685
+ },
2686
+ "history": {
2687
+ "age": 47,
2688
+ "gender": "non-binary",
2689
+ "relevant_pmh": "slight electrolyte imbalance",
2690
+ "time_course": "intermittent for 2 weeks"
2691
+ }
2692
+ },
2693
+ {
2694
+ "chief_complaint": "I'm here for my annual asthma check-up.",
2695
+ "vitals": {
2696
+ "hr": 70,
2697
+ "bp_sys": 119,
2698
+ "bp_dia": 78,
2699
+ "spo2": 96,
2700
+ "rr": 17,
2701
+ "temp": 36.6,
2702
+ "avpu": "A"
2703
+ },
2704
+ "history": {
2705
+ "age": 22,
2706
+ "gender": "male",
2707
+ "relevant_pmh": "asthma",
2708
+ "time_course": "control is good"
2709
+ }
2710
+ },
2711
+ {
2712
+ "chief_complaint": "I've run out of my blood pressure pills, can I get a new prescription?",
2713
+ "vitals": {
2714
+ "hr": 72,
2715
+ "bp_sys": 130,
2716
+ "bp_dia": 85,
2717
+ "spo2": 98,
2718
+ "rr": 16,
2719
+ "temp": 36.5,
2720
+ "avpu": "A"
2721
+ },
2722
+ "history": {
2723
+ "age": 67,
2724
+ "gender": "male",
2725
+ "relevant PMH": "hypertension",
2726
+ "time course": "last refill was 3 months ago"
2727
+ }
2728
+ },
2729
+ {
2730
+ "chief_complaint": "I've got this itchy patch on my arm, it's been there for a while now.",
2731
+ "vitals": {
2732
+ "hr": 78,
2733
+ "bp_sys": 122,
2734
+ "bp_dia": 80,
2735
+ "spo2": 98,
2736
+ "rr": 14,
2737
+ "temp": 36.8,
2738
+ "avpu": "A"
2739
+ },
2740
+ "history": {
2741
+ "age": 22,
2742
+ "gender": "female",
2743
+ "relevant PMH": "eczema",
2744
+ "time course": "3 weeks"
2745
+ }
2746
+ },
2747
+ {
2748
+ "chief_complaint": "I need a sick note for work, I've had this back ache on and off for a month.",
2749
+ "vitals": {
2750
+ "hr": 70,
2751
+ "bp_sys": 128,
2752
+ "bp_dia": 82,
2753
+ "spo2": 99,
2754
+ "rr": 15,
2755
+ "temp": 37.0,
2756
+ "avpu": "A"
2757
+ },
2758
+ "history": {
2759
+ "age": 36,
2760
+ "gender": "male",
2761
+ "relevant PMH": "recurrent low back pain",
2762
+ "time course": "1 month"
2763
+ }
2764
+ },
2765
+ {
2766
+ "chief_complaint": "I'm here for my regular diabetes medication check-up.",
2767
+ "vitals": {
2768
+ "hr": 74,
2769
+ "bp_sys": 125,
2770
+ "bp_dia": 78,
2771
+ "spo2": 97,
2772
+ "rr": 16,
2773
+ "temp": 36.7,
2774
+ "avpu": "A"
2775
+ },
2776
+ "history": {
2777
+ "age": 58,
2778
+ "gender": "female",
2779
+ "relevant PMH": "type 2 diabetes",
2780
+ "time course": "routine check-up"
2781
+ }
2782
+ },
2783
+ {
2784
+ "chief_complaint": "My knee has been aching for weeks, should I be worried?",
2785
+ "vitals": {
2786
+ "hr": 72,
2787
+ "bp_sys": 120,
2788
+ "bp_dia": 76,
2789
+ "spo2": 98,
2790
+ "rr": 15,
2791
+ "temp": 36.9,
2792
+ "avpu": "A"
2793
+ },
2794
+ "history": {
2795
+ "age": 44,
2796
+ "gender": "female",
2797
+ "relevant PMH": "none",
2798
+ "time course": "6 weeks"
2799
+ }
2800
+ },
2801
+ {
2802
+ "chief_complaint": "I need to get my asthma inhaler refilled, I ran out last week.",
2803
+ "vitals": {
2804
+ "hr": 80,
2805
+ "bp_sys": 118,
2806
+ "bp_dia": 75,
2807
+ "spo2": 99,
2808
+ "rr": 18,
2809
+ "temp": 36.6,
2810
+ "avpu": "A"
2811
+ },
2812
+ "history": {
2813
+ "age": 29,
2814
+ "gender": "male",
2815
+ "relevant PMH": "asthma",
2816
+ "time course": "inhaler finished last week"
2817
+ }
2818
+ },
2819
+ {
2820
+ "chief_complaint": "I've had a headache, just dull, for about two weeks now.",
2821
+ "vitals": {
2822
+ "hr": 68,
2823
+ "bp_sys": 115,
2824
+ "bp_dia": 78,
2825
+ "spo2": 98,
2826
+ "rr": 14,
2827
+ "temp": 37.1,
2828
+ "avpu": "A"
2829
+ },
2830
+ "history": {
2831
+ "age": 40,
2832
+ "gender": "non-binary",
2833
+ "relevant PMH": "migraine",
2834
+ "time course": "2 weeks"
2835
+ }
2836
+ },
2837
+ {
2838
+ "chief_complaint": "Can I get something for these occasional heartburn episodes?",
2839
+ "vitals": {
2840
+ "hr": 76,
2841
+ "bp_sys": 120,
2842
+ "bp_dia": 77,
2843
+ "spo2": 97,
2844
+ "rr": 16,
2845
+ "temp": 36.5,
2846
+ "avpu": "A"
2847
+ },
2848
+ "history": {
2849
+ "age": 55,
2850
+ "gender": "female",
2851
+ "relevant PMH": "none",
2852
+ "time course": "over the past month"
2853
+ }
2854
+ },
2855
+ {
2856
+ "chief_complaint": "This cough won't go away, it's been annoying for about a month.",
2857
+ "vitals": {
2858
+ "hr": 72,
2859
+ "bp_sys": 118,
2860
+ "bp_dia": 75,
2861
+ "spo2": 96,
2862
+ "rr": 17,
2863
+ "temp": 36.8,
2864
+ "avpu": "A"
2865
+ },
2866
+ "history": {
2867
+ "age": 48,
2868
+ "gender": "male",
2869
+ "relevant PMH": "seasonal allergies",
2870
+ "time course": "1 month"
2871
+ }
2872
+ },
2873
+ {
2874
+ "chief_complaint": "Just wanted to see if there's anything I can do for this dry skin, it's been like this for ages.",
2875
+ "vitals": {
2876
+ "hr": 68,
2877
+ "bp_sys": 110,
2878
+ "bp_dia": 70,
2879
+ "spo2": 99,
2880
+ "rr": 15,
2881
+ "temp": 36.4,
2882
+ "avpu": "A"
2883
+ },
2884
+ "history": {
2885
+ "age": 61,
2886
+ "gender": "female",
2887
+ "relevant PMH": "hypothyroidism",
2888
+ "time course": "several months"
2889
+ }
2890
+ }
2891
+ ]
2892
+ }
data/gpt_train.jsonl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c1ac7b5b7901ba36504cc8ed7ca102b3b7e29bd5fac83225fdfd977ce7ff9f04
3
+ size 126843
data/gpt_train_1768262967.jsonl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:43d5315e895395ea1527ba8a9c7ce4b1be5ce0980e32e7072a61aed1cf2b4089
3
+ size 126440
data/gpt_train_1768263238.jsonl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:59614711514581ebb39937cc2de39b17f26385d0a3a8b609f8bfbe88ee45e747
3
+ size 119683
data/gpt_train_1768263486.jsonl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9c23626a2d5066662063c823e5ecf73d452ebed6a8ab16a126a001d823973087
3
+ size 127940
data/gpt_train_1768263718.jsonl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:258cc804387a1ee0ec0aa2cb4aebe264417f354312e2ae74a1278f12ddf6463f
3
+ size 127083
data/gpt_train_1768263967.jsonl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:35fcb23391df7fb23f62fd363fd97981b7cd46c0cfe758546ac09595d3640f23
3
+ size 127273
data/gpt_train_batch2.jsonl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:19862a3186d9a1205ba62d11aeed4990b98e53ca04ddbe70ed694bf84c76a269
3
+ size 127088
data/gpt_train_batch3.jsonl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dae84d7dcd8b6af4cd13175c0f6954028dfd819786d8557b24b80b5f3a0b5e63
3
+ size 127042
data/gpt_train_batch4.jsonl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a65fd188d583f72e0048b04d9c428b60a35778d9f6c0f9eab3c07c17a5e890bd
3
+ size 127784
data/gpt_train_batch5.jsonl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8b8dbd36f3499ef0ee3421f8257ea667934b123d126837b2eda2bc5c6389e952
3
+ size 127646
data/gpt_train_batch6.jsonl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c1ac7b5b7901ba36504cc8ed7ca102b3b7e29bd5fac83225fdfd977ce7ff9f04
3
+ size 126843
data/train.jsonl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c2190ce6670716d02d73bbab104ce778329e01c0f9457df4ead1266be403fbdb
3
+ size 391884
data/train_expanded.jsonl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:83f8d6551e957ba67bf874998d0026d761b7f8b892e59ab8521fce7193bc54d0
3
+ size 1656706
data/val.jsonl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0da84966544b66084b27166bcbaa52ca15e5625c1ef89708713b3edd4d71937a
3
+ size 78769
demo_human_play.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Demo script: Play the Triage Environment as a Human
3
+
4
+ Run this to test the environment interactively.
5
+ """
6
+
7
+ import sys
8
+ sys.path.insert(0, '.')
9
+
10
+ from nursesim_rl import TriageEnv
11
+
12
+
13
+ def main():
14
+ env = TriageEnv(render_mode="human", seed=42)
15
+ obs, info = env.reset()
16
+
17
+ print("\n🏥 Welcome to the A&E Triage Simulator!")
18
+ print("You are the Triage Nurse. Assess each patient and assign a category.\n")
19
+
20
+ total_reward = 0
21
+ step = 0
22
+
23
+ while True:
24
+ # Render current patient
25
+ env.render()
26
+
27
+ if obs["patient_id"] == "":
28
+ print("\n✅ Shift complete! No more patients.")
29
+ break
30
+
31
+ # Get user input
32
+ try:
33
+ category = int(input("\nEnter triage category (1-5): "))
34
+ if category < 1 or category > 5:
35
+ print("Invalid category. Please enter 1-5.")
36
+ continue
37
+ except ValueError:
38
+ print("Invalid input. Please enter a number.")
39
+ continue
40
+
41
+ print("\nInterventions:")
42
+ for i, intervention in enumerate(env.INTERVENTIONS):
43
+ print(f" [{i}] {intervention}")
44
+
45
+ try:
46
+ intervention_idx = int(input("Choose intervention (0-6): "))
47
+ if intervention_idx < 0 or intervention_idx >= len(env.INTERVENTIONS):
48
+ intervention_idx = 0
49
+ except ValueError:
50
+ intervention_idx = 0
51
+
52
+ # Take action
53
+ action = {
54
+ "triage_category": category,
55
+ "intervention": intervention_idx,
56
+ }
57
+
58
+ obs, reward, terminated, truncated, info = env.step(action)
59
+ total_reward += reward
60
+ step += 1
61
+
62
+ # Feedback
63
+ true_cat = info.get("true_category")
64
+ if true_cat and category == true_cat:
65
+ print(f"\n✅ Correct! Category {category} was right. Reward: +{reward:.1f}")
66
+ elif true_cat:
67
+ print(f"\n⚠️ The correct category was {true_cat}. You chose {category}. Reward: {reward:.1f}")
68
+
69
+ if terminated or truncated:
70
+ break
71
+
72
+ # Final stats
73
+ print("\n" + "="*60)
74
+ print("📊 SHIFT SUMMARY")
75
+ print("="*60)
76
+ print(f" Patients Seen: {info.get('patients_seen', step)}")
77
+ print(f" Correct Triage: {info.get('correct_triage', 0)}")
78
+ print(f" Safety Failures: {info.get('safety_failures', 0)}")
79
+ print(f" Total Reward: {total_reward:.1f}")
80
+ print("="*60)
81
+
82
+ env.close()
83
+
84
+
85
+ if __name__ == "__main__":
86
+ main()
generate_dataset.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Training Dataset Generator for NurseSim-RL
3
+
4
+ Generates a dataset of triage scenarios with expert decisions for SFT training.
5
+ Output format: JSONL compatible with Unsloth/TRL.
6
+ """
7
+
8
+ import json
9
+ import random
10
+ from typing import Dict, List
11
+ from pathlib import Path
12
+
13
+ # Import from our environment
14
+ import sys
15
+ sys.path.insert(0, str(Path(__file__).parent))
16
+ from nursesim_rl.patient_generator import PatientGenerator, SCENARIOS
17
+
18
+
19
+ def format_observation(patient_data: Dict) -> str:
20
+ """Format patient data as a text observation for the LLM."""
21
+ vitals = patient_data["vitals"]
22
+ return f"""PATIENT PRESENTING TO A&E TRIAGE
23
+
24
+ Chief Complaint: "{patient_data['complaint']}"
25
+
26
+ Vitals:
27
+ - HR: {vitals['hr']:.0f} bpm
28
+ - BP: {vitals['bp_sys']:.0f}/{vitals['bp_dia']:.0f} mmHg
29
+ - SpO2: {vitals['spo2']:.0f}%
30
+ - RR: {vitals['rr']:.0f} /min
31
+ - Temp: {vitals['temp']:.1f}C
32
+ - AVPU: {vitals['avpu']}
33
+
34
+ History: {patient_data['history']}
35
+
36
+ WAITING ROOM: 12 patients | AVAILABLE BEDS: 4
37
+
38
+ What is your triage decision?"""
39
+
40
+
41
+ def get_expert_decision(category: int) -> Dict:
42
+ """Get the expert triage decision based on category."""
43
+ decisions = {
44
+ 1: {
45
+ "category": 1,
46
+ "category_name": "Immediate (Red)",
47
+ "intervention": "send_to_resus",
48
+ "reasoning": "Life-threatening presentation requiring immediate resuscitation. Activate trauma/medical emergency team."
49
+ },
50
+ 2: {
51
+ "category": 2,
52
+ "category_name": "Very Urgent (Orange)",
53
+ "intervention": "send_to_majors",
54
+ "reasoning": "Time-critical condition. Requires senior review within 10 minutes. Prioritise assessment."
55
+ },
56
+ 3: {
57
+ "category": 3,
58
+ "category_name": "Urgent (Yellow)",
59
+ "intervention": "send_to_majors",
60
+ "reasoning": "Urgent presentation requiring assessment within 60 minutes. Monitor for deterioration."
61
+ },
62
+ 4: {
63
+ "category": 4,
64
+ "category_name": "Standard (Green)",
65
+ "intervention": "send_to_minors",
66
+ "reasoning": "Stable presentation suitable for minor injuries/illness stream. Can wait safely."
67
+ },
68
+ 5: {
69
+ "category": 5,
70
+ "category_name": "Non-urgent (Blue)",
71
+ "intervention": "refer_to_gp",
72
+ "reasoning": "Non-urgent presentation. Redirect to primary care or self-care advice."
73
+ },
74
+ }
75
+ return decisions[category]
76
+
77
+
78
+ def format_response(decision: Dict) -> str:
79
+ """Format the expert decision as an LLM response."""
80
+ return f"""TRIAGE DECISION:
81
+
82
+ Category: {decision['category']} - {decision['category_name']}
83
+ Intervention: {decision['intervention']}
84
+
85
+ Clinical Reasoning: {decision['reasoning']}"""
86
+
87
+
88
+ def generate_dataset(n_samples: int = 500, seed: int = 42) -> List[Dict]:
89
+ """Generate a training dataset of triage scenarios."""
90
+ random.seed(seed)
91
+ dataset = []
92
+
93
+ # Distribution adjusted for balanced IMMEDIATE case training
94
+ # Original: {1: 0.05, 2: 0.15, 3: 0.35, 4: 0.35, 5: 0.10}
95
+ category_weights = {1: 0.20, 2: 0.20, 3: 0.25, 4: 0.25, 5: 0.10}
96
+
97
+ for i in range(n_samples):
98
+ # Weighted category selection
99
+ category = random.choices(
100
+ list(category_weights.keys()),
101
+ weights=list(category_weights.values())
102
+ )[0]
103
+
104
+ # Get a random scenario for this category
105
+ scenario = random.choice(SCENARIOS[category])
106
+
107
+ # Add some noise to vitals
108
+ noisy_vitals = {}
109
+ for k, v in scenario["vitals"].items():
110
+ if isinstance(v, (int, float)) and k != "avpu":
111
+ noise = random.gauss(0, abs(v) * 0.05) if v != 0 else 0
112
+ noisy_vitals[k] = v + noise
113
+ else:
114
+ noisy_vitals[k] = v
115
+
116
+ patient_data = {
117
+ "complaint": scenario["chief_complaint"],
118
+ "vitals": noisy_vitals,
119
+ "history": scenario["history"],
120
+ }
121
+
122
+ # Format as instruction-following example
123
+ observation = format_observation(patient_data)
124
+ decision = get_expert_decision(category)
125
+ response = format_response(decision)
126
+
127
+ # Alpaca/ChatML format
128
+ example = {
129
+ "instruction": "You are an expert A&E Triage Nurse using the Manchester Triage System. Assess the following patient and provide your triage decision with clinical reasoning.",
130
+ "input": observation,
131
+ "output": response,
132
+ "category": category, # For analysis
133
+ }
134
+
135
+ dataset.append(example)
136
+
137
+ return dataset
138
+
139
+
140
+ def save_dataset(dataset: List[Dict], output_path: str):
141
+ """Save dataset to JSONL format."""
142
+ with open(output_path, 'w', encoding='utf-8') as f:
143
+ for example in dataset:
144
+ f.write(json.dumps(example, ensure_ascii=False) + '\n')
145
+ print(f"[OK] Saved {len(dataset)} examples to {output_path}")
146
+
147
+
148
+ def main():
149
+ print("\n" + "="*60)
150
+ print("[DATASET] NurseSim-RL Training Data Generator")
151
+ print("="*60 + "\n")
152
+
153
+ # Generate training set
154
+ print("Generating training dataset (500 examples)...")
155
+ train_data = generate_dataset(n_samples=500, seed=42)
156
+ save_dataset(train_data, "data/train.jsonl")
157
+
158
+ # Generate validation set
159
+ print("Generating validation dataset (100 examples)...")
160
+ val_data = generate_dataset(n_samples=100, seed=123)
161
+ save_dataset(val_data, "data/val.jsonl")
162
+
163
+ # Stats
164
+ print("\n" + "-"*40)
165
+ print("Dataset Statistics:")
166
+ for cat in range(1, 6):
167
+ train_count = sum(1 for x in train_data if x["category"] == cat)
168
+ val_count = sum(1 for x in val_data if x["category"] == cat)
169
+ print(f" Category {cat}: {train_count} train / {val_count} val")
170
+ print("-"*40 + "\n")
171
+
172
+ # Preview
173
+ print("Sample training example:")
174
+ print("-"*40)
175
+ sample = train_data[0]
176
+ print(f"[INSTRUCTION]\n{sample['instruction']}\n")
177
+ print(f"[INPUT]\n{sample['input']}\n")
178
+ print(f"[OUTPUT]\n{sample['output']}")
179
+ print("-"*40 + "\n")
180
+
181
+
182
+ if __name__ == "__main__":
183
+ # Create data directory
184
+ Path("data").mkdir(exist_ok=True)
185
+ main()
generate_gpt_scenarios.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ GPT-Powered Triage Scenario Generator
3
+ Generates gold-standard synthetic training data using GPT-5/4
4
+ """
5
+
6
+ import os
7
+ import json
8
+ import openai
9
+ from pathlib import Path
10
+
11
+ # Try to get API key from environment
12
+ API_KEY = os.environ.get('OPENAI_API_KEY')
13
+ if not API_KEY:
14
+ print("⚠️ Please set OPENAI_API_KEY environment variable")
15
+ print("Run: $env:OPENAI_API_KEY='your-key-here'")
16
+ exit(1)
17
+
18
+ client = openai.OpenAI(api_key=API_KEY)
19
+
20
+ # Find working model
21
+ MODELS = ['gpt-4o', 'gpt-4-turbo', 'gpt-4', 'gpt-3.5-turbo']
22
+ MODEL = None
23
+
24
+ print("🔍 Finding available model...")
25
+ for m in MODELS:
26
+ try:
27
+ client.chat.completions.create(model=m, messages=[{"role": "user", "content": "test"}], max_tokens=5)
28
+ MODEL = m
29
+ print(f"✅ Using: {m}")
30
+ break
31
+ except Exception as e:
32
+ continue
33
+
34
+ if not MODEL:
35
+ print("❌ No model available")
36
+ exit(1)
37
+
38
+ # System prompt for clinical accuracy
39
+ SYSTEM_PROMPT = """You are an expert A&E Triage Nurse and clinical educator with 20 years of experience.
40
+ You are creating realistic training scenarios for the Manchester Triage System.
41
+
42
+ For each scenario, provide a JSON object with:
43
+ 1. chief_complaint: What the patient/relative actually SAYS (patient language, not medical jargon)
44
+ 2. vitals: Object with hr, bp_sys, bp_dia, spo2, rr, temp, avpu (A/V/P/U)
45
+ 3. history: Brief clinical history (age, gender, relevant PMH, time course)
46
+
47
+ CRITICAL RULES:
48
+ - Vitals MUST be physiologically consistent (shock = low BP + high HR)
49
+ - Chief complaints use patient language, not medical terminology
50
+ - Include diverse demographics
51
+ - Cover atypical presentations
52
+
53
+ Return a JSON array of 10 scenarios. No markdown, just JSON."""
54
+
55
+ CATEGORY_PROMPTS = {
56
+ 1: """Generate 10 Category 1 (IMMEDIATE/Red) triage scenarios.
57
+ MTS Discriminators: Airway compromise, Inadequate breathing, Shock, Unresponsive, Currently fitting
58
+
59
+ Include: STEMI, stroke, anaphylaxis, trauma, septic shock, cardiac arrest, status epilepticus,
60
+ massive GI bleed, tension pneumothorax, DKA coma, eclampsia, meningococcal sepsis, drowning, burns with inhalation.""",
61
+
62
+ 2: """Generate 10 Category 2 (VERY URGENT/Orange) scenarios.
63
+ MTS Discriminators: Severe pain, Altered consciousness, Very hot adult/child, Significant mechanism
64
+
65
+ Include: Chest pain (possible ACS), thunderclap headache, focal neurology, high fever with red flags,
66
+ significant trauma, acute abdomen, spinal injury, ongoing seizure that stopped.""",
67
+
68
+ 3: """Generate 10 Category 3 (URGENT/Yellow) scenarios.
69
+ MTS Discriminators: Moderate pain, Hot adult/child, Persistent vomiting, Pleuritic pain
70
+
71
+ Include: COPD exacerbation, cellulitis, renal colic, fractures, moderate asthma,
72
+ acute confusion (elderly), DVT symptoms, pyelonephritis.""",
73
+
74
+ 4: """Generate 10 Category 4 (STANDARD/Green) scenarios.
75
+ MTS Discriminators: Recent mild pain, Warm, Recent problem
76
+
77
+ Include: Minor injuries, viral illnesses, stable chronic conditions, minor lacerations,
78
+ sprains, insect bites, UTI symptoms, ear infections.""",
79
+
80
+ 5: """Generate 10 Category 5 (NON-URGENT/Blue) scenarios.
81
+ MTS Discriminators: Recent mild problem
82
+
83
+ Include: Prescription requests, chronic stable issues, minor rashes, medication reviews,
84
+ sick notes, minor aches lasting weeks."""
85
+ }
86
+
87
+
88
+ def generate_batch(category: int, batch_num: int) -> list:
89
+ """Generate a batch of scenarios for a category"""
90
+ try:
91
+ response = client.chat.completions.create(
92
+ model=MODEL,
93
+ messages=[
94
+ {"role": "system", "content": SYSTEM_PROMPT},
95
+ {"role": "user", "content": CATEGORY_PROMPTS[category] + f"\n\nBatch {batch_num} - ensure unique scenarios not seen before."}
96
+ ],
97
+ temperature=0.9,
98
+ max_tokens=4000
99
+ )
100
+
101
+ content = response.choices[0].message.content.strip()
102
+ # Clean markdown
103
+ if content.startswith('```'):
104
+ content = content.split('```')[1]
105
+ if content.startswith('json'):
106
+ content = content[4:]
107
+ content = content.strip()
108
+
109
+ try:
110
+ return json.loads(content)
111
+ except json.JSONDecodeError:
112
+ print(f" ⚠️ JSON Error. Attempting repair...")
113
+ # Simple repair: try to find the list bracket
114
+ start = content.find('[')
115
+ end = content.rfind(']')
116
+ if start != -1 and end != -1:
117
+ try:
118
+ return json.loads(content[start:end+1])
119
+ except:
120
+ pass
121
+ print(f" ❌ Failed to parse JSON batch.")
122
+ return []
123
+
124
+ except Exception as e:
125
+ print(f" Error: {str(e)[:60]}")
126
+ return []
127
+
128
+
129
+ def format_training_example(scenario: dict, category: int) -> dict:
130
+ """Convert scenario to training format"""
131
+ vitals = scenario.get('vitals', {})
132
+
133
+ input_text = f"""PATIENT PRESENTING TO A&E TRIAGE
134
+
135
+ Chief Complaint: "{scenario.get('chief_complaint', 'Unknown')}"
136
+
137
+ Vitals:
138
+ - HR: {vitals.get('hr', 80):.0f} bpm
139
+ - BP: {vitals.get('bp_sys', 120):.0f}/{vitals.get('bp_dia', 80):.0f} mmHg
140
+ - SpO2: {vitals.get('spo2', 98):.0f}%
141
+ - RR: {vitals.get('rr', 16):.0f} /min
142
+ - Temp: {vitals.get('temp', 37.0):.1f}C
143
+ - AVPU: {vitals.get('avpu', 'A')}
144
+
145
+ History: {scenario.get('history', 'Unknown')}
146
+
147
+ WAITING ROOM: 12 patients | AVAILABLE BEDS: 4
148
+
149
+ What is your triage decision?"""
150
+
151
+ decisions = {
152
+ 1: ("Immediate (Red)", "send_to_resus", "Life-threatening presentation requiring immediate resuscitation."),
153
+ 2: ("Very Urgent (Orange)", "send_to_majors", "Time-critical condition. Requires senior review within 10 minutes."),
154
+ 3: ("Urgent (Yellow)", "send_to_majors", "Urgent presentation requiring assessment within 60 minutes."),
155
+ 4: ("Standard (Green)", "send_to_minors", "Stable presentation suitable for minor injuries/illness stream."),
156
+ 5: ("Non-urgent (Blue)", "refer_to_gp", "Non-urgent presentation. Redirect to primary care.")
157
+ }
158
+
159
+ name, action, reason = decisions[category]
160
+ output_text = f"""TRIAGE DECISION:
161
+
162
+ Category: {category} - {name}
163
+ Intervention: {action}
164
+
165
+ Clinical Reasoning: {reason}"""
166
+
167
+ return {
168
+ "instruction": "You are an expert A&E Triage Nurse using the Manchester Triage System. Assess the following patient and provide your triage decision with clinical reasoning.",
169
+ "input": input_text,
170
+ "output": output_text,
171
+ "category": category
172
+ }
173
+
174
+
175
+ def main():
176
+ print("\n" + "="*60)
177
+ print("🏥 GPT-Powered Triage Scenario Generator")
178
+ print("="*60 + "\n")
179
+
180
+ # Configure batches per category
181
+ batches = {1: 5, 2: 3, 3: 3, 4: 3, 5: 2}
182
+
183
+ all_scenarios = {}
184
+ training_data = []
185
+
186
+ for cat in [1, 2, 3, 4, 5]:
187
+ print(f"\n📋 Category {cat} ({batches[cat]} batches):")
188
+ all_scenarios[cat] = []
189
+
190
+ for b in range(batches[cat]):
191
+ print(f" Batch {b+1}...", end=" ")
192
+ scenarios = generate_batch(cat, b+1)
193
+ all_scenarios[cat].extend(scenarios)
194
+ print(f"✅ {len(scenarios)} scenarios")
195
+
196
+ # Convert to training format
197
+ for s in all_scenarios[cat]:
198
+ try:
199
+ example = format_training_example(s, cat)
200
+ training_data.append(example)
201
+ except:
202
+ continue
203
+
204
+ print(f" Total Cat {cat}: {len(all_scenarios[cat])} scenarios")
205
+
206
+ # Save raw scenarios
207
+ with open('data/gpt_scenarios.json', 'w') as f:
208
+ json.dump(all_scenarios, f, indent=2)
209
+ print(f"\n✅ Saved raw scenarios: data/gpt_scenarios.json")
210
+
211
+ # Save as JSONL with unique timestamp
212
+ import time
213
+ timestamp = int(time.time())
214
+ output_file = f'data/gpt_train_{timestamp}.jsonl'
215
+
216
+ with open(output_file, 'w') as f:
217
+ for ex in training_data:
218
+ f.write(json.dumps(ex) + '\n')
219
+ print(f"✅ Saved training data: {output_file} ({len(training_data)} examples)")
220
+
221
+ # skipped automatic merging to avoid race conditions
222
+ print(f"✅ Saved batch: {output_file} ({len(training_data)} examples)")
223
+
224
+ print("\n" + "="*60)
225
+ print("✅ Generation complete!")
226
+ print("="*60 + "\n")
227
+
228
+
229
+ if __name__ == "__main__":
230
+ main()
nursesim_rl/__init__.py CHANGED
@@ -1,10 +1,16 @@
1
- """
2
- NurseSim-RL: A Triage Environment for Reinforcement Learning
3
- OpenEnv Challenge Entry - 2026
4
- """
5
-
6
- from .triage_env import TriageEnv
7
- from .patient_generator import PatientGenerator
8
-
9
- __version__ = "0.1.0"
10
- __all__ = ["TriageEnv", "PatientGenerator"]
 
 
 
 
 
 
 
1
+ """
2
+ NurseSim-RL: A Triage Environment for Reinforcement Learning
3
+ OpenEnv Challenge Entry - 2026
4
+ """
5
+
6
+ from .triage_env import TriageEnv
7
+ from .patient_generator import PatientGenerator
8
+ from .semantic_wrapper import NurseEmbedWrapper, make_semantic_triage_env
9
+
10
+ __version__ = "0.2.0"
11
+ __all__ = [
12
+ "TriageEnv",
13
+ "PatientGenerator",
14
+ "NurseEmbedWrapper",
15
+ "make_semantic_triage_env",
16
+ ]
nursesim_rl/patient_generator.py CHANGED
@@ -1,272 +1,272 @@
1
- """
2
- Patient Generator for NurseSim-RL
3
-
4
- Generates synthetic patient scenarios based on Manchester Triage System categories.
5
- """
6
-
7
- import random
8
- from dataclasses import dataclass
9
- from typing import Dict, List, Optional
10
-
11
-
12
- @dataclass
13
- class Patient:
14
- """Represents a patient presenting to A&E."""
15
- id: str
16
- chief_complaint: str
17
- vitals: Dict[str, float]
18
- history: str
19
- true_category: int # 1-5 (Ground truth for reward calculation)
20
- time_arrived: int
21
-
22
-
23
- # Manchester Triage System Scenarios
24
- # Gold-standard scenarios validated against MTS discriminators and clinical guidelines
25
- SCENARIOS = {
26
- # Category 1: Immediate (Red) - Life threatening
27
- # MTS Discriminators: Airway compromise, Inadequate breathing, Shock, Unresponsive
28
- 1: [
29
- # Original scenarios
30
- {
31
- "chief_complaint": "I can't breathe... my chest is crushing... the pain goes down my arm.",
32
- "vitals": {"hr": 120, "bp_sys": 85, "bp_dia": 50, "spo2": 88, "rr": 32, "temp": 36.5, "avpu": "V"},
33
- "history": "65yo male, known cardiac history, sudden onset 20 mins ago."
34
- },
35
- {
36
- "chief_complaint": "He collapsed and isn't responding to me!",
37
- "vitals": {"hr": 0, "bp_sys": 0, "bp_dia": 0, "spo2": 0, "rr": 0, "temp": 35.0, "avpu": "U"},
38
- "history": "72yo male found unresponsive by wife. Bystander CPR in progress."
39
- },
40
- {
41
- "chief_complaint": "My face is swelling up and I can't swallow... I ate shellfish.",
42
- "vitals": {"hr": 130, "bp_sys": 70, "bp_dia": 40, "spo2": 85, "rr": 28, "temp": 37.0, "avpu": "A"},
43
- "history": "28yo female, known shellfish allergy, stridor audible."
44
- },
45
- # NEW: STEMI - Classic presentation
46
- {
47
- "chief_complaint": "There's an elephant sitting on my chest... I feel like I'm going to die.",
48
- "vitals": {"hr": 110, "bp_sys": 80, "bp_dia": 55, "spo2": 91, "rr": 28, "temp": 36.8, "avpu": "A"},
49
- "history": "58yo male, sweating profusely, nausea, pain radiating to jaw and left arm, 30 mins onset."
50
- },
51
- # NEW: Acute Stroke - FAST positive
52
- {
53
- "chief_complaint": "My husband's face has dropped and he can't speak properly!",
54
- "vitals": {"hr": 95, "bp_sys": 195, "bp_dia": 110, "spo2": 96, "rr": 18, "temp": 37.0, "avpu": "V"},
55
- "history": "70yo male, sudden onset 45 mins ago, right-sided weakness, slurred speech, facial droop."
56
- },
57
- # NEW: Tension Pneumothorax
58
- {
59
- "chief_complaint": "I was stabbed... I can't breathe... everything is going dark.",
60
- "vitals": {"hr": 135, "bp_sys": 75, "bp_dia": 45, "spo2": 78, "rr": 40, "temp": 36.5, "avpu": "V"},
61
- "history": "25yo male, stab wound to right chest, trachea deviated left, absent breath sounds right."
62
- },
63
- # NEW: Septic Shock
64
- {
65
- "chief_complaint": "She's been confused all day and now she's going cold and clammy.",
66
- "vitals": {"hr": 125, "bp_sys": 70, "bp_dia": 40, "spo2": 88, "rr": 30, "temp": 39.8, "avpu": "V"},
67
- "history": "78yo female, recent UTI, mottled skin, delayed capillary refill, drowsy."
68
- },
69
- # NEW: Major Trauma - MVA
70
- {
71
- "chief_complaint": "He was thrown from the car... he's not making sense!",
72
- "vitals": {"hr": 130, "bp_sys": 85, "bp_dia": 50, "spo2": 90, "rr": 32, "temp": 35.5, "avpu": "V"},
73
- "history": "35yo male, unrestrained driver, high-speed RTC, GCS 9, deformed left femur."
74
- },
75
- # NEW: Massive GI Bleed
76
- {
77
- "chief_complaint": "I've vomited loads of blood and now I feel faint...",
78
- "vitals": {"hr": 140, "bp_sys": 75, "bp_dia": 45, "spo2": 94, "rr": 26, "temp": 36.2, "avpu": "V"},
79
- "history": "55yo male, known liver disease, haematemesis x3, melena, pale and diaphoretic."
80
- },
81
- # NEW: Status Epilepticus
82
- {
83
- "chief_complaint": "My daughter won't stop fitting! It's been going on for ages!",
84
- "vitals": {"hr": 145, "bp_sys": 160, "bp_dia": 95, "spo2": 82, "rr": 8, "temp": 38.5, "avpu": "U"},
85
- "history": "8yo female, known epilepsy, continuous tonic-clonic seizure for 12 minutes, cyanosed."
86
- },
87
- # NEW: DKA with Coma
88
- {
89
- "chief_complaint": "He's a diabetic and now he won't wake up properly.",
90
- "vitals": {"hr": 115, "bp_sys": 90, "bp_dia": 55, "spo2": 95, "rr": 35, "temp": 37.2, "avpu": "P"},
91
- "history": "22yo male, Type 1 diabetic, Kussmaul breathing, fruity breath, missed insulin for 3 days."
92
- },
93
- # NEW: Eclampsia
94
- {
95
- "chief_complaint": "She's 8 months pregnant and started fitting!",
96
- "vitals": {"hr": 120, "bp_sys": 180, "bp_dia": 120, "spo2": 89, "rr": 24, "temp": 37.5, "avpu": "U"},
97
- "history": "32yo female, 34 weeks pregnant, seizure witnessed, severe headache and visual disturbance earlier."
98
- },
99
- # NEW: Complete Airway Obstruction
100
- {
101
- "chief_complaint": "He was eating steak and now he can't breathe at all!",
102
- "vitals": {"hr": 140, "bp_sys": 160, "bp_dia": 100, "spo2": 65, "rr": 0, "temp": 37.0, "avpu": "A"},
103
- "history": "60yo male, choking at restaurant, cannot cough or speak, universal choking sign, cyanotic."
104
- },
105
- # NEW: Haemorrhagic Stroke (SAH)
106
- {
107
- "chief_complaint": "It's the worst headache of my life... like being hit with a hammer.",
108
- "vitals": {"hr": 55, "bp_sys": 200, "bp_dia": 115, "spo2": 94, "rr": 14, "temp": 37.3, "avpu": "V"},
109
- "history": "48yo female, sudden onset thunderclap headache, vomiting, photophobia, now drowsy."
110
- },
111
- # NEW: Ruptured AAA
112
- {
113
- "chief_complaint": "The pain in my back is unbearable... I feel like I'm going to pass out.",
114
- "vitals": {"hr": 125, "bp_sys": 75, "bp_dia": 50, "spo2": 93, "rr": 28, "temp": 36.0, "avpu": "V"},
115
- "history": "72yo male, known AAA, sudden severe abdominal/back pain, pulsatile mass, pale and sweating."
116
- },
117
- # NEW: Atypical ACS (Elderly)
118
- {
119
- "chief_complaint": "I just feel really unwell... something is very wrong.",
120
- "vitals": {"hr": 45, "bp_sys": 80, "bp_dia": 50, "spo2": 88, "rr": 24, "temp": 36.5, "avpu": "V"},
121
- "history": "82yo female, known diabetes, vague malaise, nausea, cool and clammy, no chest pain."
122
- },
123
- # NEW: Meningococcal Septicaemia
124
- {
125
- "chief_complaint": "My child has a rash that won't go away when I press it!",
126
- "vitals": {"hr": 180, "bp_sys": 70, "bp_dia": 40, "spo2": 90, "rr": 45, "temp": 40.2, "avpu": "V"},
127
- "history": "4yo male, non-blanching purpuric rash, headache, photophobia, neck stiffness, drowsy."
128
- },
129
- # NEW: Drowning/Near-drowning
130
- {
131
- "chief_complaint": "He was pulled from the pool and he's not breathing properly!",
132
- "vitals": {"hr": 50, "bp_sys": 70, "bp_dia": 45, "spo2": 60, "rr": 6, "temp": 34.0, "avpu": "U"},
133
- "history": "6yo male, submersion ~5 mins, bystander CPR given, now gasping, hypothermic."
134
- },
135
- # NEW: Severe Burns with Inhalation
136
- {
137
- "chief_complaint": "She was in a house fire... her voice sounds strange now.",
138
- "vitals": {"hr": 130, "bp_sys": 90, "bp_dia": 55, "spo2": 85, "rr": 32, "temp": 37.5, "avpu": "A"},
139
- "history": "45yo female, facial burns, singed nasal hairs, hoarse voice, stridor developing."
140
- },
141
- ],
142
-
143
- # Category 2: Very Urgent (Orange) - Time critical
144
- 2: [
145
- {
146
- "chief_complaint": "I have the worst headache of my life. It came on suddenly.",
147
- "vitals": {"hr": 90, "bp_sys": 180, "bp_dia": 100, "spo2": 97, "rr": 18, "temp": 37.2, "avpu": "A"},
148
- "history": "45yo female, sudden onset occipital headache, photophobia, neck stiffness."
149
- },
150
- {
151
- "chief_complaint": "My little boy is having a fit and won't stop!",
152
- "vitals": {"hr": 150, "bp_sys": 90, "bp_dia": 55, "spo2": 90, "rr": 24, "temp": 39.5, "avpu": "U"},
153
- "history": "3yo male, febrile seizure ongoing for 8 minutes."
154
- },
155
- {
156
- "chief_complaint": "I fell and I can't feel my legs.",
157
- "vitals": {"hr": 100, "bp_sys": 140, "bp_dia": 85, "spo2": 98, "rr": 20, "temp": 36.8, "avpu": "A"},
158
- "history": "55yo male, fell from ladder, complaining of neck pain, no sensation below T4."
159
- },
160
- ],
161
-
162
- # Category 3: Urgent (Yellow)
163
- 3: [
164
- {
165
- "chief_complaint": "I've had abdominal pain for 2 days. It's getting worse and I'm vomiting.",
166
- "vitals": {"hr": 105, "bp_sys": 110, "bp_dia": 70, "spo2": 97, "rr": 20, "temp": 38.2, "avpu": "A"},
167
- "history": "32yo female, RIF pain, guarding, rebound tenderness."
168
- },
169
- {
170
- "chief_complaint": "I've been short of breath for a few days. It's worse when I walk.",
171
- "vitals": {"hr": 95, "bp_sys": 125, "bp_dia": 80, "spo2": 92, "rr": 24, "temp": 37.0, "avpu": "A"},
172
- "history": "70yo male, COPD, productive cough, increased work of breathing."
173
- },
174
- {
175
- "chief_complaint": "I cut my hand on a knife. It won't stop bleeding.",
176
- "vitals": {"hr": 88, "bp_sys": 130, "bp_dia": 82, "spo2": 99, "rr": 16, "temp": 36.9, "avpu": "A"},
177
- "history": "40yo male, deep laceration to palm, tendon visible, bleeding controlled with pressure."
178
- },
179
- ],
180
-
181
- # Category 4: Standard (Green)
182
- 4: [
183
- {
184
- "chief_complaint": "I've had a sore throat and cough for 3 days.",
185
- "vitals": {"hr": 78, "bp_sys": 120, "bp_dia": 75, "spo2": 99, "rr": 14, "temp": 37.8, "avpu": "A"},
186
- "history": "25yo female, coryzal symptoms, no difficulty swallowing, eating and drinking well."
187
- },
188
- {
189
- "chief_complaint": "I twisted my ankle playing football yesterday.",
190
- "vitals": {"hr": 72, "bp_sys": 118, "bp_dia": 72, "spo2": 99, "rr": 14, "temp": 36.8, "avpu": "A"},
191
- "history": "22yo male, swollen lateral ankle, can weight bear with pain, no deformity."
192
- },
193
- {
194
- "chief_complaint": "I've had diarrhoea and vomiting since last night.",
195
- "vitals": {"hr": 85, "bp_sys": 115, "bp_dia": 70, "spo2": 98, "rr": 16, "temp": 37.5, "avpu": "A"},
196
- "history": "35yo female, kept down fluids this morning, passing urine, no blood in stool."
197
- },
198
- ],
199
-
200
- # Category 5: Non-urgent (Blue)
201
- 5: [
202
- {
203
- "chief_complaint": "I need a repeat prescription for my blood pressure tablets.",
204
- "vitals": {"hr": 70, "bp_sys": 135, "bp_dia": 85, "spo2": 99, "rr": 14, "temp": 36.7, "avpu": "A"},
205
- "history": "60yo male, ran out of Amlodipine, asymptomatic."
206
- },
207
- {
208
- "chief_complaint": "I've had a rash on my arm for a week. It's itchy.",
209
- "vitals": {"hr": 68, "bp_sys": 120, "bp_dia": 78, "spo2": 99, "rr": 14, "temp": 36.8, "avpu": "A"},
210
- "history": "30yo female, localised erythematous rash, no systemic symptoms, not spreading."
211
- },
212
- {
213
- "chief_complaint": "I just want my sick note signing.",
214
- "vitals": {"hr": 72, "bp_sys": 122, "bp_dia": 80, "spo2": 99, "rr": 14, "temp": 36.8, "avpu": "A"},
215
- "history": "45yo male, recovering from back strain, no red flags."
216
- },
217
- ],
218
- }
219
-
220
-
221
- class PatientGenerator:
222
- """Generates patient scenarios for the Triage environment."""
223
-
224
- def __init__(self, seed: Optional[int] = None):
225
- if seed is not None:
226
- random.seed(seed)
227
- self._patient_count = 0
228
-
229
- def generate(self, category: Optional[int] = None) -> Patient:
230
- """
231
- Generate a random patient.
232
-
233
- Args:
234
- category: Optional specific category (1-5). If None, weighted random selection.
235
-
236
- Returns:
237
- A Patient object.
238
- """
239
- if category is None:
240
- # Weighted distribution mimicking real A&E (more Cat 3-4 than Cat 1)
241
- weights = [5, 15, 35, 35, 10] # % distribution
242
- category = random.choices([1, 2, 3, 4, 5], weights=weights)[0]
243
-
244
- scenario = random.choice(SCENARIOS[category])
245
- self._patient_count += 1
246
-
247
- # Add some noise to vitals
248
- noisy_vitals = {
249
- k: v + random.gauss(0, v * 0.05) if isinstance(v, float) else v
250
- for k, v in scenario["vitals"].items()
251
- }
252
-
253
- return Patient(
254
- id=f"P{self._patient_count:04d}",
255
- chief_complaint=scenario["chief_complaint"],
256
- vitals=noisy_vitals,
257
- history=scenario["history"],
258
- true_category=category,
259
- time_arrived=0, # Will be set by environment
260
- )
261
-
262
- def generate_batch(self, n: int) -> List[Patient]:
263
- """Generate a batch of n patients."""
264
- return [self.generate() for _ in range(n)]
265
-
266
-
267
- if __name__ == "__main__":
268
- # Quick test
269
- gen = PatientGenerator(seed=42)
270
- for _ in range(5):
271
- patient = gen.generate()
272
- print(f"{patient.id}: Cat {patient.true_category} - {patient.chief_complaint[:50]}...")
 
1
+ """
2
+ Patient Generator for NurseSim-RL
3
+
4
+ Generates synthetic patient scenarios based on Manchester Triage System categories.
5
+ """
6
+
7
+ import random
8
+ from dataclasses import dataclass
9
+ from typing import Dict, List, Optional
10
+
11
+
12
+ @dataclass
13
+ class Patient:
14
+ """Represents a patient presenting to A&E."""
15
+ id: str
16
+ chief_complaint: str
17
+ vitals: Dict[str, float]
18
+ history: str
19
+ true_category: int # 1-5 (Ground truth for reward calculation)
20
+ time_arrived: int
21
+
22
+
23
+ # Manchester Triage System Scenarios
24
+ # Gold-standard scenarios validated against MTS discriminators and clinical guidelines
25
+ SCENARIOS = {
26
+ # Category 1: Immediate (Red) - Life threatening
27
+ # MTS Discriminators: Airway compromise, Inadequate breathing, Shock, Unresponsive
28
+ 1: [
29
+ # Original scenarios
30
+ {
31
+ "chief_complaint": "I can't breathe... my chest is crushing... the pain goes down my arm.",
32
+ "vitals": {"hr": 120, "bp_sys": 85, "bp_dia": 50, "spo2": 88, "rr": 32, "temp": 36.5, "avpu": "V"},
33
+ "history": "65yo male, known cardiac history, sudden onset 20 mins ago."
34
+ },
35
+ {
36
+ "chief_complaint": "He collapsed and isn't responding to me!",
37
+ "vitals": {"hr": 0, "bp_sys": 0, "bp_dia": 0, "spo2": 0, "rr": 0, "temp": 35.0, "avpu": "U"},
38
+ "history": "72yo male found unresponsive by wife. Bystander CPR in progress."
39
+ },
40
+ {
41
+ "chief_complaint": "My face is swelling up and I can't swallow... I ate shellfish.",
42
+ "vitals": {"hr": 130, "bp_sys": 70, "bp_dia": 40, "spo2": 85, "rr": 28, "temp": 37.0, "avpu": "A"},
43
+ "history": "28yo female, known shellfish allergy, stridor audible."
44
+ },
45
+ # NEW: STEMI - Classic presentation
46
+ {
47
+ "chief_complaint": "There's an elephant sitting on my chest... I feel like I'm going to die.",
48
+ "vitals": {"hr": 110, "bp_sys": 80, "bp_dia": 55, "spo2": 91, "rr": 28, "temp": 36.8, "avpu": "A"},
49
+ "history": "58yo male, sweating profusely, nausea, pain radiating to jaw and left arm, 30 mins onset."
50
+ },
51
+ # NEW: Acute Stroke - FAST positive
52
+ {
53
+ "chief_complaint": "My husband's face has dropped and he can't speak properly!",
54
+ "vitals": {"hr": 95, "bp_sys": 195, "bp_dia": 110, "spo2": 96, "rr": 18, "temp": 37.0, "avpu": "V"},
55
+ "history": "70yo male, sudden onset 45 mins ago, right-sided weakness, slurred speech, facial droop."
56
+ },
57
+ # NEW: Tension Pneumothorax
58
+ {
59
+ "chief_complaint": "I was stabbed... I can't breathe... everything is going dark.",
60
+ "vitals": {"hr": 135, "bp_sys": 75, "bp_dia": 45, "spo2": 78, "rr": 40, "temp": 36.5, "avpu": "V"},
61
+ "history": "25yo male, stab wound to right chest, trachea deviated left, absent breath sounds right."
62
+ },
63
+ # NEW: Septic Shock
64
+ {
65
+ "chief_complaint": "She's been confused all day and now she's going cold and clammy.",
66
+ "vitals": {"hr": 125, "bp_sys": 70, "bp_dia": 40, "spo2": 88, "rr": 30, "temp": 39.8, "avpu": "V"},
67
+ "history": "78yo female, recent UTI, mottled skin, delayed capillary refill, drowsy."
68
+ },
69
+ # NEW: Major Trauma - MVA
70
+ {
71
+ "chief_complaint": "He was thrown from the car... he's not making sense!",
72
+ "vitals": {"hr": 130, "bp_sys": 85, "bp_dia": 50, "spo2": 90, "rr": 32, "temp": 35.5, "avpu": "V"},
73
+ "history": "35yo male, unrestrained driver, high-speed RTC, GCS 9, deformed left femur."
74
+ },
75
+ # NEW: Massive GI Bleed
76
+ {
77
+ "chief_complaint": "I've vomited loads of blood and now I feel faint...",
78
+ "vitals": {"hr": 140, "bp_sys": 75, "bp_dia": 45, "spo2": 94, "rr": 26, "temp": 36.2, "avpu": "V"},
79
+ "history": "55yo male, known liver disease, haematemesis x3, melena, pale and diaphoretic."
80
+ },
81
+ # NEW: Status Epilepticus
82
+ {
83
+ "chief_complaint": "My daughter won't stop fitting! It's been going on for ages!",
84
+ "vitals": {"hr": 145, "bp_sys": 160, "bp_dia": 95, "spo2": 82, "rr": 8, "temp": 38.5, "avpu": "U"},
85
+ "history": "8yo female, known epilepsy, continuous tonic-clonic seizure for 12 minutes, cyanosed."
86
+ },
87
+ # NEW: DKA with Coma
88
+ {
89
+ "chief_complaint": "He's a diabetic and now he won't wake up properly.",
90
+ "vitals": {"hr": 115, "bp_sys": 90, "bp_dia": 55, "spo2": 95, "rr": 35, "temp": 37.2, "avpu": "P"},
91
+ "history": "22yo male, Type 1 diabetic, Kussmaul breathing, fruity breath, missed insulin for 3 days."
92
+ },
93
+ # NEW: Eclampsia
94
+ {
95
+ "chief_complaint": "She's 8 months pregnant and started fitting!",
96
+ "vitals": {"hr": 120, "bp_sys": 180, "bp_dia": 120, "spo2": 89, "rr": 24, "temp": 37.5, "avpu": "U"},
97
+ "history": "32yo female, 34 weeks pregnant, seizure witnessed, severe headache and visual disturbance earlier."
98
+ },
99
+ # NEW: Complete Airway Obstruction
100
+ {
101
+ "chief_complaint": "He was eating steak and now he can't breathe at all!",
102
+ "vitals": {"hr": 140, "bp_sys": 160, "bp_dia": 100, "spo2": 65, "rr": 0, "temp": 37.0, "avpu": "A"},
103
+ "history": "60yo male, choking at restaurant, cannot cough or speak, universal choking sign, cyanotic."
104
+ },
105
+ # NEW: Haemorrhagic Stroke (SAH)
106
+ {
107
+ "chief_complaint": "It's the worst headache of my life... like being hit with a hammer.",
108
+ "vitals": {"hr": 55, "bp_sys": 200, "bp_dia": 115, "spo2": 94, "rr": 14, "temp": 37.3, "avpu": "V"},
109
+ "history": "48yo female, sudden onset thunderclap headache, vomiting, photophobia, now drowsy."
110
+ },
111
+ # NEW: Ruptured AAA
112
+ {
113
+ "chief_complaint": "The pain in my back is unbearable... I feel like I'm going to pass out.",
114
+ "vitals": {"hr": 125, "bp_sys": 75, "bp_dia": 50, "spo2": 93, "rr": 28, "temp": 36.0, "avpu": "V"},
115
+ "history": "72yo male, known AAA, sudden severe abdominal/back pain, pulsatile mass, pale and sweating."
116
+ },
117
+ # NEW: Atypical ACS (Elderly)
118
+ {
119
+ "chief_complaint": "I just feel really unwell... something is very wrong.",
120
+ "vitals": {"hr": 45, "bp_sys": 80, "bp_dia": 50, "spo2": 88, "rr": 24, "temp": 36.5, "avpu": "V"},
121
+ "history": "82yo female, known diabetes, vague malaise, nausea, cool and clammy, no chest pain."
122
+ },
123
+ # NEW: Meningococcal Septicaemia
124
+ {
125
+ "chief_complaint": "My child has a rash that won't go away when I press it!",
126
+ "vitals": {"hr": 180, "bp_sys": 70, "bp_dia": 40, "spo2": 90, "rr": 45, "temp": 40.2, "avpu": "V"},
127
+ "history": "4yo male, non-blanching purpuric rash, headache, photophobia, neck stiffness, drowsy."
128
+ },
129
+ # NEW: Drowning/Near-drowning
130
+ {
131
+ "chief_complaint": "He was pulled from the pool and he's not breathing properly!",
132
+ "vitals": {"hr": 50, "bp_sys": 70, "bp_dia": 45, "spo2": 60, "rr": 6, "temp": 34.0, "avpu": "U"},
133
+ "history": "6yo male, submersion ~5 mins, bystander CPR given, now gasping, hypothermic."
134
+ },
135
+ # NEW: Severe Burns with Inhalation
136
+ {
137
+ "chief_complaint": "She was in a house fire... her voice sounds strange now.",
138
+ "vitals": {"hr": 130, "bp_sys": 90, "bp_dia": 55, "spo2": 85, "rr": 32, "temp": 37.5, "avpu": "A"},
139
+ "history": "45yo female, facial burns, singed nasal hairs, hoarse voice, stridor developing."
140
+ },
141
+ ],
142
+
143
+ # Category 2: Very Urgent (Orange) - Time critical
144
+ 2: [
145
+ {
146
+ "chief_complaint": "I have the worst headache of my life. It came on suddenly.",
147
+ "vitals": {"hr": 90, "bp_sys": 180, "bp_dia": 100, "spo2": 97, "rr": 18, "temp": 37.2, "avpu": "A"},
148
+ "history": "45yo female, sudden onset occipital headache, photophobia, neck stiffness."
149
+ },
150
+ {
151
+ "chief_complaint": "My little boy is having a fit and won't stop!",
152
+ "vitals": {"hr": 150, "bp_sys": 90, "bp_dia": 55, "spo2": 90, "rr": 24, "temp": 39.5, "avpu": "U"},
153
+ "history": "3yo male, febrile seizure ongoing for 8 minutes."
154
+ },
155
+ {
156
+ "chief_complaint": "I fell and I can't feel my legs.",
157
+ "vitals": {"hr": 100, "bp_sys": 140, "bp_dia": 85, "spo2": 98, "rr": 20, "temp": 36.8, "avpu": "A"},
158
+ "history": "55yo male, fell from ladder, complaining of neck pain, no sensation below T4."
159
+ },
160
+ ],
161
+
162
+ # Category 3: Urgent (Yellow)
163
+ 3: [
164
+ {
165
+ "chief_complaint": "I've had abdominal pain for 2 days. It's getting worse and I'm vomiting.",
166
+ "vitals": {"hr": 105, "bp_sys": 110, "bp_dia": 70, "spo2": 97, "rr": 20, "temp": 38.2, "avpu": "A"},
167
+ "history": "32yo female, RIF pain, guarding, rebound tenderness."
168
+ },
169
+ {
170
+ "chief_complaint": "I've been short of breath for a few days. It's worse when I walk.",
171
+ "vitals": {"hr": 95, "bp_sys": 125, "bp_dia": 80, "spo2": 92, "rr": 24, "temp": 37.0, "avpu": "A"},
172
+ "history": "70yo male, COPD, productive cough, increased work of breathing."
173
+ },
174
+ {
175
+ "chief_complaint": "I cut my hand on a knife. It won't stop bleeding.",
176
+ "vitals": {"hr": 88, "bp_sys": 130, "bp_dia": 82, "spo2": 99, "rr": 16, "temp": 36.9, "avpu": "A"},
177
+ "history": "40yo male, deep laceration to palm, tendon visible, bleeding controlled with pressure."
178
+ },
179
+ ],
180
+
181
+ # Category 4: Standard (Green)
182
+ 4: [
183
+ {
184
+ "chief_complaint": "I've had a sore throat and cough for 3 days.",
185
+ "vitals": {"hr": 78, "bp_sys": 120, "bp_dia": 75, "spo2": 99, "rr": 14, "temp": 37.8, "avpu": "A"},
186
+ "history": "25yo female, coryzal symptoms, no difficulty swallowing, eating and drinking well."
187
+ },
188
+ {
189
+ "chief_complaint": "I twisted my ankle playing football yesterday.",
190
+ "vitals": {"hr": 72, "bp_sys": 118, "bp_dia": 72, "spo2": 99, "rr": 14, "temp": 36.8, "avpu": "A"},
191
+ "history": "22yo male, swollen lateral ankle, can weight bear with pain, no deformity."
192
+ },
193
+ {
194
+ "chief_complaint": "I've had diarrhoea and vomiting since last night.",
195
+ "vitals": {"hr": 85, "bp_sys": 115, "bp_dia": 70, "spo2": 98, "rr": 16, "temp": 37.5, "avpu": "A"},
196
+ "history": "35yo female, kept down fluids this morning, passing urine, no blood in stool."
197
+ },
198
+ ],
199
+
200
+ # Category 5: Non-urgent (Blue)
201
+ 5: [
202
+ {
203
+ "chief_complaint": "I need a repeat prescription for my blood pressure tablets.",
204
+ "vitals": {"hr": 70, "bp_sys": 135, "bp_dia": 85, "spo2": 99, "rr": 14, "temp": 36.7, "avpu": "A"},
205
+ "history": "60yo male, ran out of Amlodipine, asymptomatic."
206
+ },
207
+ {
208
+ "chief_complaint": "I've had a rash on my arm for a week. It's itchy.",
209
+ "vitals": {"hr": 68, "bp_sys": 120, "bp_dia": 78, "spo2": 99, "rr": 14, "temp": 36.8, "avpu": "A"},
210
+ "history": "30yo female, localised erythematous rash, no systemic symptoms, not spreading."
211
+ },
212
+ {
213
+ "chief_complaint": "I just want my sick note signing.",
214
+ "vitals": {"hr": 72, "bp_sys": 122, "bp_dia": 80, "spo2": 99, "rr": 14, "temp": 36.8, "avpu": "A"},
215
+ "history": "45yo male, recovering from back strain, no red flags."
216
+ },
217
+ ],
218
+ }
219
+
220
+
221
+ class PatientGenerator:
222
+ """Generates patient scenarios for the Triage environment."""
223
+
224
+ def __init__(self, seed: Optional[int] = None):
225
+ if seed is not None:
226
+ random.seed(seed)
227
+ self._patient_count = 0
228
+
229
+ def generate(self, category: Optional[int] = None) -> Patient:
230
+ """
231
+ Generate a random patient.
232
+
233
+ Args:
234
+ category: Optional specific category (1-5). If None, weighted random selection.
235
+
236
+ Returns:
237
+ A Patient object.
238
+ """
239
+ if category is None:
240
+ # Weighted distribution mimicking real A&E (more Cat 3-4 than Cat 1)
241
+ weights = [5, 15, 35, 35, 10] # % distribution
242
+ category = random.choices([1, 2, 3, 4, 5], weights=weights)[0]
243
+
244
+ scenario = random.choice(SCENARIOS[category])
245
+ self._patient_count += 1
246
+
247
+ # Add some noise to vitals
248
+ noisy_vitals = {
249
+ k: v + random.gauss(0, v * 0.05) if isinstance(v, float) else v
250
+ for k, v in scenario["vitals"].items()
251
+ }
252
+
253
+ return Patient(
254
+ id=f"P{self._patient_count:04d}",
255
+ chief_complaint=scenario["chief_complaint"],
256
+ vitals=noisy_vitals,
257
+ history=scenario["history"],
258
+ true_category=category,
259
+ time_arrived=0, # Will be set by environment
260
+ )
261
+
262
+ def generate_batch(self, n: int) -> List[Patient]:
263
+ """Generate a batch of n patients."""
264
+ return [self.generate() for _ in range(n)]
265
+
266
+
267
+ if __name__ == "__main__":
268
+ # Quick test
269
+ gen = PatientGenerator(seed=42)
270
+ for _ in range(5):
271
+ patient = gen.generate()
272
+ print(f"{patient.id}: Cat {patient.true_category} - {patient.chief_complaint[:50]}...")
nursesim_rl/semantic_wrapper.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ NurseEmbedWrapper: A Gymnasium wrapper that converts text observations to NurseEmbed vectors.
3
+
4
+ This enables Language-Conditioned Reinforcement Learning for nursing scenarios.
5
+ """
6
+
7
+ import gymnasium as gym
8
+ from gymnasium import spaces
9
+ import numpy as np
10
+ from typing import Any, Dict, Tuple
11
+
12
+ # Lazy load NurseEmbed to avoid import errors if not available
13
+ _embed_model = None
14
+
15
+ def _get_embed_model():
16
+ """Lazy load the NurseEmbed model."""
17
+ global _embed_model
18
+ if _embed_model is None:
19
+ try:
20
+ from sentence_transformers import SentenceTransformer
21
+ _embed_model = SentenceTransformer("NurseCitizenDeveloper/NurseEmbed-300M")
22
+ print("[OK] NurseEmbed model loaded successfully")
23
+ except Exception as e:
24
+ print(f"[WARN] NurseEmbed not available: {e}")
25
+ # Fallback to random embeddings for testing
26
+ _embed_model = "fallback"
27
+ return _embed_model
28
+
29
+
30
+ class NurseEmbedWrapper(gym.Wrapper):
31
+ """
32
+ Wraps a TriageEnv and converts text observations to NurseEmbed vectors.
33
+ Also flattens the Dict action space to MultiDiscrete for SB3 compatibility.
34
+
35
+ Instead of the agent seeing:
36
+ {"chief_complaint": "Chest pain radiating to arm...", "vitals": {...}, ...}
37
+
38
+ It sees:
39
+ np.array([...390 dimensional semantic vector...])
40
+
41
+ The vector encodes the MEANING of the clinical presentation.
42
+ """
43
+
44
+ EMBED_DIM = 384 # NurseEmbed-300M outputs 384D vectors (nomic base)
45
+
46
+ def __init__(self, env: gym.Env, use_vitals: bool = True):
47
+ """
48
+ Args:
49
+ env: The underlying TriageEnv
50
+ use_vitals: Whether to append vitals to the embedding
51
+ """
52
+ super().__init__(env)
53
+
54
+ self.use_vitals = use_vitals
55
+ self.model = _get_embed_model()
56
+
57
+ # Calculate observation dimension
58
+ obs_dim = self.EMBED_DIM
59
+ if use_vitals:
60
+ obs_dim += 6 # HR, BP_sys, BP_dia, SpO2, RR, Temp
61
+
62
+ # Override observation space to be a flat Box
63
+ self.observation_space = spaces.Box(
64
+ low=-np.inf,
65
+ high=np.inf,
66
+ shape=(obs_dim,),
67
+ dtype=np.float32
68
+ )
69
+
70
+ # Flatten action space for SB3 compatibility
71
+ # Original: Dict({'triage_category': Discrete(5, start=1), 'intervention': Discrete(7)})
72
+ # New: MultiDiscrete([5, 7]) where first dim is category (0-4, add 1 later) and second is intervention
73
+ self.action_space = spaces.MultiDiscrete([5, 7])
74
+
75
+ # Cache for embeddings (same text -> same embedding)
76
+ self._embedding_cache: Dict[str, np.ndarray] = {}
77
+
78
+ def reset(self, **kwargs) -> Tuple[np.ndarray, Dict]:
79
+ """Reset and convert observation."""
80
+ obs, info = self.env.reset(**kwargs)
81
+ return self._convert_observation(obs), info
82
+
83
+ def step(self, action: np.ndarray) -> Tuple[np.ndarray, float, bool, bool, Dict]:
84
+ """Convert action, step, convert observation."""
85
+ # Convert flat action [category_idx, intervention_idx] to Dict
86
+ dict_action = {
87
+ "triage_category": int(action[0]) + 1, # 0-4 -> 1-5
88
+ "intervention": int(action[1])
89
+ }
90
+ obs, reward, terminated, truncated, info = self.env.step(dict_action)
91
+ return self._convert_observation(obs), reward, terminated, truncated, info
92
+
93
+ def _convert_observation(self, obs: Dict) -> np.ndarray:
94
+ """Convert Dict observation to semantic vector."""
95
+ # Build text representation
96
+ text = self._build_clinical_text(obs)
97
+
98
+ # Get embedding (with caching)
99
+ embedding = self._get_embedding(text)
100
+
101
+ # Optionally append vitals
102
+ if self.use_vitals:
103
+ vitals_vector = self._extract_vitals(obs)
104
+ embedding = np.concatenate([embedding, vitals_vector])
105
+
106
+ return embedding.astype(np.float32)
107
+
108
+ def _build_clinical_text(self, obs: Dict) -> str:
109
+ """Build a clinical description from the observation."""
110
+ complaint = obs.get("chief_complaint", "Unknown complaint")
111
+ history = obs.get("history", "")
112
+
113
+ # Create a rich clinical description
114
+ text = f"Patient presents with: {complaint}. Clinical history: {history}."
115
+
116
+ # Add vitals context as text for semantic understanding
117
+ vitals = obs.get("vitals", {})
118
+ if vitals:
119
+ text += f" Vital signs: HR {vitals.get('hr', 'N/A')}, "
120
+ text += f"BP {vitals.get('bp_sys', 'N/A')}/{vitals.get('bp_dia', 'N/A')}, "
121
+ text += f"SpO2 {vitals.get('spo2', 'N/A')}%, "
122
+ text += f"RR {vitals.get('rr', 'N/A')}, "
123
+ text += f"AVPU {vitals.get('avpu', 'A')}."
124
+
125
+ return text
126
+
127
+ def _get_embedding(self, text: str) -> np.ndarray:
128
+ """Get embedding with caching."""
129
+ if text in self._embedding_cache:
130
+ return self._embedding_cache[text]
131
+
132
+ if self.model == "fallback":
133
+ # Fallback: deterministic pseudo-random embedding based on text hash
134
+ np.random.seed(hash(text) % 2**32)
135
+ embedding = np.random.randn(self.EMBED_DIM)
136
+ else:
137
+ embedding = self.model.encode(text, normalize_embeddings=True)
138
+
139
+ self._embedding_cache[text] = embedding
140
+ return embedding
141
+
142
+ def _extract_vitals(self, obs: Dict) -> np.ndarray:
143
+ """Extract vitals as a normalized vector."""
144
+ vitals = obs.get("vitals", {})
145
+ return np.array([
146
+ vitals.get("hr", 70) / 200.0, # Normalize HR
147
+ vitals.get("bp_sys", 120) / 200.0,
148
+ vitals.get("bp_dia", 80) / 150.0,
149
+ vitals.get("spo2", 98) / 100.0,
150
+ vitals.get("rr", 16) / 40.0,
151
+ (vitals.get("temp", 37.0) - 35) / 5.0, # Normalize temp
152
+ ], dtype=np.float32)
153
+
154
+
155
+ def make_semantic_triage_env(seed: int = None, **kwargs) -> gym.Env:
156
+ """Factory function to create a semantically-aware triage environment."""
157
+ from nursesim_rl import TriageEnv
158
+
159
+ base_env = TriageEnv(seed=seed, **kwargs)
160
+ wrapped_env = NurseEmbedWrapper(base_env, use_vitals=True)
161
+
162
+ return wrapped_env
163
+
164
+
165
+ # Register wrapped version
166
+ gym.register(
167
+ id="NurseSim-SemanticTriage-v0",
168
+ entry_point="nursesim_rl.semantic_wrapper:make_semantic_triage_env",
169
+ )
nursesim_rl/triage_env.py CHANGED
@@ -1,303 +1,303 @@
1
- """
2
- TriageEnv: A Gymnasium-compatible RL environment for A&E Triage.
3
-
4
- OpenEnv Challenge Entry - 2026
5
- """
6
-
7
- import gymnasium as gym
8
- from gymnasium import spaces
9
- import numpy as np
10
- from typing import Any, Dict, Optional, Tuple
11
-
12
- from .patient_generator import PatientGenerator, Patient
13
-
14
-
15
- class TriageEnv(gym.Env):
16
- """
17
- A&E Triage Environment.
18
-
19
- The agent plays the role of a Triage Nurse, assessing patients and
20
- assigning them to the correct Manchester Triage System category.
21
-
22
- Observation:
23
- - patient_complaint (str): The patient's chief complaint
24
- - vitals (dict): HR, BP, SpO2, RR, Temp, AVPU
25
- - history (str): Brief clinical history
26
- - waiting_room (int): Number of patients currently waiting
27
- - available_beds (int): Beds available in Resus/Majors
28
-
29
- Action:
30
- - triage_category (int): 1-5 (Immediate to Non-urgent)
31
- - intervention (str): One of the allowed interventions
32
-
33
- Reward:
34
- - +10 for correct triage category
35
- - +5 for adjacent category (within 1)
36
- - -50 for critical safety failure (under-triaging P1/P2 by 2+ levels)
37
- - -1 per minute waiting for high-acuity patients
38
- """
39
-
40
- metadata = {"render_modes": ["human", "ansi"], "render_fps": 1}
41
-
42
- INTERVENTIONS = [
43
- "send_to_resus",
44
- "send_to_majors",
45
- "send_to_minors",
46
- "order_ecg",
47
- "give_analgesia",
48
- "discharge",
49
- "refer_to_gp",
50
- ]
51
-
52
- def __init__(
53
- self,
54
- max_patients: int = 20,
55
- max_steps: int = 50,
56
- render_mode: Optional[str] = None,
57
- seed: Optional[int] = None,
58
- ):
59
- super().__init__()
60
-
61
- self.max_patients = max_patients
62
- self.max_steps = max_steps
63
- self.render_mode = render_mode
64
-
65
- self.patient_generator = PatientGenerator(seed=seed)
66
-
67
- # Action space: Discrete triage category + intervention
68
- self.action_space = spaces.Dict({
69
- "triage_category": spaces.Discrete(5, start=1), # 1-5
70
- "intervention": spaces.Discrete(len(self.INTERVENTIONS)),
71
- })
72
-
73
- # Observation space
74
- self.observation_space = spaces.Dict({
75
- "patient_id": spaces.Text(10),
76
- "chief_complaint": spaces.Text(500),
77
- "vitals": spaces.Dict({
78
- "hr": spaces.Box(0, 300, shape=(), dtype=np.float32),
79
- "bp_sys": spaces.Box(0, 300, shape=(), dtype=np.float32),
80
- "bp_dia": spaces.Box(0, 200, shape=(), dtype=np.float32),
81
- "spo2": spaces.Box(0, 100, shape=(), dtype=np.float32),
82
- "rr": spaces.Box(0, 60, shape=(), dtype=np.float32),
83
- "temp": spaces.Box(30, 45, shape=(), dtype=np.float32),
84
- "avpu": spaces.Text(1),
85
- }),
86
- "history": spaces.Text(500),
87
- "waiting_room": spaces.Discrete(100),
88
- "available_beds": spaces.Discrete(20),
89
- })
90
-
91
- # State
92
- self.current_patient: Optional[Patient] = None
93
- self.waiting_queue: list = []
94
- self.step_count: int = 0
95
- self.total_reward: float = 0.0
96
- self.available_beds: int = 10
97
- self.episode_stats: Dict[str, Any] = {}
98
-
99
- def reset(
100
- self,
101
- seed: Optional[int] = None,
102
- options: Optional[Dict] = None,
103
- ) -> Tuple[Dict, Dict]:
104
- """Reset the environment to initial state."""
105
- super().reset(seed=seed)
106
-
107
- if seed is not None:
108
- self.patient_generator = PatientGenerator(seed=seed)
109
-
110
- # Reset state
111
- self.step_count = 0
112
- self.total_reward = 0.0
113
- self.available_beds = 10
114
- self.episode_stats = {
115
- "correct_triage": 0,
116
- "safety_failures": 0,
117
- "patients_seen": 0,
118
- }
119
-
120
- # Generate initial waiting room
121
- initial_patients = np.random.randint(3, 8)
122
- self.waiting_queue = self.patient_generator.generate_batch(initial_patients)
123
- for i, p in enumerate(self.waiting_queue):
124
- p.time_arrived = -i * 5 # Stagger arrival times
125
-
126
- # Get first patient
127
- self.current_patient = self._get_next_patient()
128
-
129
- return self._get_observation(), self._get_info()
130
-
131
- def step(self, action: Dict) -> Tuple[Dict, float, bool, bool, Dict]:
132
- """
133
- Execute one step in the environment.
134
-
135
- Args:
136
- action: Dict with 'triage_category' (1-5) and 'intervention' (index)
137
-
138
- Returns:
139
- observation, reward, terminated, truncated, info
140
- """
141
- self.step_count += 1
142
-
143
- if self.current_patient is None:
144
- # No more patients - episode ends
145
- return self._get_observation(), 0.0, True, False, self._get_info()
146
-
147
- # Parse action
148
- assigned_category = action.get("triage_category", 3)
149
- intervention_idx = action.get("intervention", 0)
150
- intervention = self.INTERVENTIONS[intervention_idx]
151
-
152
- # Calculate reward
153
- reward = self._calculate_reward(assigned_category, intervention)
154
- self.total_reward += reward
155
- self.episode_stats["patients_seen"] += 1
156
-
157
- # Update bed availability based on intervention
158
- if intervention in ["send_to_resus", "send_to_majors"]:
159
- self.available_beds = max(0, self.available_beds - 1)
160
- elif intervention in ["discharge", "refer_to_gp"]:
161
- self.available_beds = min(10, self.available_beds + 1)
162
-
163
- # Possibly add new patients to queue
164
- if np.random.random() < 0.3: # 30% chance of new arrival
165
- new_patient = self.patient_generator.generate()
166
- new_patient.time_arrived = self.step_count
167
- self.waiting_queue.append(new_patient)
168
-
169
- # Get next patient
170
- self.current_patient = self._get_next_patient()
171
-
172
- # Check termination
173
- terminated = self.current_patient is None and len(self.waiting_queue) == 0
174
- truncated = self.step_count >= self.max_steps
175
-
176
- return self._get_observation(), reward, terminated, truncated, self._get_info()
177
-
178
- def _calculate_reward(self, assigned_category: int, intervention: str) -> float:
179
- """Calculate reward based on triage decision."""
180
- if self.current_patient is None:
181
- return 0.0
182
-
183
- true_category = self.current_patient.true_category
184
- category_diff = abs(assigned_category - true_category)
185
-
186
- reward = 0.0
187
-
188
- # Category accuracy
189
- if category_diff == 0:
190
- reward += 10.0
191
- self.episode_stats["correct_triage"] += 1
192
- elif category_diff == 1:
193
- reward += 5.0 # Close enough
194
- else:
195
- reward -= 5.0 * category_diff # Penalty scales with error
196
-
197
- # Critical safety failure: Under-triaging a critical patient
198
- if true_category <= 2 and assigned_category >= true_category + 2:
199
- reward -= 50.0
200
- self.episode_stats["safety_failures"] += 1
201
-
202
- # Intervention appropriateness
203
- if true_category == 1 and intervention == "send_to_resus":
204
- reward += 5.0
205
- elif true_category == 5 and intervention in ["discharge", "refer_to_gp"]:
206
- reward += 3.0
207
- elif true_category == 1 and intervention == "discharge":
208
- reward -= 30.0 # Never discharge a P1!
209
-
210
- return reward
211
-
212
- def _get_next_patient(self) -> Optional[Patient]:
213
- """Get the next patient from the queue (FIFO with priority override)."""
214
- if not self.waiting_queue:
215
- return None
216
-
217
- # Priority override: P1 patients jump the queue
218
- for i, patient in enumerate(self.waiting_queue):
219
- if patient.true_category == 1:
220
- return self.waiting_queue.pop(i)
221
-
222
- # Otherwise FIFO
223
- return self.waiting_queue.pop(0)
224
-
225
- def _get_observation(self) -> Dict:
226
- """Build the observation dictionary."""
227
- if self.current_patient is None:
228
- return {
229
- "patient_id": "",
230
- "chief_complaint": "No patients waiting.",
231
- "vitals": {
232
- "hr": 0.0, "bp_sys": 0.0, "bp_dia": 0.0,
233
- "spo2": 0.0, "rr": 0.0, "temp": 0.0, "avpu": "A"
234
- },
235
- "history": "",
236
- "waiting_room": len(self.waiting_queue),
237
- "available_beds": self.available_beds,
238
- }
239
-
240
- return {
241
- "patient_id": self.current_patient.id,
242
- "chief_complaint": self.current_patient.chief_complaint,
243
- "vitals": {
244
- "hr": float(self.current_patient.vitals.get("hr", 0)),
245
- "bp_sys": float(self.current_patient.vitals.get("bp_sys", 0)),
246
- "bp_dia": float(self.current_patient.vitals.get("bp_dia", 0)),
247
- "spo2": float(self.current_patient.vitals.get("spo2", 0)),
248
- "rr": float(self.current_patient.vitals.get("rr", 0)),
249
- "temp": float(self.current_patient.vitals.get("temp", 0)),
250
- "avpu": str(self.current_patient.vitals.get("avpu", "A")),
251
- },
252
- "history": self.current_patient.history,
253
- "waiting_room": len(self.waiting_queue),
254
- "available_beds": self.available_beds,
255
- }
256
-
257
- def _get_info(self) -> Dict:
258
- """Return additional info."""
259
- return {
260
- "step": self.step_count,
261
- "total_reward": self.total_reward,
262
- "true_category": self.current_patient.true_category if self.current_patient else None,
263
- **self.episode_stats,
264
- }
265
-
266
- def render(self) -> Optional[str]:
267
- """Render the environment."""
268
- if self.render_mode == "human" or self.render_mode == "ansi":
269
- obs = self._get_observation()
270
- output = f"""
271
- ╔══════════════════════════════════════════════════════════════════╗
272
- ║ A&E TRIAGE SIMULATOR │ Step: {self.step_count:3d} │ Waiting: {obs['waiting_room']:2d} │ Beds: {obs['available_beds']:2d} ║
273
- ╠══════════════════════════════════════════════════════════════════╣
274
- ║ PATIENT: {obs['patient_id']:<54} ║
275
- ╠──────────────────────────────────────────────────────────────────╣
276
- ║ Chief Complaint: ║
277
- ║ "{obs['chief_complaint'][:60]:<60}" ║
278
- ╠──────────────────────────────────────────────────────────────────╣
279
- ║ VITALS: ║
280
- ║ HR: {obs['vitals']['hr']:>3.0f} │ BP: {obs['vitals']['bp_sys']:>3.0f}/{obs['vitals']['bp_dia']:<3.0f} │ SpO2: {obs['vitals']['spo2']:>3.0f}% ║
281
- ║ RR: {obs['vitals']['rr']:>3.0f} │ Temp: {obs['vitals']['temp']:.1f}°C │ AVPU: {obs['vitals']['avpu']} ║
282
- ╠──────────────────────────────────────────────────────────────────╣
283
- ║ History: {obs['history'][:55]:<55} ║
284
- ╠══════════════════════════════════════════════════════════════════╣
285
- ║ What is your triage decision? ║
286
- ║ [1] Immediate [2] Very Urgent [3] Urgent [4] Std [5] Non ║
287
- ╚══════════════════════════════════════════════════════════════════╝
288
- """
289
- if self.render_mode == "human":
290
- print(output)
291
- return output
292
- return None
293
-
294
- def close(self):
295
- """Clean up resources."""
296
- pass
297
-
298
-
299
- # Register with Gymnasium
300
- gym.register(
301
- id="NurseSim-Triage-v0",
302
- entry_point="nursesim_rl:TriageEnv",
303
- )
 
1
+ """
2
+ TriageEnv: A Gymnasium-compatible RL environment for A&E Triage.
3
+
4
+ OpenEnv Challenge Entry - 2026
5
+ """
6
+
7
+ import gymnasium as gym
8
+ from gymnasium import spaces
9
+ import numpy as np
10
+ from typing import Any, Dict, Optional, Tuple
11
+
12
+ from .patient_generator import PatientGenerator, Patient
13
+
14
+
15
+ class TriageEnv(gym.Env):
16
+ """
17
+ A&E Triage Environment.
18
+
19
+ The agent plays the role of a Triage Nurse, assessing patients and
20
+ assigning them to the correct Manchester Triage System category.
21
+
22
+ Observation:
23
+ - patient_complaint (str): The patient's chief complaint
24
+ - vitals (dict): HR, BP, SpO2, RR, Temp, AVPU
25
+ - history (str): Brief clinical history
26
+ - waiting_room (int): Number of patients currently waiting
27
+ - available_beds (int): Beds available in Resus/Majors
28
+
29
+ Action:
30
+ - triage_category (int): 1-5 (Immediate to Non-urgent)
31
+ - intervention (str): One of the allowed interventions
32
+
33
+ Reward:
34
+ - +10 for correct triage category
35
+ - +5 for adjacent category (within 1)
36
+ - -50 for critical safety failure (under-triaging P1/P2 by 2+ levels)
37
+ - -1 per minute waiting for high-acuity patients
38
+ """
39
+
40
+ metadata = {"render_modes": ["human", "ansi"], "render_fps": 1}
41
+
42
+ INTERVENTIONS = [
43
+ "send_to_resus",
44
+ "send_to_majors",
45
+ "send_to_minors",
46
+ "order_ecg",
47
+ "give_analgesia",
48
+ "discharge",
49
+ "refer_to_gp",
50
+ ]
51
+
52
+ def __init__(
53
+ self,
54
+ max_patients: int = 20,
55
+ max_steps: int = 50,
56
+ render_mode: Optional[str] = None,
57
+ seed: Optional[int] = None,
58
+ ):
59
+ super().__init__()
60
+
61
+ self.max_patients = max_patients
62
+ self.max_steps = max_steps
63
+ self.render_mode = render_mode
64
+
65
+ self.patient_generator = PatientGenerator(seed=seed)
66
+
67
+ # Action space: Discrete triage category + intervention
68
+ self.action_space = spaces.Dict({
69
+ "triage_category": spaces.Discrete(5, start=1), # 1-5
70
+ "intervention": spaces.Discrete(len(self.INTERVENTIONS)),
71
+ })
72
+
73
+ # Observation space
74
+ self.observation_space = spaces.Dict({
75
+ "patient_id": spaces.Text(10),
76
+ "chief_complaint": spaces.Text(500),
77
+ "vitals": spaces.Dict({
78
+ "hr": spaces.Box(0, 300, shape=(), dtype=np.float32),
79
+ "bp_sys": spaces.Box(0, 300, shape=(), dtype=np.float32),
80
+ "bp_dia": spaces.Box(0, 200, shape=(), dtype=np.float32),
81
+ "spo2": spaces.Box(0, 100, shape=(), dtype=np.float32),
82
+ "rr": spaces.Box(0, 60, shape=(), dtype=np.float32),
83
+ "temp": spaces.Box(30, 45, shape=(), dtype=np.float32),
84
+ "avpu": spaces.Text(1),
85
+ }),
86
+ "history": spaces.Text(500),
87
+ "waiting_room": spaces.Discrete(100),
88
+ "available_beds": spaces.Discrete(20),
89
+ })
90
+
91
+ # State
92
+ self.current_patient: Optional[Patient] = None
93
+ self.waiting_queue: list = []
94
+ self.step_count: int = 0
95
+ self.total_reward: float = 0.0
96
+ self.available_beds: int = 10
97
+ self.episode_stats: Dict[str, Any] = {}
98
+
99
+ def reset(
100
+ self,
101
+ seed: Optional[int] = None,
102
+ options: Optional[Dict] = None,
103
+ ) -> Tuple[Dict, Dict]:
104
+ """Reset the environment to initial state."""
105
+ super().reset(seed=seed)
106
+
107
+ if seed is not None:
108
+ self.patient_generator = PatientGenerator(seed=seed)
109
+
110
+ # Reset state
111
+ self.step_count = 0
112
+ self.total_reward = 0.0
113
+ self.available_beds = 10
114
+ self.episode_stats = {
115
+ "correct_triage": 0,
116
+ "safety_failures": 0,
117
+ "patients_seen": 0,
118
+ }
119
+
120
+ # Generate initial waiting room
121
+ initial_patients = np.random.randint(3, 8)
122
+ self.waiting_queue = self.patient_generator.generate_batch(initial_patients)
123
+ for i, p in enumerate(self.waiting_queue):
124
+ p.time_arrived = -i * 5 # Stagger arrival times
125
+
126
+ # Get first patient
127
+ self.current_patient = self._get_next_patient()
128
+
129
+ return self._get_observation(), self._get_info()
130
+
131
+ def step(self, action: Dict) -> Tuple[Dict, float, bool, bool, Dict]:
132
+ """
133
+ Execute one step in the environment.
134
+
135
+ Args:
136
+ action: Dict with 'triage_category' (1-5) and 'intervention' (index)
137
+
138
+ Returns:
139
+ observation, reward, terminated, truncated, info
140
+ """
141
+ self.step_count += 1
142
+
143
+ if self.current_patient is None:
144
+ # No more patients - episode ends
145
+ return self._get_observation(), 0.0, True, False, self._get_info()
146
+
147
+ # Parse action
148
+ assigned_category = action.get("triage_category", 3)
149
+ intervention_idx = action.get("intervention", 0)
150
+ intervention = self.INTERVENTIONS[intervention_idx]
151
+
152
+ # Calculate reward
153
+ reward = self._calculate_reward(assigned_category, intervention)
154
+ self.total_reward += reward
155
+ self.episode_stats["patients_seen"] += 1
156
+
157
+ # Update bed availability based on intervention
158
+ if intervention in ["send_to_resus", "send_to_majors"]:
159
+ self.available_beds = max(0, self.available_beds - 1)
160
+ elif intervention in ["discharge", "refer_to_gp"]:
161
+ self.available_beds = min(10, self.available_beds + 1)
162
+
163
+ # Possibly add new patients to queue
164
+ if np.random.random() < 0.3: # 30% chance of new arrival
165
+ new_patient = self.patient_generator.generate()
166
+ new_patient.time_arrived = self.step_count
167
+ self.waiting_queue.append(new_patient)
168
+
169
+ # Get next patient
170
+ self.current_patient = self._get_next_patient()
171
+
172
+ # Check termination
173
+ terminated = self.current_patient is None and len(self.waiting_queue) == 0
174
+ truncated = self.step_count >= self.max_steps
175
+
176
+ return self._get_observation(), reward, terminated, truncated, self._get_info()
177
+
178
+ def _calculate_reward(self, assigned_category: int, intervention: str) -> float:
179
+ """Calculate reward based on triage decision."""
180
+ if self.current_patient is None:
181
+ return 0.0
182
+
183
+ true_category = self.current_patient.true_category
184
+ category_diff = abs(assigned_category - true_category)
185
+
186
+ reward = 0.0
187
+
188
+ # Category accuracy
189
+ if category_diff == 0:
190
+ reward += 10.0
191
+ self.episode_stats["correct_triage"] += 1
192
+ elif category_diff == 1:
193
+ reward += 5.0 # Close enough
194
+ else:
195
+ reward -= 5.0 * category_diff # Penalty scales with error
196
+
197
+ # Critical safety failure: Under-triaging a critical patient
198
+ if true_category <= 2 and assigned_category >= true_category + 2:
199
+ reward -= 50.0
200
+ self.episode_stats["safety_failures"] += 1
201
+
202
+ # Intervention appropriateness
203
+ if true_category == 1 and intervention == "send_to_resus":
204
+ reward += 5.0
205
+ elif true_category == 5 and intervention in ["discharge", "refer_to_gp"]:
206
+ reward += 3.0
207
+ elif true_category == 1 and intervention == "discharge":
208
+ reward -= 30.0 # Never discharge a P1!
209
+
210
+ return reward
211
+
212
+ def _get_next_patient(self) -> Optional[Patient]:
213
+ """Get the next patient from the queue (FIFO with priority override)."""
214
+ if not self.waiting_queue:
215
+ return None
216
+
217
+ # Priority override: P1 patients jump the queue
218
+ for i, patient in enumerate(self.waiting_queue):
219
+ if patient.true_category == 1:
220
+ return self.waiting_queue.pop(i)
221
+
222
+ # Otherwise FIFO
223
+ return self.waiting_queue.pop(0)
224
+
225
+ def _get_observation(self) -> Dict:
226
+ """Build the observation dictionary."""
227
+ if self.current_patient is None:
228
+ return {
229
+ "patient_id": "",
230
+ "chief_complaint": "No patients waiting.",
231
+ "vitals": {
232
+ "hr": 0.0, "bp_sys": 0.0, "bp_dia": 0.0,
233
+ "spo2": 0.0, "rr": 0.0, "temp": 0.0, "avpu": "A"
234
+ },
235
+ "history": "",
236
+ "waiting_room": len(self.waiting_queue),
237
+ "available_beds": self.available_beds,
238
+ }
239
+
240
+ return {
241
+ "patient_id": self.current_patient.id,
242
+ "chief_complaint": self.current_patient.chief_complaint,
243
+ "vitals": {
244
+ "hr": float(self.current_patient.vitals.get("hr", 0)),
245
+ "bp_sys": float(self.current_patient.vitals.get("bp_sys", 0)),
246
+ "bp_dia": float(self.current_patient.vitals.get("bp_dia", 0)),
247
+ "spo2": float(self.current_patient.vitals.get("spo2", 0)),
248
+ "rr": float(self.current_patient.vitals.get("rr", 0)),
249
+ "temp": float(self.current_patient.vitals.get("temp", 0)),
250
+ "avpu": str(self.current_patient.vitals.get("avpu", "A")),
251
+ },
252
+ "history": self.current_patient.history,
253
+ "waiting_room": len(self.waiting_queue),
254
+ "available_beds": self.available_beds,
255
+ }
256
+
257
+ def _get_info(self) -> Dict:
258
+ """Return additional info."""
259
+ return {
260
+ "step": self.step_count,
261
+ "total_reward": self.total_reward,
262
+ "true_category": self.current_patient.true_category if self.current_patient else None,
263
+ **self.episode_stats,
264
+ }
265
+
266
+ def render(self) -> Optional[str]:
267
+ """Render the environment."""
268
+ if self.render_mode == "human" or self.render_mode == "ansi":
269
+ obs = self._get_observation()
270
+ output = f"""
271
+ ╔══════════════════════════════════════════════════════════════════╗
272
+ ║ A&E TRIAGE SIMULATOR │ Step: {self.step_count:3d} │ Waiting: {obs['waiting_room']:2d} │ Beds: {obs['available_beds']:2d} ║
273
+ ╠══════════════════════════════════════════════════════════════════╣
274
+ ║ PATIENT: {obs['patient_id']:<54} ║
275
+ ╠──────────────────────────────────────────────────────────────────╣
276
+ ║ Chief Complaint: ║
277
+ ║ "{obs['chief_complaint'][:60]:<60}" ║
278
+ ╠──────────────────────────────────────────────────────────────────╣
279
+ ║ VITALS: ║
280
+ ║ HR: {obs['vitals']['hr']:>3.0f} │ BP: {obs['vitals']['bp_sys']:>3.0f}/{obs['vitals']['bp_dia']:<3.0f} │ SpO2: {obs['vitals']['spo2']:>3.0f}% ║
281
+ ║ RR: {obs['vitals']['rr']:>3.0f} │ Temp: {obs['vitals']['temp']:.1f}°C │ AVPU: {obs['vitals']['avpu']} ║
282
+ ╠──────────────────────────────────────────────────────────────────╣
283
+ ║ History: {obs['history'][:55]:<55} ║
284
+ ╠══════════════════════════════════════════════════════════════════╣
285
+ ║ What is your triage decision? ║
286
+ ║ [1] Immediate [2] Very Urgent [3] Urgent [4] Std [5] Non ║
287
+ ╚══════════════════════════════════════════════════════════════════╝
288
+ """
289
+ if self.render_mode == "human":
290
+ print(output)
291
+ return output
292
+ return None
293
+
294
+ def close(self):
295
+ """Clean up resources."""
296
+ pass
297
+
298
+
299
+ # Register with Gymnasium
300
+ gym.register(
301
+ id="NurseSim-Triage-v0",
302
+ entry_point="nursesim_rl:TriageEnv",
303
+ )
package.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "nursesim-rl",
3
+ "version": "0.1.0",
4
+ "description": "A Triage Environment for Reinforcement Learning - OpenEnv Challenge Entry",
5
+ "author": "Lincoln Gombedza",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "reinforcement-learning",
9
+ "nursing",
10
+ "triage",
11
+ "openenv",
12
+ "gymnasium"
13
+ ],
14
+ "dependencies": {
15
+ "gymnasium": ">=0.29.0",
16
+ "numpy": ">=1.24.0"
17
+ }
18
+ }
push_dataset.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import os
3
+ import sys
4
+ from huggingface_hub import HfApi, create_repo
5
+ from datasets import load_dataset
6
+
7
+ def push_dataset(file_path, repo_id, token=None):
8
+ """
9
+ Pushes a JSONL dataset to Hugging Face Hub
10
+ """
11
+ print(f"🚀 Loading dataset from {file_path}...")
12
+ try:
13
+ dataset = load_dataset('json', data_files=file_path)
14
+ except Exception as e:
15
+ print(f"❌ Error loading dataset: {e}")
16
+ return
17
+
18
+ print(f"📦 Pushing to {repo_id}...")
19
+ try:
20
+ dataset.push_to_hub(repo_id, token=token)
21
+ print(f"✅ Successfully pushed to https://huggingface.co/datasets/{repo_id}")
22
+ except Exception as e:
23
+ print(f"❌ Error pushing to Hub: {e}")
24
+
25
+ if __name__ == "__main__":
26
+ if len(sys.argv) < 3:
27
+ print("Usage: python push_dataset.py <file_path> <repo_id>")
28
+ sys.exit(1)
29
+
30
+ file_path = sys.argv[1]
31
+ repo_id = sys.argv[2]
32
+
33
+ push_dataset(file_path, repo_id)
requirements.txt CHANGED
@@ -1,12 +1,12 @@
1
- # NurseSim-Triage Gradio Demo - Hugging Face Spaces Requirements
2
- # Compatible with ZeroGPU (No Unsloth - uses standard Transformers+PEFT)
3
-
4
- gradio
5
- spaces
6
- torch
7
- transformers
8
- peft
9
- bitsandbytes
10
- accelerate
11
- agentbeats
12
- gymnasium
 
1
+ # NurseSim-Triage Gradio Demo - Hugging Face Spaces Requirements
2
+ # Compatible with ZeroGPU (No Unsloth - uses standard Transformers+PEFT)
3
+
4
+ gradio
5
+ spaces
6
+ torch
7
+ transformers
8
+ peft
9
+ bitsandbytes
10
+ accelerate
11
+ agentbeats
12
+ gymnasium
test_env.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Test script: Verify the Triage Environment works correctly
3
+
4
+ Run: python test_env.py
5
+ """
6
+
7
+ import sys
8
+ sys.path.insert(0, '.')
9
+
10
+ from nursesim_rl import TriageEnv, PatientGenerator
11
+
12
+
13
+ def test_patient_generator():
14
+ """Test the patient generator."""
15
+ print("Testing PatientGenerator...")
16
+ gen = PatientGenerator(seed=42)
17
+
18
+ for category in range(1, 6):
19
+ patient = gen.generate(category=category)
20
+ assert patient.true_category == category
21
+ assert len(patient.chief_complaint) > 0
22
+ assert "hr" in patient.vitals
23
+ print(f" [OK] Category {category}: {patient.chief_complaint[:40]}...")
24
+
25
+ print(" [OK] PatientGenerator tests passed!\n")
26
+
27
+
28
+ def test_triage_env():
29
+ """Test the triage environment."""
30
+ print("Testing TriageEnv...")
31
+
32
+ env = TriageEnv(seed=42)
33
+ obs, info = env.reset()
34
+
35
+ assert "patient_id" in obs
36
+ assert "chief_complaint" in obs
37
+ assert "vitals" in obs
38
+ assert "waiting_room" in obs
39
+ print(f" [OK] Reset works, first patient: {obs['patient_id']}")
40
+
41
+ # Take some steps
42
+ for i in range(5):
43
+ action = {
44
+ "triage_category": 3, # Default to Urgent
45
+ "intervention": 1, # Send to majors
46
+ }
47
+ obs, reward, terminated, truncated, info = env.step(action)
48
+ print(f" [OK] Step {i+1}: Reward={reward:.1f}, Waiting={obs['waiting_room']}")
49
+
50
+ if terminated or truncated:
51
+ break
52
+
53
+ env.close()
54
+ print(" [OK] TriageEnv tests passed!\n")
55
+
56
+
57
+ def test_reward_calculation():
58
+ """Test reward calculations."""
59
+ print("Testing Reward Logic...")
60
+
61
+ env = TriageEnv(seed=123)
62
+ obs, info = env.reset()
63
+
64
+ # Force a specific patient for testing
65
+ from nursesim_rl.patient_generator import Patient
66
+ test_patient = Patient(
67
+ id="TEST001",
68
+ chief_complaint="Test complaint",
69
+ vitals={"hr": 100, "bp_sys": 120, "bp_dia": 80, "spo2": 98, "rr": 16, "temp": 37.0, "avpu": "A"},
70
+ history="Test history",
71
+ true_category=1, # Critical patient!
72
+ time_arrived=0,
73
+ )
74
+ env.current_patient = test_patient
75
+
76
+ # Test correct triage
77
+ action = {"triage_category": 1, "intervention": 0} # Correct: Cat 1, Resus
78
+ _, reward, _, _, _ = env.step(action)
79
+ print(f" Correct triage (Cat 1): Reward = {reward:.1f} (expected +15)")
80
+
81
+ # Reset and test safety failure
82
+ env.reset()
83
+ env.current_patient = test_patient
84
+ action = {"triage_category": 4, "intervention": 5} # Wrong: Cat 4, Discharge (DANGEROUS!)
85
+ _, reward, _, _, _ = env.step(action)
86
+ print(f" Safety failure (Cat 1 -> 4 + Discharge): Reward = {reward:.1f} (expected negative)")
87
+
88
+ env.close()
89
+ print(" [OK] Reward logic tests passed!\n")
90
+
91
+
92
+ if __name__ == "__main__":
93
+ print("\n" + "="*60)
94
+ print("[TEST] NURSESIM-RL TEST SUITE")
95
+ print("="*60 + "\n")
96
+
97
+ test_patient_generator()
98
+ test_triage_env()
99
+ test_reward_calculation()
100
+
101
+ print("="*60)
102
+ print("[PASS] ALL TESTS PASSED!")
103
+ print("="*60 + "\n")
test_semantic.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Test script for the Semantic RL Environment.
3
+ Validates that the NurseEmbedWrapper works correctly.
4
+ """
5
+
6
+ import numpy as np
7
+ from nursesim_rl import TriageEnv, NurseEmbedWrapper
8
+
9
+ def test_semantic_wrapper():
10
+ print("=" * 60)
11
+ print("Testing NurseEmbed Semantic RL Wrapper")
12
+ print("=" * 60)
13
+
14
+ # Create base environment
15
+ print("\n[1] Creating base TriageEnv...")
16
+ base_env = TriageEnv(max_steps=10, seed=42)
17
+
18
+ # Wrap with NurseEmbed
19
+ print("[2] Wrapping with NurseEmbedWrapper...")
20
+ semantic_env = NurseEmbedWrapper(base_env, use_vitals=True)
21
+
22
+ # Check observation space
23
+ print(f"\n[3] Observation Space Check:")
24
+ print(f" Base Env: {type(base_env.observation_space)}")
25
+ print(f" Semantic Env: {semantic_env.observation_space}")
26
+ print(f" Expected shape: (390,) [384 embed + 6 vitals]")
27
+
28
+ # Reset and get observation
29
+ print("\n[4] Resetting environment...")
30
+ obs, info = semantic_env.reset(seed=42)
31
+
32
+ print(f" Observation type: {type(obs)}")
33
+ print(f" Observation shape: {obs.shape}")
34
+ print(f" Observation range: [{obs.min():.3f}, {obs.max():.3f}]")
35
+
36
+ # Take a step
37
+ print("\n[5] Taking a step with action (Cat=3, Intervention=2)...")
38
+ action = {"triage_category": 3, "intervention": 2}
39
+ obs2, reward, terminated, truncated, info = semantic_env.step(action)
40
+
41
+ print(f" Reward: {reward}")
42
+ print(f" Terminated: {terminated}")
43
+ print(f" New observation shape: {obs2.shape}")
44
+
45
+ # Verify embedding is meaningful (not just zeros)
46
+ print("\n[6] Embedding quality check:")
47
+ embed_part = obs[:384] # First 384 dims are the embedding
48
+ print(f" Embedding L2 norm: {np.linalg.norm(embed_part):.3f}")
49
+ print(f" Embedding is normalized: {abs(np.linalg.norm(embed_part) - 1.0) < 0.1}")
50
+
51
+ # Test caching
52
+ print("\n[7] Testing embedding cache...")
53
+ obs3, _ = semantic_env.reset(seed=42) # Same seed = same patient
54
+ cache_hit = np.allclose(obs[:384], obs3[:384])
55
+ print(f" Cache working: {cache_hit}")
56
+
57
+ print("\n" + "=" * 60)
58
+ print("ALL TESTS PASSED!")
59
+ print("=" * 60)
60
+
61
+ return True
62
+
63
+ if __name__ == "__main__":
64
+ test_semantic_wrapper()
tests/test_a2a_compliance.py ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ A2A Protocol Compliance Tests for NurseSim-Triage Agent
4
+
5
+ Tests the agent's conformance to the Agent-to-Agent (A2A) protocol specification.
6
+ """
7
+
8
+ import json
9
+ import subprocess
10
+ import time
11
+ import requests
12
+ from pathlib import Path
13
+
14
+
15
+ def test_agent_card_exists():
16
+ """Test that agent-card.json exists and is valid JSON."""
17
+ agent_card_path = Path(".well-known/agent-card.json")
18
+ assert agent_card_path.exists(), "agent-card.json not found"
19
+
20
+ with open(agent_card_path) as f:
21
+ card = json.load(f)
22
+
23
+ # Validate required fields
24
+ assert "name" in card, "Missing 'name' field"
25
+ assert "protocol" in card, "Missing 'protocol' field"
26
+ assert card["protocol"] == "a2a/v1.0", f"Invalid protocol version: {card['protocol']}"
27
+ assert "capabilities" in card, "Missing 'capabilities' field"
28
+ assert "input_schema" in card["capabilities"], "Missing input_schema"
29
+ assert "output_schema" in card["capabilities"], "Missing output_schema"
30
+
31
+ print("✓ Agent card validation passed")
32
+ return card
33
+
34
+
35
+ def test_input_schema_validation(agent_card):
36
+ """Test that input schema is properly defined."""
37
+ input_schema = agent_card["capabilities"]["input_schema"]
38
+
39
+ assert input_schema["type"] == "object"
40
+ assert "properties" in input_schema
41
+ assert "complaint" in input_schema["properties"]
42
+ assert "vitals" in input_schema["properties"]
43
+
44
+ # Validate vitals schema
45
+ vitals_schema = input_schema["properties"]["vitals"]
46
+ assert "heart_rate" in vitals_schema["properties"]
47
+ assert "blood_pressure" in vitals_schema["properties"]
48
+ assert "spo2" in vitals_schema["properties"]
49
+ assert "temperature" in vitals_schema["properties"]
50
+
51
+ print("✓ Input schema validation passed")
52
+
53
+
54
+ def test_output_schema_validation(agent_card):
55
+ """Test that output schema is properly defined."""
56
+ output_schema = agent_card["capabilities"]["output_schema"]
57
+
58
+ assert output_schema["type"] == "object"
59
+ assert "properties" in output_schema
60
+ assert "triage_category" in output_schema["properties"]
61
+ assert "assessment" in output_schema["properties"]
62
+
63
+ # Validate triage category enum
64
+ triage_prop = output_schema["properties"]["triage_category"]
65
+ assert "enum" in triage_prop
66
+ expected_categories = ["Immediate", "Very Urgent", "Urgent", "Standard", "Non-Urgent"]
67
+ assert triage_prop["enum"] == expected_categories
68
+
69
+ print("✓ Output schema validation passed")
70
+
71
+
72
+ def test_agent_task_processing():
73
+ """Test that the agent can process a task and return valid output."""
74
+ # This test requires the agent to be running
75
+ # For CI/CD, we'll test the agent_main.py module directly
76
+
77
+ try:
78
+ from agent_main import NurseSimTriageAgent
79
+
80
+ # Initialize agent (requires HF_TOKEN in environment)
81
+ agent = NurseSimTriageAgent()
82
+
83
+ # Test task
84
+ test_task = {
85
+ "complaint": "Severe chest pain radiating to left arm",
86
+ "vitals": {
87
+ "heart_rate": 115,
88
+ "blood_pressure": "85/60",
89
+ "spo2": 91,
90
+ "temperature": 37.5
91
+ }
92
+ }
93
+
94
+ # Process task
95
+ result = agent.process_task(test_task)
96
+
97
+ # Validate result structure
98
+ assert "triage_category" in result, "Missing triage_category in result"
99
+ assert "assessment" in result, "Missing assessment in result"
100
+ assert isinstance(result["assessment"], str), "Assessment must be a string"
101
+
102
+ # Validate triage category is valid
103
+ valid_categories = ["Immediate", "Very Urgent", "Urgent", "Standard", "Non-Urgent", "Error"]
104
+ assert result["triage_category"] in valid_categories, \
105
+ f"Invalid triage category: {result['triage_category']}"
106
+
107
+ print(f"✓ Agent task processing passed")
108
+ print(f" Triage: {result['triage_category']}")
109
+ print(f" Assessment: {result['assessment'][:100]}...")
110
+
111
+ return True
112
+
113
+ except ImportError:
114
+ print("⚠ Skipping task processing test (agent_main.py not available)")
115
+ return False
116
+ except ValueError as e:
117
+ if "HF_TOKEN" in str(e):
118
+ print("⚠ Skipping task processing test (HF_TOKEN not set)")
119
+ return False
120
+ raise
121
+
122
+
123
+ def test_agent_health_check():
124
+ """Test agent health check endpoint."""
125
+ try:
126
+ from agent_main import NurseSimTriageAgent
127
+
128
+ agent = NurseSimTriageAgent()
129
+ health = agent.health_check()
130
+
131
+ assert "status" in health
132
+ assert health["status"] == "healthy"
133
+ assert "model_loaded" in health
134
+
135
+ print("✓ Health check passed")
136
+ return True
137
+
138
+ except (ImportError, ValueError):
139
+ print("⚠ Skipping health check test")
140
+ return False
141
+
142
+
143
+ def run_all_tests():
144
+ """Run all A2A compliance tests."""
145
+ print("\n" + "="*60)
146
+ print("NurseSim-Triage A2A Protocol Compliance Tests")
147
+ print("="*60 + "\n")
148
+
149
+ try:
150
+ # Test 1: Agent card validation
151
+ agent_card = test_agent_card_exists()
152
+
153
+ # Test 2: Input schema validation
154
+ test_input_schema_validation(agent_card)
155
+
156
+ # Test 3: Output schema validation
157
+ test_output_schema_validation(agent_card)
158
+
159
+ # Test 4: Task processing (requires model)
160
+ test_agent_task_processing()
161
+
162
+ # Test 5: Health check
163
+ test_agent_health_check()
164
+
165
+ print("\n" + "="*60)
166
+ print("✓ All A2A compliance tests passed!")
167
+ print("="*60 + "\n")
168
+
169
+ return True
170
+
171
+ except AssertionError as e:
172
+ print(f"\n✗ Test failed: {e}")
173
+ return False
174
+ except Exception as e:
175
+ print(f"\n✗ Unexpected error: {e}")
176
+ import traceback
177
+ traceback.print_exc()
178
+ return False
179
+
180
+
181
+ if __name__ == "__main__":
182
+ import sys
183
+ success = run_all_tests()
184
+ sys.exit(0 if success else 1)
tests/test_dual_mode.sh ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # Dual-Mode Configuration Test
3
+ # Tests that the agent correctly switches between Gradio and A2A modes
4
+
5
+ set -e
6
+
7
+ echo "========================================"
8
+ echo "NurseSim-Triage Dual-Mode Test"
9
+ echo "========================================"
10
+ echo ""
11
+
12
+ # Test 1: A2A Mode
13
+ echo "Test 1: Setting AGENT_MODE=a2a"
14
+ export AGENT_MODE=a2a
15
+ # Run in background and redirect to file
16
+ ./run.sh > a2a_output.log 2>&1 &
17
+ pid=$!
18
+ echo "Process started with PID: $pid"
19
+ sleep 5
20
+ # Kill the process
21
+ kill $pid 2>/dev/null || true
22
+
23
+ # Check output file
24
+ if grep -q "AgentBeats A2A mode" a2a_output.log; then
25
+ echo "✓ A2A mode launched correctly"
26
+ cat a2a_output.log
27
+ rm a2a_output.log
28
+ else
29
+ echo "✗ A2A mode failed. Log output:"
30
+ cat a2a_output.log
31
+ rm a2a_output.log || true
32
+ exit 1
33
+ fi
34
+
35
+ # Test 2: Gradio Mode
36
+ echo ""
37
+ echo "Test 2: Setting AGENT_MODE=gradio"
38
+ export AGENT_MODE=gradio
39
+ ./run.sh > gradio_output.log 2>&1 &
40
+ pid=$!
41
+ echo "Process started with PID: $pid"
42
+ sleep 5
43
+ kill $pid 2>/dev/null || true
44
+
45
+ if grep -q "Gradio demo" gradio_output.log; then
46
+ echo "✓ Gradio mode launched correctly"
47
+ cat gradio_output.log
48
+ rm gradio_output.log
49
+ else
50
+ echo "✗ Gradio mode failed. Log output:"
51
+ cat gradio_output.log
52
+ rm gradio_output.log || true
53
+ exit 1
54
+ fi
55
+
56
+ # Test 3: Invalid Mode
57
+ echo ""
58
+ echo "Test 3: Invalid AGENT_MODE"
59
+ export AGENT_MODE=invalid
60
+ if ./run.sh 2>&1 | grep -q "Error: Invalid AGENT_MODE"; then
61
+ echo "✓ Invalid mode properly rejected"
62
+ else
63
+ echo "✗ Invalid mode handling failed"
64
+ exit 1
65
+ fi
66
+
67
+ echo ""
68
+ echo "========================================"
69
+ echo "✓ All dual-mode tests passed!"
70
+ echo "========================================"
train_semantic_agent.py ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Train a PPO Agent on the Semantic Triage Environment.
3
+
4
+ This script trains an agent that learns from NurseEmbed-encoded clinical observations.
5
+ """
6
+
7
+ import os
8
+ import numpy as np
9
+ from datetime import datetime
10
+
11
+ # Stable Baselines 3
12
+ from stable_baselines3 import PPO
13
+ from stable_baselines3.common.env_util import make_vec_env
14
+ from stable_baselines3.common.callbacks import EvalCallback, BaseCallback
15
+ from stable_baselines3.common.vec_env import DummyVecEnv
16
+
17
+ # Our environment
18
+ from nursesim_rl import TriageEnv, NurseEmbedWrapper
19
+
20
+
21
+ class PrintProgressCallback(BaseCallback):
22
+ """Simple callback to print training progress."""
23
+
24
+ def __init__(self, print_freq: int = 1000, verbose: int = 0):
25
+ super().__init__(verbose)
26
+ self.print_freq = print_freq
27
+
28
+ def _on_step(self) -> bool:
29
+ if self.n_calls % self.print_freq == 0:
30
+ # Get recent episode rewards if available
31
+ if len(self.model.ep_info_buffer) > 0:
32
+ mean_reward = np.mean([ep['r'] for ep in self.model.ep_info_buffer])
33
+ mean_length = np.mean([ep['l'] for ep in self.model.ep_info_buffer])
34
+ print(f"Step {self.n_calls}: Mean Reward = {mean_reward:.2f}, Mean Length = {mean_length:.1f}")
35
+ return True
36
+
37
+
38
+ def make_semantic_env():
39
+ """Factory function for creating the semantic environment."""
40
+ base_env = TriageEnv(max_steps=50, max_patients=20)
41
+ semantic_env = NurseEmbedWrapper(base_env, use_vitals=True)
42
+ return semantic_env
43
+
44
+
45
+ def train_semantic_agent(
46
+ total_timesteps: int = 50000,
47
+ save_path: str = "models/semantic_ppo",
48
+ log_dir: str = "logs/semantic_ppo",
49
+ ):
50
+ """Train a PPO agent on the semantic triage environment."""
51
+
52
+ print("=" * 60)
53
+ print("SEMANTIC RL TRAINING")
54
+ print(f"Started: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
55
+ print("=" * 60)
56
+
57
+ # Create directories
58
+ os.makedirs(save_path, exist_ok=True)
59
+ os.makedirs(log_dir, exist_ok=True)
60
+
61
+ # Create vectorized environment
62
+ print("\n[1] Creating Semantic Environment...")
63
+ env = DummyVecEnv([make_semantic_env])
64
+
65
+ print(f" Observation space: {env.observation_space}")
66
+ print(f" Action space: {env.action_space}")
67
+
68
+ # Create evaluation environment
69
+ eval_env = DummyVecEnv([make_semantic_env])
70
+
71
+ # Create the PPO agent
72
+ print("\n[2] Initializing PPO Agent...")
73
+ model = PPO(
74
+ "MlpPolicy",
75
+ env,
76
+ learning_rate=3e-4,
77
+ n_steps=2048,
78
+ batch_size=64,
79
+ n_epochs=10,
80
+ gamma=0.99,
81
+ gae_lambda=0.95,
82
+ clip_range=0.2,
83
+ ent_coef=0.01,
84
+ verbose=0,
85
+ tensorboard_log=log_dir,
86
+ )
87
+
88
+ print(f" Policy architecture: {model.policy}")
89
+
90
+ # Callbacks
91
+ progress_callback = PrintProgressCallback(print_freq=2000)
92
+ eval_callback = EvalCallback(
93
+ eval_env,
94
+ best_model_save_path=save_path,
95
+ log_path=log_dir,
96
+ eval_freq=5000,
97
+ n_eval_episodes=5,
98
+ deterministic=True,
99
+ verbose=0,
100
+ )
101
+
102
+ # Train!
103
+ print(f"\n[3] Training for {total_timesteps:,} timesteps...")
104
+ print("-" * 60)
105
+
106
+ model.learn(
107
+ total_timesteps=total_timesteps,
108
+ callback=[progress_callback, eval_callback],
109
+ progress_bar=True,
110
+ )
111
+
112
+ # Save final model
113
+ final_path = os.path.join(save_path, "semantic_ppo_final")
114
+ model.save(final_path)
115
+ print(f"\n[4] Model saved to: {final_path}")
116
+
117
+ # Quick evaluation
118
+ print("\n[5] Final Evaluation (10 episodes)...")
119
+ rewards = []
120
+ for i in range(10):
121
+ obs = eval_env.reset()
122
+ episode_reward = 0
123
+ done = False
124
+ while not done:
125
+ action, _ = model.predict(obs, deterministic=True)
126
+ obs, reward, done, info = eval_env.step(action)
127
+ episode_reward += reward[0]
128
+ rewards.append(episode_reward)
129
+
130
+ print(f" Mean Reward: {np.mean(rewards):.2f} +/- {np.std(rewards):.2f}")
131
+ print(f" Best Episode: {np.max(rewards):.2f}")
132
+ print(f" Worst Episode: {np.min(rewards):.2f}")
133
+
134
+ print("\n" + "=" * 60)
135
+ print("TRAINING COMPLETE!")
136
+ print("=" * 60)
137
+
138
+ return model
139
+
140
+
141
+ if __name__ == "__main__":
142
+ # Run with reduced timesteps for quick demo
143
+ train_semantic_agent(total_timesteps=20000)
viz/semantic_clusters.png ADDED

Git LFS Details

  • SHA256: c77b4b852aabb13a79d62c0cc2c3863544c98d69111a2c966c6de63713f34dc5
  • Pointer size: 131 Bytes
  • Size of remote file: 121 kB
viz_semantic.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Visualize "What the Agent Sees" - Semantic Embedding Projection
3
+
4
+ This script generates a 2D t-SNE visualization of how the RL agent
5
+ perceives clinical observations through NurseEmbed semantic vectors.
6
+ """
7
+
8
+ import numpy as np
9
+ import matplotlib.pyplot as plt
10
+ from sklearn.manifold import TSNE
11
+ from sklearn.decomposition import PCA
12
+ import os
13
+
14
+ from nursesim_rl import TriageEnv, NurseEmbedWrapper
15
+
16
+
17
+ def collect_observations(n_observations: int = 200, seed: int = 42):
18
+ """Collect observations and their true categories."""
19
+
20
+ print(f"[1] Collecting {n_observations} observations...")
21
+
22
+ # Create semantic environment
23
+ base_env = TriageEnv(max_steps=100, max_patients=50, seed=seed)
24
+ env = NurseEmbedWrapper(base_env, use_vitals=True)
25
+
26
+ observations = []
27
+ categories = []
28
+ complaints = []
29
+
30
+ obs, info = env.reset(seed=seed)
31
+
32
+ for i in range(n_observations):
33
+ # Store observation and metadata
34
+ observations.append(obs.copy())
35
+
36
+ # Get true category from wrapped env
37
+ if base_env.current_patient:
38
+ categories.append(base_env.current_patient.true_category)
39
+ complaints.append(base_env.current_patient.chief_complaint[:50])
40
+ else:
41
+ categories.append(3) # Default
42
+ complaints.append("No patient")
43
+
44
+ # Take a random action to move to next patient
45
+ action = env.action_space.sample()
46
+ obs, _, terminated, truncated, _ = env.step(action)
47
+
48
+ if terminated or truncated:
49
+ obs, _ = env.reset()
50
+
51
+ print(f" Collected {len(observations)} observations")
52
+
53
+ return np.array(observations), np.array(categories), complaints
54
+
55
+
56
+ def visualize_embeddings(observations, categories, complaints, output_path="viz"):
57
+ """Create t-SNE visualization of embeddings."""
58
+
59
+ print("[2] Computing t-SNE projection...")
60
+
61
+ # Extract just the embedding part (first 384 dims)
62
+ embeddings = observations[:, :384]
63
+
64
+ # First reduce with PCA for speed (384 -> 50)
65
+ pca = PCA(n_components=50)
66
+ embeddings_pca = pca.fit_transform(embeddings)
67
+ print(f" PCA explained variance: {sum(pca.explained_variance_ratio_):.2%}")
68
+
69
+ # Then t-SNE to 2D
70
+ tsne = TSNE(n_components=2, perplexity=30, random_state=42, max_iter=1000)
71
+ embeddings_2d = tsne.fit_transform(embeddings_pca)
72
+
73
+ print("[3] Creating visualization...")
74
+
75
+ # Create figure
76
+ fig, ax = plt.subplots(figsize=(12, 10))
77
+
78
+ # Color map for triage categories
79
+ colors = {
80
+ 1: '#FF0000', # Immediate - Red
81
+ 2: '#FF8C00', # Very Urgent - Orange
82
+ 3: '#FFD700', # Urgent - Yellow
83
+ 4: '#32CD32', # Standard - Green
84
+ 5: '#1E90FF', # Non-urgent - Blue
85
+ }
86
+
87
+ category_names = {
88
+ 1: 'P1: Immediate',
89
+ 2: 'P2: Very Urgent',
90
+ 3: 'P3: Urgent',
91
+ 4: 'P4: Standard',
92
+ 5: 'P5: Non-urgent'
93
+ }
94
+
95
+ # Plot each category
96
+ for cat in sorted(set(categories)):
97
+ mask = categories == cat
98
+ ax.scatter(
99
+ embeddings_2d[mask, 0],
100
+ embeddings_2d[mask, 1],
101
+ c=colors.get(cat, '#888888'),
102
+ label=category_names.get(cat, f'Category {cat}'),
103
+ alpha=0.7,
104
+ s=100,
105
+ edgecolors='white',
106
+ linewidths=0.5
107
+ )
108
+
109
+ # Styling
110
+ ax.set_title(
111
+ 'What the Agent Sees: Semantic Patient Clusters\n'
112
+ '(t-SNE projection of NurseEmbed vectors)',
113
+ fontsize=14, fontweight='bold'
114
+ )
115
+ ax.set_xlabel('Semantic Dimension 1', fontsize=11)
116
+ ax.set_ylabel('Semantic Dimension 2', fontsize=11)
117
+
118
+ # Legend
119
+ ax.legend(loc='upper right', title='Triage Category', fontsize=10)
120
+
121
+ # Clean up axes
122
+ ax.spines['top'].set_visible(False)
123
+ ax.spines['right'].set_visible(False)
124
+ ax.grid(True, alpha=0.3)
125
+
126
+ # Add annotation
127
+ ax.text(
128
+ 0.02, 0.02,
129
+ 'Patients with similar clinical presentations\ncluster together in semantic space.',
130
+ transform=ax.transAxes,
131
+ fontsize=9,
132
+ verticalalignment='bottom',
133
+ style='italic',
134
+ bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5)
135
+ )
136
+
137
+ # Save
138
+ os.makedirs(output_path, exist_ok=True)
139
+ output_file = os.path.join(output_path, 'semantic_clusters.png')
140
+ plt.tight_layout()
141
+ plt.savefig(output_file, dpi=150, bbox_inches='tight')
142
+ print(f"[4] Saved to: {output_file}")
143
+
144
+ # Also show statistics
145
+ print("\n[5] Cluster Statistics:")
146
+ for cat in sorted(set(categories)):
147
+ mask = categories == cat
148
+ center = embeddings_2d[mask].mean(axis=0)
149
+ print(f" {category_names[cat]}: {mask.sum()} patients, center at ({center[0]:.1f}, {center[1]:.1f})")
150
+
151
+ return output_file
152
+
153
+
154
+ def main():
155
+ print("=" * 60)
156
+ print("SEMANTIC EMBEDDING VISUALIZATION")
157
+ print("'What the Agent Sees'")
158
+ print("=" * 60 + "\n")
159
+
160
+ # Collect data
161
+ observations, categories, complaints = collect_observations(n_observations=150)
162
+
163
+ # Visualize
164
+ output_file = visualize_embeddings(observations, categories, complaints)
165
+
166
+ print("\n" + "=" * 60)
167
+ print("VISUALIZATION COMPLETE!")
168
+ print(f"Open: {os.path.abspath(output_file)}")
169
+ print("=" * 60)
170
+
171
+
172
+ if __name__ == "__main__":
173
+ main()