Update: Add Semantic RL Mode (NurseEmbed Integration)
Browse files- .gitattributes +1 -0
- .github/workflows/ci.yml +78 -0
- .github/workflows/sync-hf-space.yml +57 -0
- .gitignore +51 -0
- .well-known/agent-card.json +117 -117
- Dockerfile +36 -36
- HF_BLOG_POST.md +284 -0
- HF_BLOG_POST_CLEAN.md +270 -0
- LICENSE +21 -0
- MODEL_CARD.md +117 -0
- README.md +331 -330
- SUBMISSION_ABSTRACT.md +30 -0
- WANDB_REPORT_TEXT.md +50 -0
- agent_main.py +272 -272
- app.py +159 -159
- data/gpt_scenarios.json +2892 -0
- data/gpt_train.jsonl +3 -0
- data/gpt_train_1768262967.jsonl +3 -0
- data/gpt_train_1768263238.jsonl +3 -0
- data/gpt_train_1768263486.jsonl +3 -0
- data/gpt_train_1768263718.jsonl +3 -0
- data/gpt_train_1768263967.jsonl +3 -0
- data/gpt_train_batch2.jsonl +3 -0
- data/gpt_train_batch3.jsonl +3 -0
- data/gpt_train_batch4.jsonl +3 -0
- data/gpt_train_batch5.jsonl +3 -0
- data/gpt_train_batch6.jsonl +3 -0
- data/train.jsonl +3 -0
- data/train_expanded.jsonl +3 -0
- data/val.jsonl +3 -0
- demo_human_play.py +86 -0
- generate_dataset.py +185 -0
- generate_gpt_scenarios.py +230 -0
- nursesim_rl/__init__.py +16 -10
- nursesim_rl/patient_generator.py +272 -272
- nursesim_rl/semantic_wrapper.py +169 -0
- nursesim_rl/triage_env.py +303 -303
- package.json +18 -0
- push_dataset.py +33 -0
- requirements.txt +12 -12
- test_env.py +103 -0
- test_semantic.py +64 -0
- tests/test_a2a_compliance.py +184 -0
- tests/test_dual_mode.sh +70 -0
- train_semantic_agent.py +143 -0
- viz/semantic_clusters.png +3 -0
- viz_semantic.py +173 -0
.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 |
+

|
| 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 |
+

|
| 159 |
+
|
| 160 |
+
#### Loss Progression (Epochs)
|
| 161 |
+

|
| 162 |
+
|
| 163 |
+
#### Gradient Norm Stability
|
| 164 |
+

|
| 165 |
+
*Gradient norm stabilized after ~20 steps, indicating healthy convergence.*
|
| 166 |
+
|
| 167 |
+
#### Learning Rate Schedule
|
| 168 |
+

|
| 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 |
+

|
| 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 |
+

|
| 145 |
+
|
| 146 |
+
#### Loss Progression (Epochs)
|
| 147 |
+

|
| 148 |
+
|
| 149 |
+
#### Gradient Norm Stability
|
| 150 |
+

|
| 151 |
+
*Gradient norm stabilized after ~20 steps, indicating healthy convergence.*
|
| 152 |
+
|
| 153 |
+
#### Learning Rate Schedule
|
| 154 |
+

|
| 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 |
-
[](https://agentbeats.dev/ClinyQAi/nursesim-triage)
|
| 13 |
-
|
| 14 |
-
[](https://rdi.berkeley.edu/agentx-agentbeats)
|
| 15 |
-
[](https://huggingface.co/NurseCitizenDeveloper/NurseSim-Triage-Llama-3.2-3B)
|
| 16 |
-
[](https://wandb.ai/mrlincs-nursing-citizen-development/huggingface)
|
| 17 |
-
[](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 |
-

|
| 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 |
-
- **
|
| 34 |
-
- **
|
| 35 |
-
- **
|
| 36 |
-
- **
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
-
|
| 66 |
-
|
| 67 |
-
"
|
| 68 |
-
|
| 69 |
-
"
|
| 70 |
-
"
|
| 71 |
-
"
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
│ ├──
|
| 83 |
-
│
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
│
|
| 89 |
-
|
| 90 |
-
├──
|
| 91 |
-
├──
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
export
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
export
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
|
| 145 |
-
|
|
| 146 |
-
| **
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
- **
|
| 154 |
-
- **
|
| 155 |
-
- **
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
export
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
-
|
| 177 |
-
|
| 178 |
-
"
|
| 179 |
-
|
| 180 |
-
"
|
| 181 |
-
"
|
| 182 |
-
"
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
|
| 213 |
-
|
|
| 214 |
-
| **
|
| 215 |
-
| **
|
| 216 |
-
| **
|
| 217 |
-
| **
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
|
| 228 |
-
|
|
| 229 |
-
|
|
| 230 |
-
|
|
| 231 |
-
|
|
| 232 |
-
|
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
- **
|
| 238 |
-
- **
|
| 239 |
-
- **
|
| 240 |
-
- **
|
| 241 |
-
- **
|
| 242 |
-
- **
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
- **
|
| 251 |
-
- **
|
| 252 |
-
- **
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
- **
|
| 257 |
-
- **
|
| 258 |
-
- **Response
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
-
|
| 272 |
-
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
- `
|
| 289 |
-
- `
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
-
|
| 298 |
-
-
|
| 299 |
-
-
|
| 300 |
-
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
- **
|
| 311 |
-
- **Professor
|
| 312 |
-
- **
|
| 313 |
-
- **
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
- **
|
| 318 |
-
- **
|
| 319 |
-
- **
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
- **
|
| 324 |
-
- **
|
| 325 |
-
- **
|
| 326 |
-
- **
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
|
|
|
|
|
| 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 |
+
[](https://agentbeats.dev/ClinyQAi/nursesim-triage)
|
| 13 |
+
|
| 14 |
+
[](https://rdi.berkeley.edu/agentx-agentbeats)
|
| 15 |
+
[](https://huggingface.co/NurseCitizenDeveloper/NurseSim-Triage-Llama-3.2-3B)
|
| 16 |
+
[](https://wandb.ai/mrlincs-nursing-citizen-development/huggingface)
|
| 17 |
+
[](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 |
+

|
| 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 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
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()
|