paramjitbaral commited on
Commit
43cbe78
·
verified ·
1 Parent(s): ee9f0c7

Upload folder using huggingface_hub

Browse files
README.md CHANGED
@@ -1,300 +1,205 @@
1
  ---
2
- title: ACDE OpenEnv
3
- emoji: "🚑"
4
  colorFrom: blue
5
  colorTo: green
6
  sdk: docker
7
  pinned: false
 
8
  base_path: /web
 
 
9
  ---
10
 
11
- # Emergency Routing Simulation (ACDE)
12
 
13
- This project is a simulation environment for emergency ambulance routing.
14
 
15
- In simple terms:
16
- - A patient needs urgent care.
17
- - Several hospitals are available.
18
- - Each hospital has trade-offs (distance, traffic, ICU certainty, specialization).
19
- - Conditions can change while the ambulance is moving.
20
- - The agent must decide where to go, step by step.
21
 
22
- The goal is not to be perfect every time. The goal is to make realistic decisions under uncertainty.
23
 
24
- ## What This Project Does
 
 
25
 
26
- This environment helps you test decision logic in situations where information is incomplete and time is limited.
27
 
28
- It supports three difficulty levels:
29
- - `acde_easy`
30
- - `acde_medium`
31
- - `acde_hard`
32
 
33
- As difficulty increases, uncertainty and penalties increase too.
 
 
34
 
35
- ## How It Works (Simple Flow)
36
 
37
- Every episode follows this loop:
38
-
39
- 1. The environment is reset with a seed and task.
40
- 2. You get an observation:
41
- - Patient condition
42
- - Required specialization
43
- - Hospital list with visible signals
44
- 3. The policy scores hospitals.
45
- 4. One hospital is selected.
46
- 5. The environment validates arrival using hidden checks.
47
- 6. You receive outcome + reward.
48
- 7. If not done, repeat until success or failure.
49
-
50
- ## What Makes It Realistic
51
-
52
- This is not a static lookup problem. It includes realistic uncertainty:
53
-
54
- - Displayed ICU status can differ from actual ICU status.
55
- - Traffic can change between steps.
56
- - Hospital overload can change outcomes.
57
- - Specialist availability can fail at arrival.
58
- - A hospital that failed once may become usable later.
59
-
60
- The policy includes safety rules such as:
61
- - Immediate retry protection after rejection.
62
- - Cooldown handling for recently failed hospitals.
63
- - Exploration among top options (not blind random picks).
64
-
65
- ## Project Layout
66
-
67
- Key files:
68
-
69
- - `app/environment/core.py`
70
- - Main environment loop (`reset`, `step`, transition logic)
71
- - `app/environment/validation.py`
72
- - Hidden validation checks (ICU, specialist, overload, outcome)
73
- - `app/environment/graders.py`
74
- - Final scoring and pass/fail grading
75
- - `app/models/`
76
- - Pydantic models for state, observation, reward, action
77
- - `app/server/app.py`
78
- - FastAPI server endpoints
79
- - `inference.py`
80
- - Local policy runner (CLI episodes)
81
- - `data/learning_memory.json`
82
- - Rolling policy memory
83
- - `data/trajectory_history.jsonl`
84
- - Per-step trajectory logs
85
-
86
- ## API Endpoints
87
-
88
- When server mode is running:
89
-
90
- - `GET /health`
91
- - `POST /reset`
92
- - `POST /step`
93
- - `GET /state`
94
 
95
  ## Action Space
96
 
97
- The agent sends one action per step as JSON:
98
 
99
- ```json
100
- {
101
- "step": 1,
102
- "hospital_id": "H3",
103
- "rationale": "short decision reason"
104
- }
105
- ```
106
-
107
- Action fields:
108
- - `step` (int, >=1): must match current environment step
109
- - `hospital_id` (str): target hospital identifier
110
- - `rationale` (str, optional): policy explanation
111
 
112
  ## Observation Space
113
 
114
- Each `reset()` and `step()` returns an observation with:
115
- - episode metadata: `episode_id`, `seed`, `task_id`, `scenario_name`, `scenario_difficulty`
116
- - patient state: `patient_condition`, `required_specialization`, remaining time fields
117
- - hospital list: `hospital_id`, `distance_km`, `icu`, `specialization`, `traffic`
118
- - routing history: visited/failed hospitals and failure reasons
119
- - hidden-state feedback: `last_arrival_outcome` summary (status/reason/suitability)
120
- - memory snapshot used by the baseline policy
121
-
122
- Core schema is defined by Pydantic models in:
123
- - `app/models/action.py`
124
- - `app/models/observation.py`
125
- - `app/models/state.py`
126
- - `app/models/reward.py`
127
-
128
- ## Required Environment Variables
129
-
130
- Before running `inference.py`, define:
131
- - `API_BASE_URL`: API base URL for the OpenAI-compatible endpoint
132
- - `MODEL_NAME`: model name used for rationale generation
133
- - `HF_TOKEN`: API key/token
134
-
135
- Windows PowerShell example:
136
-
137
- ```powershell
138
- $env:API_BASE_URL = "https://api-inference.huggingface.co/v1"
139
- $env:MODEL_NAME = "your-model-id"
140
- $env:HF_TOKEN = "your-token"
141
- ```
142
 
143
- ## Installation
 
 
 
 
 
 
 
 
 
 
144
 
145
- ## 1) Prerequisites
146
 
147
- - Python 3.10+ (3.12 works)
148
- - `pip`
149
 
150
- ## 2) Open a terminal in this folder
 
 
 
 
 
 
 
 
 
 
 
151
 
152
- Folder should be:
153
- - `my_env`
154
 
155
- ## 3) Create and activate a virtual environment (recommended)
156
 
157
- Windows PowerShell:
 
 
158
 
159
- ```powershell
160
- python -m venv .venv
161
- .\.venv\Scripts\Activate.ps1
162
- ```
163
 
164
- macOS/Linux:
165
 
166
- ```bash
167
- python -m venv .venv
168
- source .venv/bin/activate
169
- ```
170
 
171
- ## 4) Install dependencies
 
 
 
172
 
173
- ```bash
174
- pip install -e .
175
- ```
176
 
177
- If editable install is not needed:
 
 
178
 
179
- ```bash
180
- pip install .
181
- ```
182
 
183
- ## Running the Project
184
 
185
- ## Option A: Run policy episodes directly (most common)
 
 
186
 
187
- Run one medium episode:
188
 
189
- ```bash
190
- python inference.py --mode single --task acde_medium --episodes 1 --seed 555
191
- ```
192
 
193
- Run 10 hard episodes:
194
 
195
- ```bash
196
- python inference.py --mode single --task acde_hard --episodes 10 --seed 555
197
- ```
 
198
 
199
- Run all levels in sequence:
200
 
201
- ```bash
202
- python inference.py --mode full --episodes 3 --seed 555
203
- ```
204
 
205
- If you run without `--task`, the script asks for level interactively.
 
 
 
206
 
207
- ## Option B: Run as HTTP service
208
 
209
- Start API server:
210
 
211
- ```bash
212
- uvicorn app.server.app:app --host 0.0.0.0 --port 7860
213
- ```
214
 
215
- Health check:
216
-
217
- ```bash
218
- curl http://127.0.0.1:7860/health
219
- ```
220
 
221
- ## Understanding Output
222
 
223
- During `inference.py` runs, you will see:
 
 
 
 
 
224
 
225
- - Scenario details
226
- - Hospital options and scores
227
- - Decision strategy text
228
- - Outcome per step (`ACCEPTED`, `PARTIAL`, `REJECTED`)
229
- - Final episode summary
230
- - Batch summary (success rate, average score, average steps)
231
-
232
- Example summary:
233
 
234
  ```text
235
- Batch summary:
236
- Success rate: 20.0%
237
- Average score: 0.39
238
- Average steps: 3.6
 
239
  ```
240
 
241
- ## Data Files
242
-
243
- The simulation writes data to `data/`:
244
-
245
- - `learning_memory.json`
246
- - Long-term policy memory
247
- - `trajectory_history.jsonl`
248
- - One JSON object per step
249
- - `learning_archive.json`
250
- - Aggregate run history and profiles
251
-
252
- If you want a clean run baseline, back up and clear these files.
253
-
254
- ## Typical Targets (Guideline)
255
-
256
- These are practical targets, not strict rules:
257
 
258
- - Easy: usually high success, often fewer steps
259
- - Medium: mixed outcomes with meaningful rerouting
260
- - Hard: lower success, more failures, more steps
261
-
262
- If hard success is too high, increase uncertainty or rejection pressure.
263
- If hard success is too low, ease one or two hard-only probabilities.
264
-
265
- ## Troubleshooting
266
-
267
- ## "NameError" or model field errors
268
-
269
- Make sure model fields and observation fields match after logic changes.
270
- If you added new state keys, also add them in observation models.
271
-
272
- ## Script asks for seed/level unexpectedly
273
 
274
- Pass flags explicitly:
275
 
276
  ```bash
277
- python inference.py --mode single --task acde_hard --episodes 10 --seed 555
 
 
 
 
278
  ```
279
 
280
- ## No module named app
281
-
282
- Run commands from inside `my_env` folder, and ensure install succeeded:
283
 
284
  ```bash
285
- pip install -e .
 
286
  ```
287
 
288
- ## Uvicorn command not found
289
 
290
- Install server deps in your active environment:
291
 
292
- ```bash
293
- pip install uvicorn fastapi
294
- ```
 
 
 
 
 
295
 
296
- ## Notes
297
 
298
- - This project is designed for iterative policy tuning.
299
- - Small changes in hard-mode probabilities can noticeably shift success rates.
300
- - Always test with at least 10-30 episodes before concluding behavior changes.
 
 
1
  ---
2
+ title: Acde-Openenv Environment Server
3
+ emoji: 🚑
4
  colorFrom: blue
5
  colorTo: green
6
  sdk: docker
7
  pinned: false
8
+ app_port: 8000
9
  base_path: /web
10
+ tags:
11
+ - openenv
12
  ---
13
 
14
+ # Adaptive Crisis Decision Environment (ACDE)
15
 
16
+ ACDE is an OpenEnv-compliant, real-world emergency dispatch environment for evaluating decision-making under uncertainty. The task models ambulance routing to hospitals under changing traffic, hidden ICU truth, and patient deterioration.
17
 
18
+ ## Why This Is Real-World
 
 
 
 
 
19
 
20
+ This environment simulates a practical operation used by emergency coordinators:
21
 
22
+ - choosing destination hospitals under incomplete information
23
+ - trading off travel time vs ICU availability vs specialization
24
+ - handling a non-stationary world where traffic and capacity shift
25
 
26
+ ## OpenEnv API
27
 
28
+ Implemented in [app/environment/core.py](app/environment/core.py) and exposed via [app/server/app.py](app/server/app.py).
 
 
 
29
 
30
+ - reset(seed, task_id) -> observation
31
+ - step(action) -> observation, reward, done, info
32
+ - state() -> state
33
 
34
+ HTTP endpoints:
35
 
36
+ - GET /health
37
+ - POST /reset
38
+ - POST /step
39
+ - GET /state
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
  ## Action Space
42
 
43
+ Action model: [app/models/action.py](app/models/action.py)
44
 
45
+ - step: int (1..3)
46
+ - hospital_id: string (H1/H2/H3)
47
+ - rationale: optional string
 
 
 
 
 
 
 
 
 
48
 
49
  ## Observation Space
50
 
51
+ Observation model: [app/models/observation.py](app/models/observation.py)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
+ - task_id, task_objective
54
+ - scenario metadata (name, difficulty, condition)
55
+ - required_specialization
56
+ - critical_time_limit_minutes
57
+ - current step and max_steps
58
+ - hospital list:
59
+ - hospital_id
60
+ - distance_km
61
+ - icu (available/unknown)
62
+ - specialization
63
+ - traffic (low/medium/high)
64
 
65
+ ## Reward and Info Models
66
 
67
+ Reward and grader metadata: [app/models/reward.py](app/models/reward.py)
 
68
 
69
+ - RewardModel.value in [0, 1]
70
+ - RewardBreakdown:
71
+ - survival_component
72
+ - time_efficiency_component
73
+ - specialization_component
74
+ - delay_penalty
75
+ - StepInfo includes:
76
+ - last_action_error
77
+ - task_id, difficulty, objective
78
+ - progress_score
79
+ - reward_model
80
+ - grader (final step)
81
 
82
+ ## Tasks and Agent Graders
 
83
 
84
+ Three deterministic tasks are supported:
85
 
86
+ 1. acde_easy
87
+ 2. acde_medium
88
+ 3. acde_hard
89
 
90
+ Task objective and difficulty are fixed by task_id and drive scenario selection.
 
 
 
91
 
92
+ Deterministic grader implementation: [app/environment/graders.py](app/environment/graders.py)
93
 
94
+ Final grader score in [0, 1] is based on:
 
 
 
95
 
96
+ - survival_rate
97
+ - margin_rate (time margin vs critical limit)
98
+ - specialization_rate
99
+ - repeat_failure_penalty
100
 
101
+ Difficulty-specific pass thresholds:
 
 
102
 
103
+ - easy: 0.75
104
+ - medium: 0.65
105
+ - hard: 0.50
106
 
107
+ ## Physics and Dynamics
 
 
108
 
109
+ Travel model:
110
 
111
+ - speed = base_speed * traffic_factor
112
+ - traffic_factor: low=1.0, medium=0.6, high=0.3
113
+ - travel_time_minutes = (distance_km / speed_kmh) * 60
114
 
115
+ Hidden truth:
116
 
117
+ - displayed ICU can be available/unknown
118
+ - actual ICU true/false is hidden and used for survival logic
 
119
 
120
+ Environment changes between steps:
121
 
122
+ - traffic can change
123
+ - ICU truth can flip
124
+ - displayed ICU can drift from truth
125
+ - patient critical time window shrinks
126
 
127
+ ## Reward Function (Partial Signals)
128
 
129
+ Per-step reward in [0, 1] combines:
 
 
130
 
131
+ - survival (primary)
132
+ - time efficiency
133
+ - specialization match
134
+ - delay penalty
135
 
136
+ This gives dense trajectory feedback and penalizes poor choices.
137
 
138
+ ## Reproducibility
139
 
140
+ Seed controls scenario generation, hospital parameters, traffic, and hidden ICU state. Same seed + task_id yields deterministic episode construction.
 
 
141
 
142
+ ## Inference Baseline (Required Format)
 
 
 
 
143
 
144
+ Script: [inference.py](inference.py)
145
 
146
+ - uses OpenAI client for model calls
147
+ - reads API_BASE_URL, MODEL_NAME, HF_TOKEN (or OPENAI_API_KEY)
148
+ - emits strict structured logs:
149
+ - [START]
150
+ - [STEP]
151
+ - [END]
152
 
153
+ Log format example:
 
 
 
 
 
 
 
154
 
155
  ```text
156
+ [START] task=acde_easy env=acde-openenv model=Qwen/Qwen2.5-72B-Instruct
157
+ [STEP] step=1 action=route('H1') reward=0.83 done=false error=null
158
+ [STEP] step=2 action=route('H2') reward=0.54 done=false error=null
159
+ [STEP] step=3 action=route('H1') reward=0.92 done=true error=null
160
+ [END] success=true steps=3 score=0.84 rewards=0.83,0.54,0.92
161
  ```
162
 
163
+ ## Local Setup
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
 
165
+ ```bash
166
+ pip install -e .
167
+ uvicorn app.server.app:app --host 0.0.0.0 --port 7860
168
+ ```
 
 
 
 
 
 
 
 
 
 
 
169
 
170
+ Run inference:
171
 
172
  ```bash
173
+ set API_BASE_URL=https://router.huggingface.co/v1
174
+ set MODEL_NAME=Qwen/Qwen2.5-72B-Instruct
175
+ set HF_TOKEN=***
176
+ set ENV_BASE_URL=http://127.0.0.1:7860
177
+ python inference.py
178
  ```
179
 
180
+ ## Docker
 
 
181
 
182
  ```bash
183
+ docker build -t acde-openenv .
184
+ docker run --rm -p 7860:7860 acde-openenv
185
  ```
186
 
187
+ Target runtime envelope: 2 vCPU, 8 GB RAM.
188
 
189
+ ## Hugging Face Spaces Deployment
190
 
191
+ 1. Create a new Docker Space.
192
+ 2. Push this repository as the Space source.
193
+ 3. Set Space metadata tag to openenv.
194
+ 4. Add Secrets/Variables:
195
+ - API_BASE_URL
196
+ - MODEL_NAME
197
+ - HF_TOKEN
198
+ 5. Space should expose port 7860 and respond to /health and /reset.
199
 
200
+ ## Validation Checklist
201
 
202
+ - openenv validate passes
203
+ - docker build succeeds
204
+ - /health, /reset, /step, /state respond
205
+ - inference.py runs end-to-end and prints required structured logs
app/environment/core.py CHANGED
@@ -676,7 +676,7 @@ class EmergencyEnv:
676
  status="rejected",
677
  reason="Hidden mismatch at arrival (wrong risky guess). Rerouting required.",
678
  validation_details=arrival_outcome.validation_details,
679
- reward_modifier=0.0,
680
  )
681
  return (
682
  forced_reject,
@@ -754,7 +754,7 @@ class EmergencyEnv:
754
  status="rejected",
755
  reason=reason,
756
  validation_details=new_validation,
757
- reward_modifier=0.0,
758
  ),
759
  0.12,
760
  f"Hidden case: {reason}. Rerouting required.",
@@ -805,7 +805,7 @@ class EmergencyEnv:
805
  status="rejected",
806
  reason="Condition became irreversible after delays",
807
  validation_details=arrival_outcome.validation_details,
808
- reward_modifier=0.0,
809
  ),
810
  "Partial chain cap: condition became irreversible.",
811
  )
@@ -883,7 +883,7 @@ class EmergencyEnv:
883
  status="rejected",
884
  reason="Condition relapsed after temporary stabilization",
885
  validation_details=arrival_outcome.validation_details,
886
- reward_modifier=0.0,
887
  terminal=False,
888
  ),
889
  "Recovery guard: partial relapsed to rejected.",
 
676
  status="rejected",
677
  reason="Hidden mismatch at arrival (wrong risky guess). Rerouting required.",
678
  validation_details=arrival_outcome.validation_details,
679
+ reward_modifier=0.001,
680
  )
681
  return (
682
  forced_reject,
 
754
  status="rejected",
755
  reason=reason,
756
  validation_details=new_validation,
757
+ reward_modifier=0.001,
758
  ),
759
  0.12,
760
  f"Hidden case: {reason}. Rerouting required.",
 
805
  status="rejected",
806
  reason="Condition became irreversible after delays",
807
  validation_details=arrival_outcome.validation_details,
808
+ reward_modifier=0.001,
809
  ),
810
  "Partial chain cap: condition became irreversible.",
811
  )
 
883
  status="rejected",
884
  reason="Condition relapsed after temporary stabilization",
885
  validation_details=arrival_outcome.validation_details,
886
+ reward_modifier=0.001,
887
  terminal=False,
888
  ),
889
  "Recovery guard: partial relapsed to rejected.",
app/environment/graders.py CHANGED
@@ -1,3 +1,4 @@
 
1
  from app.models.reward import GraderResult
2
 
3
 
@@ -39,7 +40,7 @@ def grade_task(
39
  margin_rate = sum(
40
  _norm_margin(t.get("travel_time", 0.0), t.get("critical_limit", 1.0))
41
  for t in trajectory
42
- ) / steps if trajectory else 0.0
43
 
44
  # Penalty for repeated failures at same hospital
45
  repeat_failures = 0
@@ -90,8 +91,8 @@ def grade_task(
90
  score = max(MIN_SCORE, min(MAX_SCORE, score))
91
 
92
  return GraderResult(
93
- task_id=task_id,
94
- difficulty=difficulty,
95
  objective=objective,
96
  score=score,
97
  passed=score >= threshold,
 
1
+ from typing import Literal, cast
2
  from app.models.reward import GraderResult
3
 
4
 
 
40
  margin_rate = sum(
41
  _norm_margin(t.get("travel_time", 0.0), t.get("critical_limit", 1.0))
42
  for t in trajectory
43
+ ) / steps if trajectory else MIN_SCORE
44
 
45
  # Penalty for repeated failures at same hospital
46
  repeat_failures = 0
 
91
  score = max(MIN_SCORE, min(MAX_SCORE, score))
92
 
93
  return GraderResult(
94
+ task_id=cast(Literal["acde_easy", "acde_hard", "acde_medium"], task_id),
95
+ difficulty=cast(Literal["easy", "hard", "medium"], difficulty),
96
  objective=objective,
97
  score=score,
98
  passed=score >= threshold,
app/environment/validation.py CHANGED
@@ -4,6 +4,7 @@ Simulates hidden validation checks performed when an ambulance arrives at a hosp
4
  Outcomes are based on difficulty level, hospital capacity, patient suitability, and randomness.
5
  """
6
 
 
7
  from app.models.state import ArrivalOutcome, HospitalValidationDetails, HospitalState
8
  from app.utils.randomizer import SeededRandomizer
9
 
@@ -64,7 +65,7 @@ class HospitalValidator:
64
  icu_available=icu_available,
65
  doctor_available=doctor_available,
66
  equipment_functional=equipment_functional,
67
- overload_status=overload_status,
68
  patient_suitability=patient_suitability,
69
  )
70
 
@@ -79,7 +80,7 @@ class HospitalValidator:
79
  )
80
 
81
  return ArrivalOutcome(
82
- status=status,
83
  reason=reason,
84
  validation_details=validation_details,
85
  reward_modifier=reward_modifier,
@@ -190,7 +191,7 @@ class HospitalValidator:
190
  # Add difficulty-based noise
191
  if difficulty == "hard":
192
  noise = self.rng.uniform(-0.15, 0.15)
193
- suitability = max(0.0, min(1.0, suitability + noise))
194
 
195
  return suitability
196
 
@@ -269,7 +270,7 @@ class HospitalValidator:
269
  return (
270
  "rejected",
271
  f"Hospital cannot admit: {', '.join(rejection_reasons[:2])}",
272
- 0.0,
273
  False,
274
  )
275
 
@@ -345,7 +346,7 @@ class HospitalValidator:
345
  return (
346
  "rejected",
347
  "Condition became non-transferable during delay; immediate critical care failed",
348
- 0.0,
349
  True,
350
  )
351
 
@@ -376,7 +377,7 @@ class HospitalValidator:
376
  return (
377
  "rejected",
378
  "Unexpected complication at arrival",
379
- 0.0,
380
  False,
381
  )
382
 
 
4
  Outcomes are based on difficulty level, hospital capacity, patient suitability, and randomness.
5
  """
6
 
7
+ from typing import Literal, cast
8
  from app.models.state import ArrivalOutcome, HospitalValidationDetails, HospitalState
9
  from app.utils.randomizer import SeededRandomizer
10
 
 
65
  icu_available=icu_available,
66
  doctor_available=doctor_available,
67
  equipment_functional=equipment_functional,
68
+ overload_status=cast(Literal["clear", "moderate", "severe"], overload_status),
69
  patient_suitability=patient_suitability,
70
  )
71
 
 
80
  )
81
 
82
  return ArrivalOutcome(
83
+ status=cast(Literal["accepted", "partial", "rejected"], status),
84
  reason=reason,
85
  validation_details=validation_details,
86
  reward_modifier=reward_modifier,
 
191
  # Add difficulty-based noise
192
  if difficulty == "hard":
193
  noise = self.rng.uniform(-0.15, 0.15)
194
+ suitability = max(0.001, min(0.999, suitability + noise))
195
 
196
  return suitability
197
 
 
270
  return (
271
  "rejected",
272
  f"Hospital cannot admit: {', '.join(rejection_reasons[:2])}",
273
+ 0.001,
274
  False,
275
  )
276
 
 
346
  return (
347
  "rejected",
348
  "Condition became non-transferable during delay; immediate critical care failed",
349
+ 0.001,
350
  True,
351
  )
352
 
 
377
  return (
378
  "rejected",
379
  "Unexpected complication at arrival",
380
+ 0.001,
381
  False,
382
  )
383
 
app/models/observation.py CHANGED
@@ -15,7 +15,7 @@ class ArrivalOutcomeObservation(BaseModel):
15
  """What happened when ambulance arrived at hospital"""
16
  status: Literal["accepted", "partial", "rejected"]
17
  reason: str
18
- suitability_score: float = Field(ge=0.0, le=1.0)
19
 
20
 
21
  class Observation(BaseModel):
 
15
  """What happened when ambulance arrived at hospital"""
16
  status: Literal["accepted", "partial", "rejected"]
17
  reason: str
18
+ suitability_score: float = Field(ge=0.001, le=0.999)
19
 
20
 
21
  class Observation(BaseModel):
app/models/state.py CHANGED
@@ -6,7 +6,7 @@ from pydantic import BaseModel, Field
6
  class LearningEntry(BaseModel):
7
  success: int = Field(default=0, ge=0)
8
  fail: int = Field(default=0, ge=0)
9
- avg: float = Field(default=0.0, ge=0.0, le=1.0)
10
  accepted: int = Field(default=0, ge=0)
11
  rejected: int = Field(default=0, ge=0)
12
 
@@ -17,7 +17,7 @@ class HospitalValidationDetails(BaseModel):
17
  doctor_available: bool
18
  equipment_functional: bool
19
  overload_status: Literal["clear", "moderate", "severe"]
20
- patient_suitability: float = Field(ge=0.0, le=1.0) # 0=unsuitable, 1=ideal
21
 
22
 
23
  class ArrivalOutcome(BaseModel):
@@ -25,7 +25,7 @@ class ArrivalOutcome(BaseModel):
25
  status: Literal["accepted", "partial", "rejected"]
26
  reason: str
27
  validation_details: HospitalValidationDetails | None = None
28
- reward_modifier: float = Field(default=1.0, ge=0.0, le=1.5)
29
  terminal: bool = False
30
 
31
 
 
6
  class LearningEntry(BaseModel):
7
  success: int = Field(default=0, ge=0)
8
  fail: int = Field(default=0, ge=0)
9
+ avg: float = Field(default=0.001, ge=0.001, le=0.999)
10
  accepted: int = Field(default=0, ge=0)
11
  rejected: int = Field(default=0, ge=0)
12
 
 
17
  doctor_available: bool
18
  equipment_functional: bool
19
  overload_status: Literal["clear", "moderate", "severe"]
20
+ patient_suitability: float = Field(ge=0.001, le=0.999) # near-0=unsuitable, near-1=ideal
21
 
22
 
23
  class ArrivalOutcome(BaseModel):
 
25
  status: Literal["accepted", "partial", "rejected"]
26
  reason: str
27
  validation_details: HospitalValidationDetails | None = None
28
+ reward_modifier: float = Field(default=1.0, ge=0.001, le=1.5)
29
  terminal: bool = False
30
 
31
 
data/learning_archive.json CHANGED
@@ -8086,6 +8086,643 @@
8086
  "best_scenario_name": "Wildfire Front (Evacuation Gridlock)",
8087
  "best_difficulty": "hard",
8088
  "best_required_specialization": "general"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8089
  }
8090
  },
8091
  "episodes": [
@@ -10137,6 +10774,142 @@
10137
  "H5"
10138
  ],
10139
  "timestamp": "2026-04-09T05:19:13.261267+00:00"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10140
  }
10141
  ]
10142
  }
 
8086
  "best_scenario_name": "Wildfire Front (Evacuation Gridlock)",
8087
  "best_difficulty": "hard",
8088
  "best_required_specialization": "general"
8089
+ },
8090
+ "509813872|acde_easy": {
8091
+ "attempts": 1,
8092
+ "best_score": 0.5568666666666667,
8093
+ "best_actions": [
8094
+ "H3",
8095
+ "H1",
8096
+ "H3"
8097
+ ],
8098
+ "best_steps": 3,
8099
+ "step_stats": {
8100
+ "1": {
8101
+ "H3": {
8102
+ "count": 1,
8103
+ "success": 0,
8104
+ "accepted": 0,
8105
+ "partial": 1,
8106
+ "rejected": 0,
8107
+ "total_reward": 0.15500000000000003,
8108
+ "avg_reward": 0.15500000000000003,
8109
+ "last_status": "PARTIAL",
8110
+ "last_reason": "Early rejection mitigated by emergency field stabilization",
8111
+ "success_rate": 0.0
8112
+ }
8113
+ },
8114
+ "2": {
8115
+ "H1": {
8116
+ "count": 1,
8117
+ "success": 0,
8118
+ "accepted": 0,
8119
+ "partial": 0,
8120
+ "rejected": 1,
8121
+ "total_reward": 0.139,
8122
+ "avg_reward": 0.139,
8123
+ "last_status": "REJECTED",
8124
+ "last_reason": "Hospital cannot admit: ICU unavailable, Hospital overloaded",
8125
+ "success_rate": 0.0
8126
+ }
8127
+ },
8128
+ "3": {
8129
+ "H3": {
8130
+ "count": 1,
8131
+ "success": 1,
8132
+ "accepted": 1,
8133
+ "partial": 0,
8134
+ "rejected": 0,
8135
+ "total_reward": 0.7160000000000001,
8136
+ "avg_reward": 0.7160000000000001,
8137
+ "last_status": "ACCEPTED",
8138
+ "last_reason": "Patient stabilized after delayed admission",
8139
+ "success_rate": 1.0
8140
+ }
8141
+ }
8142
+ },
8143
+ "last_score": 0.5568666666666667,
8144
+ "last_success": true,
8145
+ "last_run_at": "2026-04-09T08:39:45.824383+00:00",
8146
+ "last_actions": [
8147
+ "H3",
8148
+ "H1",
8149
+ "H3"
8150
+ ],
8151
+ "last_required_specialization": "trauma",
8152
+ "last_scenario_type": "accident",
8153
+ "last_scenario_name": "Highway Collision (Severe)",
8154
+ "best_success": true,
8155
+ "best_scenario_name": "Highway Collision (Severe)",
8156
+ "best_difficulty": "easy",
8157
+ "best_required_specialization": "trauma"
8158
+ },
8159
+ "509813873|acde_medium": {
8160
+ "attempts": 1,
8161
+ "best_score": 0.15059999999999998,
8162
+ "best_actions": [
8163
+ "H3"
8164
+ ],
8165
+ "best_steps": 1,
8166
+ "step_stats": {
8167
+ "1": {
8168
+ "H3": {
8169
+ "count": 1,
8170
+ "success": 0,
8171
+ "accepted": 0,
8172
+ "partial": 0,
8173
+ "rejected": 1,
8174
+ "total_reward": 0.001,
8175
+ "avg_reward": 0.001,
8176
+ "last_status": "REJECTED",
8177
+ "last_reason": "Condition became non-transferable during delay; immediate critical care failed",
8178
+ "success_rate": 0.0
8179
+ }
8180
+ }
8181
+ },
8182
+ "last_score": 0.15059999999999998,
8183
+ "last_success": false,
8184
+ "last_run_at": "2026-04-09T08:39:46.224541+00:00",
8185
+ "last_actions": [
8186
+ "H3"
8187
+ ],
8188
+ "last_required_specialization": "general",
8189
+ "last_scenario_type": "fire",
8190
+ "last_scenario_name": "Factory Fire (Chemical Exposure)",
8191
+ "best_success": false,
8192
+ "best_scenario_name": "Factory Fire (Chemical Exposure)",
8193
+ "best_difficulty": "medium",
8194
+ "best_required_specialization": "general"
8195
+ },
8196
+ "509813874|acde_hard": {
8197
+ "attempts": 1,
8198
+ "best_score": 0.15297499999999997,
8199
+ "best_actions": [
8200
+ "H1",
8201
+ "H6",
8202
+ "H4",
8203
+ "H6"
8204
+ ],
8205
+ "best_steps": 4,
8206
+ "step_stats": {
8207
+ "1": {
8208
+ "H1": {
8209
+ "count": 1,
8210
+ "success": 0,
8211
+ "accepted": 0,
8212
+ "partial": 0,
8213
+ "rejected": 1,
8214
+ "total_reward": 0.001,
8215
+ "avg_reward": 0.001,
8216
+ "last_status": "REJECTED",
8217
+ "last_reason": "Hospital cannot admit: ICU unavailable, Hospital overloaded",
8218
+ "success_rate": 0.0
8219
+ }
8220
+ },
8221
+ "2": {
8222
+ "H6": {
8223
+ "count": 1,
8224
+ "success": 0,
8225
+ "accepted": 0,
8226
+ "partial": 0,
8227
+ "rejected": 1,
8228
+ "total_reward": 0.039,
8229
+ "avg_reward": 0.039,
8230
+ "last_status": "REJECTED",
8231
+ "last_reason": "Hospital cannot admit: ICU unavailable",
8232
+ "success_rate": 0.0
8233
+ }
8234
+ },
8235
+ "3": {
8236
+ "H4": {
8237
+ "count": 1,
8238
+ "success": 0,
8239
+ "accepted": 0,
8240
+ "partial": 0,
8241
+ "rejected": 1,
8242
+ "total_reward": 0.001,
8243
+ "avg_reward": 0.001,
8244
+ "last_status": "REJECTED",
8245
+ "last_reason": "Hospital cannot admit: ICU unavailable",
8246
+ "success_rate": 0.0
8247
+ }
8248
+ },
8249
+ "4": {
8250
+ "H6": {
8251
+ "count": 1,
8252
+ "success": 0,
8253
+ "accepted": 0,
8254
+ "partial": 0,
8255
+ "rejected": 1,
8256
+ "total_reward": 0.001,
8257
+ "avg_reward": 0.001,
8258
+ "last_status": "REJECTED",
8259
+ "last_reason": "Hidden mismatch at arrival (wrong risky guess). Rerouting required.",
8260
+ "success_rate": 0.0
8261
+ }
8262
+ }
8263
+ },
8264
+ "last_score": 0.15297499999999997,
8265
+ "last_success": false,
8266
+ "last_run_at": "2026-04-09T08:39:47.915419+00:00",
8267
+ "last_actions": [
8268
+ "H1",
8269
+ "H6",
8270
+ "H4",
8271
+ "H6"
8272
+ ],
8273
+ "last_required_specialization": "general",
8274
+ "last_scenario_type": "fire",
8275
+ "last_scenario_name": "Wildfire Front (Evacuation Gridlock)",
8276
+ "best_success": false,
8277
+ "best_scenario_name": "Wildfire Front (Evacuation Gridlock)",
8278
+ "best_difficulty": "hard",
8279
+ "best_required_specialization": "general"
8280
+ },
8281
+ "579018919|acde_easy": {
8282
+ "attempts": 1,
8283
+ "best_score": 0.4829,
8284
+ "best_actions": [
8285
+ "H4",
8286
+ "H1",
8287
+ "H4"
8288
+ ],
8289
+ "best_steps": 3,
8290
+ "step_stats": {
8291
+ "1": {
8292
+ "H4": {
8293
+ "count": 1,
8294
+ "success": 0,
8295
+ "accepted": 0,
8296
+ "partial": 1,
8297
+ "rejected": 0,
8298
+ "total_reward": 0.30400000000000005,
8299
+ "avg_reward": 0.30400000000000005,
8300
+ "last_status": "PARTIAL",
8301
+ "last_reason": "Early rejection mitigated by emergency field stabilization",
8302
+ "success_rate": 0.0
8303
+ }
8304
+ },
8305
+ "2": {
8306
+ "H1": {
8307
+ "count": 1,
8308
+ "success": 0,
8309
+ "accepted": 0,
8310
+ "partial": 0,
8311
+ "rejected": 1,
8312
+ "total_reward": 0.109,
8313
+ "avg_reward": 0.109,
8314
+ "last_status": "REJECTED",
8315
+ "last_reason": "Condition became irreversible after delays",
8316
+ "success_rate": 0.0
8317
+ }
8318
+ },
8319
+ "3": {
8320
+ "H4": {
8321
+ "count": 1,
8322
+ "success": 1,
8323
+ "accepted": 1,
8324
+ "partial": 0,
8325
+ "rejected": 0,
8326
+ "total_reward": 0.562,
8327
+ "avg_reward": 0.562,
8328
+ "last_status": "ACCEPTED",
8329
+ "last_reason": "Condition stabilized after progressive treatment",
8330
+ "success_rate": 1.0
8331
+ }
8332
+ }
8333
+ },
8334
+ "last_score": 0.4829,
8335
+ "last_success": true,
8336
+ "last_run_at": "2026-04-09T08:42:06.312270+00:00",
8337
+ "last_actions": [
8338
+ "H4",
8339
+ "H1",
8340
+ "H4"
8341
+ ],
8342
+ "last_required_specialization": "general",
8343
+ "last_scenario_type": "fire",
8344
+ "last_scenario_name": "Apartment Fire (Smoke Inhalation)",
8345
+ "best_success": true,
8346
+ "best_scenario_name": "Apartment Fire (Smoke Inhalation)",
8347
+ "best_difficulty": "easy",
8348
+ "best_required_specialization": "general"
8349
+ },
8350
+ "579018920|acde_medium": {
8351
+ "attempts": 1,
8352
+ "best_score": 0.17291249999999997,
8353
+ "best_actions": [
8354
+ "H3",
8355
+ "H5",
8356
+ "H1",
8357
+ "H4"
8358
+ ],
8359
+ "best_steps": 4,
8360
+ "step_stats": {
8361
+ "1": {
8362
+ "H3": {
8363
+ "count": 1,
8364
+ "success": 0,
8365
+ "accepted": 0,
8366
+ "partial": 0,
8367
+ "rejected": 1,
8368
+ "total_reward": 0.001,
8369
+ "avg_reward": 0.001,
8370
+ "last_status": "REJECTED",
8371
+ "last_reason": "Hospital cannot admit: ICU unavailable",
8372
+ "success_rate": 0.0
8373
+ }
8374
+ },
8375
+ "2": {
8376
+ "H5": {
8377
+ "count": 1,
8378
+ "success": 0,
8379
+ "accepted": 0,
8380
+ "partial": 1,
8381
+ "rejected": 0,
8382
+ "total_reward": 0.358,
8383
+ "avg_reward": 0.358,
8384
+ "last_status": "PARTIAL",
8385
+ "last_reason": "Admitted with significant risk: No specialist available",
8386
+ "success_rate": 0.0
8387
+ }
8388
+ },
8389
+ "3": {
8390
+ "H1": {
8391
+ "count": 1,
8392
+ "success": 0,
8393
+ "accepted": 0,
8394
+ "partial": 0,
8395
+ "rejected": 1,
8396
+ "total_reward": 0.001,
8397
+ "avg_reward": 0.001,
8398
+ "last_status": "REJECTED",
8399
+ "last_reason": "Hospital cannot admit: ICU unavailable",
8400
+ "success_rate": 0.0
8401
+ }
8402
+ },
8403
+ "4": {
8404
+ "H4": {
8405
+ "count": 1,
8406
+ "success": 0,
8407
+ "accepted": 0,
8408
+ "partial": 0,
8409
+ "rejected": 1,
8410
+ "total_reward": 0.001,
8411
+ "avg_reward": 0.001,
8412
+ "last_status": "REJECTED",
8413
+ "last_reason": "Hidden mismatch at arrival (wrong risky guess). Rerouting required.",
8414
+ "success_rate": 0.0
8415
+ }
8416
+ }
8417
+ },
8418
+ "last_score": 0.17291249999999997,
8419
+ "last_success": false,
8420
+ "last_run_at": "2026-04-09T08:42:08.231621+00:00",
8421
+ "last_actions": [
8422
+ "H3",
8423
+ "H5",
8424
+ "H1",
8425
+ "H4"
8426
+ ],
8427
+ "last_required_specialization": "general",
8428
+ "last_scenario_type": "fire",
8429
+ "last_scenario_name": "Factory Fire (Chemical Exposure)",
8430
+ "best_success": false,
8431
+ "best_scenario_name": "Factory Fire (Chemical Exposure)",
8432
+ "best_difficulty": "medium",
8433
+ "best_required_specialization": "general"
8434
+ },
8435
+ "579018921|acde_hard": {
8436
+ "attempts": 1,
8437
+ "best_score": 0.3184875000000001,
8438
+ "best_actions": [
8439
+ "H1",
8440
+ "H6",
8441
+ "H3",
8442
+ "H5"
8443
+ ],
8444
+ "best_steps": 4,
8445
+ "step_stats": {
8446
+ "1": {
8447
+ "H1": {
8448
+ "count": 1,
8449
+ "success": 0,
8450
+ "accepted": 0,
8451
+ "partial": 0,
8452
+ "rejected": 1,
8453
+ "total_reward": 0.001,
8454
+ "avg_reward": 0.001,
8455
+ "last_status": "REJECTED",
8456
+ "last_reason": "Hospital cannot admit: ICU unavailable",
8457
+ "success_rate": 0.0
8458
+ }
8459
+ },
8460
+ "2": {
8461
+ "H6": {
8462
+ "count": 1,
8463
+ "success": 0,
8464
+ "accepted": 0,
8465
+ "partial": 0,
8466
+ "rejected": 1,
8467
+ "total_reward": 0.001,
8468
+ "avg_reward": 0.001,
8469
+ "last_status": "REJECTED",
8470
+ "last_reason": "Hospital cannot admit: Hospital overloaded",
8471
+ "success_rate": 0.0
8472
+ }
8473
+ },
8474
+ "3": {
8475
+ "H3": {
8476
+ "count": 1,
8477
+ "success": 0,
8478
+ "accepted": 0,
8479
+ "partial": 0,
8480
+ "rejected": 1,
8481
+ "total_reward": 0.001,
8482
+ "avg_reward": 0.001,
8483
+ "last_status": "REJECTED",
8484
+ "last_reason": "Hospital cannot admit: Hospital overloaded",
8485
+ "success_rate": 0.0
8486
+ }
8487
+ },
8488
+ "4": {
8489
+ "H5": {
8490
+ "count": 1,
8491
+ "success": 0,
8492
+ "accepted": 0,
8493
+ "partial": 1,
8494
+ "rejected": 0,
8495
+ "total_reward": 0.40800000000000014,
8496
+ "avg_reward": 0.40800000000000014,
8497
+ "last_status": "PARTIAL",
8498
+ "last_reason": "Admitted with delays: prolonged transfer strain",
8499
+ "success_rate": 0.0
8500
+ }
8501
+ }
8502
+ },
8503
+ "last_score": 0.3184875000000001,
8504
+ "last_success": false,
8505
+ "last_run_at": "2026-04-09T08:42:10.003857+00:00",
8506
+ "last_actions": [
8507
+ "H1",
8508
+ "H6",
8509
+ "H3",
8510
+ "H5"
8511
+ ],
8512
+ "last_required_specialization": "general",
8513
+ "last_scenario_type": "fire",
8514
+ "last_scenario_name": "Wildfire Front (Evacuation Gridlock)",
8515
+ "best_success": false,
8516
+ "best_scenario_name": "Wildfire Front (Evacuation Gridlock)",
8517
+ "best_difficulty": "hard",
8518
+ "best_required_specialization": "general"
8519
+ },
8520
+ "468098215|acde_easy": {
8521
+ "attempts": 1,
8522
+ "best_score": 0.23815,
8523
+ "best_actions": [
8524
+ "H3",
8525
+ "H4",
8526
+ "H1"
8527
+ ],
8528
+ "best_steps": 3,
8529
+ "step_stats": {
8530
+ "1": {
8531
+ "H3": {
8532
+ "count": 1,
8533
+ "success": 0,
8534
+ "accepted": 0,
8535
+ "partial": 1,
8536
+ "rejected": 0,
8537
+ "total_reward": 0.334,
8538
+ "avg_reward": 0.334,
8539
+ "last_status": "PARTIAL",
8540
+ "last_reason": "Critical deterioration managed temporarily; reroute still needed",
8541
+ "success_rate": 0.0
8542
+ }
8543
+ },
8544
+ "2": {
8545
+ "H4": {
8546
+ "count": 1,
8547
+ "success": 0,
8548
+ "accepted": 0,
8549
+ "partial": 0,
8550
+ "rejected": 1,
8551
+ "total_reward": 0.001,
8552
+ "avg_reward": 0.001,
8553
+ "last_status": "REJECTED",
8554
+ "last_reason": "Emergency beds became unavailable during arrival",
8555
+ "success_rate": 0.0
8556
+ }
8557
+ },
8558
+ "3": {
8559
+ "H1": {
8560
+ "count": 1,
8561
+ "success": 0,
8562
+ "accepted": 0,
8563
+ "partial": 0,
8564
+ "rejected": 1,
8565
+ "total_reward": 0.139,
8566
+ "avg_reward": 0.139,
8567
+ "last_status": "REJECTED",
8568
+ "last_reason": "Condition became non-transferable during delay; immediate critical care failed",
8569
+ "success_rate": 0.0
8570
+ }
8571
+ }
8572
+ },
8573
+ "last_score": 0.23815,
8574
+ "last_success": false,
8575
+ "last_run_at": "2026-04-09T08:48:34.391142+00:00",
8576
+ "last_actions": [
8577
+ "H3",
8578
+ "H4",
8579
+ "H1"
8580
+ ],
8581
+ "last_required_specialization": "general",
8582
+ "last_scenario_type": "fire",
8583
+ "last_scenario_name": "Apartment Fire (Smoke Inhalation)",
8584
+ "best_success": false,
8585
+ "best_scenario_name": "Apartment Fire (Smoke Inhalation)",
8586
+ "best_difficulty": "easy",
8587
+ "best_required_specialization": "general"
8588
+ },
8589
+ "468098216|acde_medium": {
8590
+ "attempts": 1,
8591
+ "best_score": 0.7869000000000002,
8592
+ "best_actions": [
8593
+ "H2",
8594
+ "H3"
8595
+ ],
8596
+ "best_steps": 2,
8597
+ "step_stats": {
8598
+ "1": {
8599
+ "H2": {
8600
+ "count": 1,
8601
+ "success": 0,
8602
+ "accepted": 0,
8603
+ "partial": 1,
8604
+ "rejected": 0,
8605
+ "total_reward": 0.37,
8606
+ "avg_reward": 0.37,
8607
+ "last_status": "PARTIAL",
8608
+ "last_reason": "Initial triage completed; transfer monitoring still required",
8609
+ "success_rate": 0.0
8610
+ }
8611
+ },
8612
+ "2": {
8613
+ "H3": {
8614
+ "count": 1,
8615
+ "success": 1,
8616
+ "accepted": 1,
8617
+ "partial": 0,
8618
+ "rejected": 0,
8619
+ "total_reward": 0.8660000000000001,
8620
+ "avg_reward": 0.8660000000000001,
8621
+ "last_status": "ACCEPTED",
8622
+ "last_reason": "Patient stabilized after delayed admission",
8623
+ "success_rate": 1.0
8624
+ }
8625
+ }
8626
+ },
8627
+ "last_score": 0.7869000000000002,
8628
+ "last_success": true,
8629
+ "last_run_at": "2026-04-09T08:48:35.228474+00:00",
8630
+ "last_actions": [
8631
+ "H2",
8632
+ "H3"
8633
+ ],
8634
+ "last_required_specialization": "trauma",
8635
+ "last_scenario_type": "accident",
8636
+ "last_scenario_name": "Urban Pile-up (Rush Hour)",
8637
+ "best_success": true,
8638
+ "best_scenario_name": "Urban Pile-up (Rush Hour)",
8639
+ "best_difficulty": "medium",
8640
+ "best_required_specialization": "trauma"
8641
+ },
8642
+ "468098217|acde_hard": {
8643
+ "attempts": 1,
8644
+ "best_score": 0.15059999999999998,
8645
+ "best_actions": [
8646
+ "H2",
8647
+ "H3",
8648
+ "H5",
8649
+ "H2"
8650
+ ],
8651
+ "best_steps": 4,
8652
+ "step_stats": {
8653
+ "1": {
8654
+ "H2": {
8655
+ "count": 1,
8656
+ "success": 0,
8657
+ "accepted": 0,
8658
+ "partial": 0,
8659
+ "rejected": 1,
8660
+ "total_reward": 0.001,
8661
+ "avg_reward": 0.001,
8662
+ "last_status": "REJECTED",
8663
+ "last_reason": "Hospital cannot admit: Hospital overloaded",
8664
+ "success_rate": 0.0
8665
+ }
8666
+ },
8667
+ "2": {
8668
+ "H3": {
8669
+ "count": 1,
8670
+ "success": 0,
8671
+ "accepted": 0,
8672
+ "partial": 0,
8673
+ "rejected": 1,
8674
+ "total_reward": 0.001,
8675
+ "avg_reward": 0.001,
8676
+ "last_status": "REJECTED",
8677
+ "last_reason": "Hidden mismatch at arrival (wrong risky guess). Rerouting required.",
8678
+ "success_rate": 0.0
8679
+ }
8680
+ },
8681
+ "3": {
8682
+ "H5": {
8683
+ "count": 1,
8684
+ "success": 0,
8685
+ "accepted": 0,
8686
+ "partial": 0,
8687
+ "rejected": 1,
8688
+ "total_reward": 0.001,
8689
+ "avg_reward": 0.001,
8690
+ "last_status": "REJECTED",
8691
+ "last_reason": "Hidden mismatch at arrival (wrong risky guess). Rerouting required.",
8692
+ "success_rate": 0.0
8693
+ }
8694
+ },
8695
+ "4": {
8696
+ "H2": {
8697
+ "count": 1,
8698
+ "success": 0,
8699
+ "accepted": 0,
8700
+ "partial": 0,
8701
+ "rejected": 1,
8702
+ "total_reward": 0.001,
8703
+ "avg_reward": 0.001,
8704
+ "last_status": "REJECTED",
8705
+ "last_reason": "Hospital cannot admit: ICU unavailable, No specialist available",
8706
+ "success_rate": 0.0
8707
+ }
8708
+ }
8709
+ },
8710
+ "last_score": 0.15059999999999998,
8711
+ "last_success": false,
8712
+ "last_run_at": "2026-04-09T08:48:36.666940+00:00",
8713
+ "last_actions": [
8714
+ "H2",
8715
+ "H3",
8716
+ "H5",
8717
+ "H2"
8718
+ ],
8719
+ "last_required_specialization": "cardiac",
8720
+ "last_scenario_type": "medical",
8721
+ "last_scenario_name": "Mass Cardiac Event (Overload)",
8722
+ "best_success": false,
8723
+ "best_scenario_name": "Mass Cardiac Event (Overload)",
8724
+ "best_difficulty": "hard",
8725
+ "best_required_specialization": "cardiac"
8726
  }
8727
  },
8728
  "episodes": [
 
10774
  "H5"
10775
  ],
10776
  "timestamp": "2026-04-09T05:19:13.261267+00:00"
10777
+ },
10778
+ {
10779
+ "seed": 509813872,
10780
+ "task_id": "acde_easy",
10781
+ "difficulty": "easy",
10782
+ "required_specialization": "trauma",
10783
+ "scenario_name": "Highway Collision (Severe)",
10784
+ "score": 0.5568666666666667,
10785
+ "success": true,
10786
+ "actions": [
10787
+ "H3",
10788
+ "H1",
10789
+ "H3"
10790
+ ],
10791
+ "timestamp": "2026-04-09T08:39:45.824383+00:00"
10792
+ },
10793
+ {
10794
+ "seed": 509813873,
10795
+ "task_id": "acde_medium",
10796
+ "difficulty": "medium",
10797
+ "required_specialization": "general",
10798
+ "scenario_name": "Factory Fire (Chemical Exposure)",
10799
+ "score": 0.15059999999999998,
10800
+ "success": false,
10801
+ "actions": [
10802
+ "H3"
10803
+ ],
10804
+ "timestamp": "2026-04-09T08:39:46.224541+00:00"
10805
+ },
10806
+ {
10807
+ "seed": 509813874,
10808
+ "task_id": "acde_hard",
10809
+ "difficulty": "hard",
10810
+ "required_specialization": "general",
10811
+ "scenario_name": "Wildfire Front (Evacuation Gridlock)",
10812
+ "score": 0.15297499999999997,
10813
+ "success": false,
10814
+ "actions": [
10815
+ "H1",
10816
+ "H6",
10817
+ "H4",
10818
+ "H6"
10819
+ ],
10820
+ "timestamp": "2026-04-09T08:39:47.915419+00:00"
10821
+ },
10822
+ {
10823
+ "seed": 579018919,
10824
+ "task_id": "acde_easy",
10825
+ "difficulty": "easy",
10826
+ "required_specialization": "general",
10827
+ "scenario_name": "Apartment Fire (Smoke Inhalation)",
10828
+ "score": 0.4829,
10829
+ "success": true,
10830
+ "actions": [
10831
+ "H4",
10832
+ "H1",
10833
+ "H4"
10834
+ ],
10835
+ "timestamp": "2026-04-09T08:42:06.312270+00:00"
10836
+ },
10837
+ {
10838
+ "seed": 579018920,
10839
+ "task_id": "acde_medium",
10840
+ "difficulty": "medium",
10841
+ "required_specialization": "general",
10842
+ "scenario_name": "Factory Fire (Chemical Exposure)",
10843
+ "score": 0.17291249999999997,
10844
+ "success": false,
10845
+ "actions": [
10846
+ "H3",
10847
+ "H5",
10848
+ "H1",
10849
+ "H4"
10850
+ ],
10851
+ "timestamp": "2026-04-09T08:42:08.231621+00:00"
10852
+ },
10853
+ {
10854
+ "seed": 579018921,
10855
+ "task_id": "acde_hard",
10856
+ "difficulty": "hard",
10857
+ "required_specialization": "general",
10858
+ "scenario_name": "Wildfire Front (Evacuation Gridlock)",
10859
+ "score": 0.3184875000000001,
10860
+ "success": false,
10861
+ "actions": [
10862
+ "H1",
10863
+ "H6",
10864
+ "H3",
10865
+ "H5"
10866
+ ],
10867
+ "timestamp": "2026-04-09T08:42:10.003857+00:00"
10868
+ },
10869
+ {
10870
+ "seed": 468098215,
10871
+ "task_id": "acde_easy",
10872
+ "difficulty": "easy",
10873
+ "required_specialization": "general",
10874
+ "scenario_name": "Apartment Fire (Smoke Inhalation)",
10875
+ "score": 0.23815,
10876
+ "success": false,
10877
+ "actions": [
10878
+ "H3",
10879
+ "H4",
10880
+ "H1"
10881
+ ],
10882
+ "timestamp": "2026-04-09T08:48:34.391142+00:00"
10883
+ },
10884
+ {
10885
+ "seed": 468098216,
10886
+ "task_id": "acde_medium",
10887
+ "difficulty": "medium",
10888
+ "required_specialization": "trauma",
10889
+ "scenario_name": "Urban Pile-up (Rush Hour)",
10890
+ "score": 0.7869000000000002,
10891
+ "success": true,
10892
+ "actions": [
10893
+ "H2",
10894
+ "H3"
10895
+ ],
10896
+ "timestamp": "2026-04-09T08:48:35.228474+00:00"
10897
+ },
10898
+ {
10899
+ "seed": 468098217,
10900
+ "task_id": "acde_hard",
10901
+ "difficulty": "hard",
10902
+ "required_specialization": "cardiac",
10903
+ "scenario_name": "Mass Cardiac Event (Overload)",
10904
+ "score": 0.15059999999999998,
10905
+ "success": false,
10906
+ "actions": [
10907
+ "H2",
10908
+ "H3",
10909
+ "H5",
10910
+ "H2"
10911
+ ],
10912
+ "timestamp": "2026-04-09T08:48:36.666940+00:00"
10913
  }
10914
  ]
10915
  }
data/learning_memory.json CHANGED
@@ -1,44 +1,44 @@
1
  {
2
  "H2": {
3
- "success": 110,
4
- "fail": 212,
5
- "avg": 0.337900621118013,
6
- "accepted": 110,
7
- "rejected": 212
8
  },
9
  "H6": {
10
  "success": 50,
11
- "fail": 143,
12
- "avg": 0.2940440414507777,
13
  "accepted": 50,
14
- "rejected": 143
15
  },
16
  "H5": {
17
- "success": 114,
18
- "fail": 178,
19
- "avg": 0.38379143835616414,
20
- "accepted": 114,
21
- "rejected": 178
22
  },
23
  "H1": {
24
  "success": 111,
25
- "fail": 110,
26
- "avg": 0.41240090497737525,
27
  "accepted": 111,
28
- "rejected": 110
29
  },
30
  "H3": {
31
- "success": 102,
32
- "fail": 156,
33
- "avg": 0.3559833333333331,
34
- "accepted": 102,
35
- "rejected": 156
36
  },
37
  "H4": {
38
- "success": 41,
39
- "fail": 105,
40
- "avg": 0.34951301369862975,
41
- "accepted": 41,
42
- "rejected": 105
43
  }
44
  }
 
1
  {
2
  "H2": {
3
+ "success": 111,
4
+ "fail": 214,
5
+ "avg": 0.3359261538461545,
6
+ "accepted": 111,
7
+ "rejected": 214
8
  },
9
  "H6": {
10
  "success": 50,
11
+ "fail": 145,
12
+ "avg": 0.291038461538462,
13
  "accepted": 50,
14
+ "rejected": 145
15
  },
16
  "H5": {
17
+ "success": 116,
18
+ "fail": 179,
19
+ "avg": 0.38248847457627094,
20
+ "accepted": 116,
21
+ "rejected": 179
22
  },
23
  "H1": {
24
  "success": 111,
25
+ "fail": 117,
26
+ "avg": 0.401454385964912,
27
  "accepted": 111,
28
+ "rejected": 117
29
  },
30
  "H3": {
31
+ "success": 106,
32
+ "fail": 160,
33
+ "avg": 0.35322067669172913,
34
+ "accepted": 106,
35
+ "rejected": 160
36
  },
37
  "H4": {
38
+ "success": 43,
39
+ "fail": 108,
40
+ "avg": 0.34369470198675456,
41
+ "accepted": 43,
42
+ "rejected": 108
43
  }
44
  }
data/trajectory_history.jsonl CHANGED
@@ -355,3 +355,31 @@
355
  {"seed": 600335718, "task": "acde_medium", "difficulty": "medium", "step": 2, "state": {"patient_condition": "serious", "remaining_time_minutes": 17.0, "failed_hospitals": ["H4"], "visited_hospitals": ["H4"], "ambulance_status": "rerouting"}, "action": {"hospital_id": "H4", "policy_score": 0.07313118251968914, "strategy": "risk-aware policy + anti-stupidity guard + immediate-retry override"}, "outcome": {"status": "ACCEPTED", "reason": "Patient stabilized after delayed admission"}, "reward": 0.516}
356
  {"seed": 600335719, "task": "acde_hard", "difficulty": "hard", "step": 1, "state": {"patient_condition": "critical", "remaining_time_minutes": 11.0, "failed_hospitals": [], "visited_hospitals": [], "ambulance_status": "en_route"}, "action": {"hospital_id": "H6", "policy_score": 0.3244976864117649, "strategy": "safe policy + critical triage + anti-stupidity guard"}, "outcome": {"status": "REJECTED", "reason": "Hidden mismatch at arrival (wrong risky guess). Rerouting required."}, "reward": 0.001}
357
  {"seed": 600335719, "task": "acde_hard", "difficulty": "hard", "step": 2, "state": {"patient_condition": "critical", "remaining_time_minutes": 11.0, "failed_hospitals": ["H6"], "visited_hospitals": ["H6"], "ambulance_status": "rerouting"}, "action": {"hospital_id": "H5", "policy_score": 0.2976383394515107, "strategy": "risk-aware policy"}, "outcome": {"status": "REJECTED", "reason": "Condition became non-transferable during delay; immediate critical care failed"}, "reward": 0.001}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
  {"seed": 600335718, "task": "acde_medium", "difficulty": "medium", "step": 2, "state": {"patient_condition": "serious", "remaining_time_minutes": 17.0, "failed_hospitals": ["H4"], "visited_hospitals": ["H4"], "ambulance_status": "rerouting"}, "action": {"hospital_id": "H4", "policy_score": 0.07313118251968914, "strategy": "risk-aware policy + anti-stupidity guard + immediate-retry override"}, "outcome": {"status": "ACCEPTED", "reason": "Patient stabilized after delayed admission"}, "reward": 0.516}
356
  {"seed": 600335719, "task": "acde_hard", "difficulty": "hard", "step": 1, "state": {"patient_condition": "critical", "remaining_time_minutes": 11.0, "failed_hospitals": [], "visited_hospitals": [], "ambulance_status": "en_route"}, "action": {"hospital_id": "H6", "policy_score": 0.3244976864117649, "strategy": "safe policy + critical triage + anti-stupidity guard"}, "outcome": {"status": "REJECTED", "reason": "Hidden mismatch at arrival (wrong risky guess). Rerouting required."}, "reward": 0.001}
357
  {"seed": 600335719, "task": "acde_hard", "difficulty": "hard", "step": 2, "state": {"patient_condition": "critical", "remaining_time_minutes": 11.0, "failed_hospitals": ["H6"], "visited_hospitals": ["H6"], "ambulance_status": "rerouting"}, "action": {"hospital_id": "H5", "policy_score": 0.2976383394515107, "strategy": "risk-aware policy"}, "outcome": {"status": "REJECTED", "reason": "Condition became non-transferable during delay; immediate critical care failed"}, "reward": 0.001}
358
+ {"seed": 509813872, "task": "acde_easy", "difficulty": "easy", "step": 1, "state": {"patient_condition": "serious", "remaining_time_minutes": 20.0, "failed_hospitals": [], "visited_hospitals": [], "ambulance_status": "en_route"}, "action": {"hospital_id": "H3", "policy_score": 0.3729382136813107, "strategy": "safe policy"}, "outcome": {"status": "PARTIAL", "reason": "Early rejection mitigated by emergency field stabilization"}, "reward": 0.15500000000000003}
359
+ {"seed": 509813872, "task": "acde_easy", "difficulty": "easy", "step": 2, "state": {"patient_condition": "serious", "remaining_time_minutes": 20.0, "failed_hospitals": [], "visited_hospitals": ["H3"], "ambulance_status": "in_transit"}, "action": {"hospital_id": "H1", "policy_score": 0.4212703202476848, "strategy": "balanced policy"}, "outcome": {"status": "REJECTED", "reason": "Hospital cannot admit: ICU unavailable, Hospital overloaded"}, "reward": 0.139}
360
+ {"seed": 509813872, "task": "acde_easy", "difficulty": "easy", "step": 3, "state": {"patient_condition": "serious", "remaining_time_minutes": 20.0, "failed_hospitals": ["H1"], "visited_hospitals": ["H3", "H1"], "ambulance_status": "rerouting"}, "action": {"hospital_id": "H3", "policy_score": 0.4992501253122812, "strategy": "risk-aware policy + anti-stupidity guard"}, "outcome": {"status": "ACCEPTED", "reason": "Patient stabilized after delayed admission"}, "reward": 0.7160000000000001}
361
+ {"seed": 509813873, "task": "acde_medium", "difficulty": "medium", "step": 1, "state": {"patient_condition": "critical", "remaining_time_minutes": 15.0, "failed_hospitals": [], "visited_hospitals": [], "ambulance_status": "en_route"}, "action": {"hospital_id": "H3", "policy_score": 0.4050067561693999, "strategy": "safe policy + critical triage"}, "outcome": {"status": "REJECTED", "reason": "Condition became non-transferable during delay; immediate critical care failed"}, "reward": 0.001}
362
+ {"seed": 509813874, "task": "acde_hard", "difficulty": "hard", "step": 1, "state": {"patient_condition": "critical", "remaining_time_minutes": 11.0, "failed_hospitals": [], "visited_hospitals": [], "ambulance_status": "en_route"}, "action": {"hospital_id": "H1", "policy_score": 0.49875162318940436, "strategy": "safe policy + critical triage"}, "outcome": {"status": "REJECTED", "reason": "Hospital cannot admit: ICU unavailable, Hospital overloaded"}, "reward": 0.001}
363
+ {"seed": 509813874, "task": "acde_hard", "difficulty": "hard", "step": 2, "state": {"patient_condition": "critical", "remaining_time_minutes": 11.0, "failed_hospitals": ["H1"], "visited_hospitals": ["H1"], "ambulance_status": "rerouting"}, "action": {"hospital_id": "H6", "policy_score": 0.49875162318940436, "strategy": "risk-aware policy + guided-exploration + anti-stupidity guard"}, "outcome": {"status": "REJECTED", "reason": "Hospital cannot admit: ICU unavailable"}, "reward": 0.039}
364
+ {"seed": 509813874, "task": "acde_hard", "difficulty": "hard", "step": 3, "state": {"patient_condition": "critical", "remaining_time_minutes": 11.0, "failed_hospitals": ["H1", "H3"], "visited_hospitals": ["H1", "H3"], "ambulance_status": "rerouting"}, "action": {"hospital_id": "H4", "policy_score": 0.14285712242855392, "strategy": "risk-aware policy"}, "outcome": {"status": "REJECTED", "reason": "Hospital cannot admit: ICU unavailable"}, "reward": 0.001}
365
+ {"seed": 509813874, "task": "acde_hard", "difficulty": "hard", "step": 4, "state": {"patient_condition": "critical", "remaining_time_minutes": 11.0, "failed_hospitals": ["H1", "H3", "H4"], "visited_hospitals": ["H1", "H3", "H4"], "ambulance_status": "rerouting"}, "action": {"hospital_id": "H6", "policy_score": 0.49875162318940436, "strategy": "risk-aware policy + anti-stupidity guard"}, "outcome": {"status": "REJECTED", "reason": "Hidden mismatch at arrival (wrong risky guess). Rerouting required."}, "reward": 0.001}
366
+ {"seed": 579018919, "task": "acde_easy", "difficulty": "easy", "step": 1, "state": {"patient_condition": "serious", "remaining_time_minutes": 18.0, "failed_hospitals": [], "visited_hospitals": [], "ambulance_status": "en_route"}, "action": {"hospital_id": "H4", "policy_score": 0.32949151496142953, "strategy": "safe policy"}, "outcome": {"status": "PARTIAL", "reason": "Early rejection mitigated by emergency field stabilization"}, "reward": 0.30400000000000005}
367
+ {"seed": 579018919, "task": "acde_easy", "difficulty": "easy", "step": 2, "state": {"patient_condition": "serious", "remaining_time_minutes": 18.0, "failed_hospitals": [], "visited_hospitals": ["H4"], "ambulance_status": "in_transit"}, "action": {"hospital_id": "H1", "policy_score": 0.34824268005046066, "strategy": "balanced policy + anti-stupidity guard"}, "outcome": {"status": "REJECTED", "reason": "Condition became irreversible after delays"}, "reward": 0.109}
368
+ {"seed": 579018919, "task": "acde_easy", "difficulty": "easy", "step": 3, "state": {"patient_condition": "unstable", "remaining_time_minutes": 18.0, "failed_hospitals": ["H1"], "visited_hospitals": ["H4", "H1"], "ambulance_status": "rerouting"}, "action": {"hospital_id": "H4", "policy_score": 0.4992501253122812, "strategy": "risk-aware policy + anti-stupidity guard"}, "outcome": {"status": "ACCEPTED", "reason": "Condition stabilized after progressive treatment"}, "reward": 0.562}
369
+ {"seed": 579018920, "task": "acde_medium", "difficulty": "medium", "step": 1, "state": {"patient_condition": "critical", "remaining_time_minutes": 15.0, "failed_hospitals": [], "visited_hospitals": [], "ambulance_status": "en_route"}, "action": {"hospital_id": "H3", "policy_score": 0.4843607591902248, "strategy": "safe policy + critical triage + anti-stupidity guard"}, "outcome": {"status": "REJECTED", "reason": "Hospital cannot admit: ICU unavailable"}, "reward": 0.001}
370
+ {"seed": 579018920, "task": "acde_medium", "difficulty": "medium", "step": 2, "state": {"patient_condition": "critical", "remaining_time_minutes": 15.0, "failed_hospitals": ["H3"], "visited_hospitals": ["H3"], "ambulance_status": "rerouting"}, "action": {"hospital_id": "H5", "policy_score": 0.48373386963831955, "strategy": "risk-aware policy + anti-stupidity guard"}, "outcome": {"status": "PARTIAL", "reason": "Admitted with significant risk: No specialist available"}, "reward": 0.358}
371
+ {"seed": 579018920, "task": "acde_medium", "difficulty": "medium", "step": 3, "state": {"patient_condition": "critical", "remaining_time_minutes": 15.0, "failed_hospitals": ["H3"], "visited_hospitals": ["H3", "H5"], "ambulance_status": "in_transit"}, "action": {"hospital_id": "H1", "policy_score": 0.16666663886108793, "strategy": "balanced policy"}, "outcome": {"status": "REJECTED", "reason": "Hospital cannot admit: ICU unavailable"}, "reward": 0.001}
372
+ {"seed": 579018920, "task": "acde_medium", "difficulty": "medium", "step": 4, "state": {"patient_condition": "critical", "remaining_time_minutes": 15.0, "failed_hospitals": ["H3", "H1"], "visited_hospitals": ["H3", "H5", "H1"], "ambulance_status": "rerouting"}, "action": {"hospital_id": "H4", "policy_score": 0.16666663886108793, "strategy": "risk-aware policy"}, "outcome": {"status": "REJECTED", "reason": "Hidden mismatch at arrival (wrong risky guess). Rerouting required."}, "reward": 0.001}
373
+ {"seed": 579018921, "task": "acde_hard", "difficulty": "hard", "step": 1, "state": {"patient_condition": "critical", "remaining_time_minutes": 11.0, "failed_hospitals": [], "visited_hospitals": [], "ambulance_status": "en_route"}, "action": {"hospital_id": "H1", "policy_score": 0.35191055672786264, "strategy": "safe policy + critical triage"}, "outcome": {"status": "REJECTED", "reason": "Hospital cannot admit: ICU unavailable"}, "reward": 0.001}
374
+ {"seed": 579018921, "task": "acde_hard", "difficulty": "hard", "step": 2, "state": {"patient_condition": "critical", "remaining_time_minutes": 11.0, "failed_hospitals": ["H1"], "visited_hospitals": ["H1"], "ambulance_status": "rerouting"}, "action": {"hospital_id": "H6", "policy_score": 0.49875162318940436, "strategy": "risk-aware policy"}, "outcome": {"status": "REJECTED", "reason": "Hospital cannot admit: Hospital overloaded"}, "reward": 0.001}
375
+ {"seed": 579018921, "task": "acde_hard", "difficulty": "hard", "step": 3, "state": {"patient_condition": "critical", "remaining_time_minutes": 11.0, "failed_hospitals": ["H1", "H6"], "visited_hospitals": ["H1", "H6"], "ambulance_status": "rerouting"}, "action": {"hospital_id": "H3", "policy_score": 0.14285712242855392, "strategy": "risk-aware policy"}, "outcome": {"status": "REJECTED", "reason": "Hospital cannot admit: Hospital overloaded"}, "reward": 0.001}
376
+ {"seed": 579018921, "task": "acde_hard", "difficulty": "hard", "step": 4, "state": {"patient_condition": "critical", "remaining_time_minutes": 11.0, "failed_hospitals": ["H1", "H6", "H4"], "visited_hospitals": ["H1", "H6", "H4"], "ambulance_status": "rerouting"}, "action": {"hospital_id": "H5", "policy_score": 0.49875162318940436, "strategy": "risk-aware policy + anti-stupidity guard"}, "outcome": {"status": "PARTIAL", "reason": "Admitted with delays: prolonged transfer strain"}, "reward": 0.40800000000000014}
377
+ {"seed": 468098215, "task": "acde_easy", "difficulty": "easy", "step": 1, "state": {"patient_condition": "serious", "remaining_time_minutes": 18.0, "failed_hospitals": [], "visited_hospitals": [], "ambulance_status": "en_route"}, "action": {"hospital_id": "H3", "policy_score": 0.3443315337257757, "strategy": "safe policy + anti-stupidity guard"}, "outcome": {"status": "PARTIAL", "reason": "Critical deterioration managed temporarily; reroute still needed"}, "reward": 0.334}
378
+ {"seed": 468098215, "task": "acde_easy", "difficulty": "easy", "step": 2, "state": {"patient_condition": "stable", "remaining_time_minutes": 18.0, "failed_hospitals": [], "visited_hospitals": ["H3"], "ambulance_status": "in_transit"}, "action": {"hospital_id": "H4", "policy_score": 0.24369633883371342, "strategy": "balanced policy"}, "outcome": {"status": "REJECTED", "reason": "Emergency beds became unavailable during arrival"}, "reward": 0.001}
379
+ {"seed": 468098215, "task": "acde_easy", "difficulty": "easy", "step": 3, "state": {"patient_condition": "stable", "remaining_time_minutes": 18.0, "failed_hospitals": ["H4"], "visited_hospitals": ["H3", "H4"], "ambulance_status": "rerouting"}, "action": {"hospital_id": "H1", "policy_score": 0.27278944968751606, "strategy": "risk-aware policy"}, "outcome": {"status": "REJECTED", "reason": "Condition became non-transferable during delay; immediate critical care failed"}, "reward": 0.139}
380
+ {"seed": 468098216, "task": "acde_medium", "difficulty": "medium", "step": 1, "state": {"patient_condition": "serious", "remaining_time_minutes": 17.0, "failed_hospitals": [], "visited_hospitals": [], "ambulance_status": "en_route"}, "action": {"hospital_id": "H2", "policy_score": 0.4171322328020302, "strategy": "safe policy + guided-exploration"}, "outcome": {"status": "PARTIAL", "reason": "Initial triage completed; transfer monitoring still required"}, "reward": 0.37}
381
+ {"seed": 468098216, "task": "acde_medium", "difficulty": "medium", "step": 2, "state": {"patient_condition": "serious", "remaining_time_minutes": 17.0, "failed_hospitals": [], "visited_hospitals": ["H2"], "ambulance_status": "in_transit"}, "action": {"hospital_id": "H3", "policy_score": 0.43825439745695755, "strategy": "balanced policy + anti-stupidity guard"}, "outcome": {"status": "ACCEPTED", "reason": "Patient stabilized after delayed admission"}, "reward": 0.8660000000000001}
382
+ {"seed": 468098217, "task": "acde_hard", "difficulty": "hard", "step": 1, "state": {"patient_condition": "critical", "remaining_time_minutes": 12.0, "failed_hospitals": [], "visited_hospitals": [], "ambulance_status": "en_route"}, "action": {"hospital_id": "H2", "policy_score": 0.49964989496849055, "strategy": "safe policy + critical triage"}, "outcome": {"status": "REJECTED", "reason": "Hospital cannot admit: Hospital overloaded"}, "reward": 0.001}
383
+ {"seed": 468098217, "task": "acde_hard", "difficulty": "hard", "step": 2, "state": {"patient_condition": "critical", "remaining_time_minutes": 12.0, "failed_hospitals": ["H2"], "visited_hospitals": ["H2"], "ambulance_status": "rerouting"}, "action": {"hospital_id": "H3", "policy_score": 0.29411764705882354, "strategy": "risk-aware policy"}, "outcome": {"status": "REJECTED", "reason": "Hidden mismatch at arrival (wrong risky guess). Rerouting required."}, "reward": 0.001}
384
+ {"seed": 468098217, "task": "acde_hard", "difficulty": "hard", "step": 3, "state": {"patient_condition": "critical", "remaining_time_minutes": 12.0, "failed_hospitals": ["H2", "H3"], "visited_hospitals": ["H2", "H3"], "ambulance_status": "rerouting"}, "action": {"hospital_id": "H5", "policy_score": 0.039999999999999994, "strategy": "risk-aware policy"}, "outcome": {"status": "REJECTED", "reason": "Hidden mismatch at arrival (wrong risky guess). Rerouting required."}, "reward": 0.001}
385
+ {"seed": 468098217, "task": "acde_hard", "difficulty": "hard", "step": 4, "state": {"patient_condition": "critical", "remaining_time_minutes": 12.0, "failed_hospitals": ["H2", "H3", "H1"], "visited_hospitals": ["H2", "H3", "H1"], "ambulance_status": "rerouting"}, "action": {"hospital_id": "H2", "policy_score": 0.29411764705882354, "strategy": "risk-aware policy + immediate-retry override"}, "outcome": {"status": "REJECTED", "reason": "Hospital cannot admit: ICU unavailable, No specialist available"}, "reward": 0.001}
inference.py CHANGED
@@ -18,13 +18,17 @@ import random
18
  from pathlib import Path
19
  from datetime import datetime, timezone
20
 
 
 
 
 
21
  from app.environment.core import EmergencyEnv
22
  from app.models.action import Action
23
 
24
  try:
25
  from openai import OpenAI
26
  except Exception: # pragma: no cover - fallback for missing optional dependency
27
- OpenAI = None
28
 
29
  TASK_ORDER = ["acde_easy", "acde_medium", "acde_hard"]
30
  LEVEL_TO_TASK = {
@@ -71,7 +75,7 @@ def runtime_llm_config() -> dict[str, str]:
71
  }
72
 
73
 
74
- def require_llm_config() -> tuple[object, str]:
75
  config = runtime_llm_config()
76
  missing = [name for name, value in config.items() if not value]
77
  if missing:
@@ -88,7 +92,7 @@ def require_llm_config() -> tuple[object, str]:
88
 
89
 
90
  def llm_rationale(
91
- client: object,
92
  model_name: str,
93
  observation: dict,
94
  chosen: dict,
@@ -315,7 +319,7 @@ def memory_score_for_hospital(
315
  if recent_failed:
316
  value -= 0.3
317
 
318
- return max(0.0, min(1.0, value))
319
 
320
 
321
  def score_hospitals(observation: dict, learning_profile: dict | None = None) -> list[dict]:
@@ -328,8 +332,8 @@ def score_hospitals(observation: dict, learning_profile: dict | None = None) ->
328
  last_status = str(last_arrival.get("status", "")).lower()
329
 
330
  scored: list[dict] = []
331
- initial_limit = float(observation.get("initial_critical_time_limit_minutes", observation["critical_time_limit_minutes"]))
332
- remaining_time = float(observation.get("remaining_time_minutes", observation["critical_time_limit_minutes"]))
333
  urgency = 1.0 - min(1.0, max(0.0, remaining_time / max(initial_limit, 1e-6)))
334
 
335
  patient_condition = observation.get("patient_condition", "").lower()
@@ -473,7 +477,7 @@ def score_hospitals(observation: dict, learning_profile: dict | None = None) ->
473
  "specialization": hospital["specialization"],
474
  "travel_time": travel_time,
475
  "memory_score": mem_score,
476
- "policy_score": max(0.0, min(1.0, score)),
477
  "specialization_match": spec_match,
478
  "tie_break_score": (
479
  (distance_score * 0.35)
@@ -500,7 +504,7 @@ def score_hospitals(observation: dict, learning_profile: dict | None = None) ->
500
  )
501
  jitter_rng = random.Random(jitter_seed)
502
  normalized *= jitter_rng.uniform(0.3, 0.7)
503
- item["policy_score"] = max(0.0, min(1.0, normalized))
504
  elif max_score > 0:
505
  for item in scored:
506
  normalized = item["policy_score"] / max_score
@@ -512,7 +516,7 @@ def score_hospitals(observation: dict, learning_profile: dict | None = None) ->
512
  )
513
  jitter_rng = random.Random(jitter_seed)
514
  normalized *= jitter_rng.uniform(0.3, 0.7)
515
- item["policy_score"] = max(0.0, min(1.0, normalized))
516
  else:
517
  tie_min = min(item.get("tie_break_score", 0.0) for item in scored)
518
  tie_max = max(item.get("tie_break_score", 0.0) for item in scored)
@@ -528,10 +532,10 @@ def score_hospitals(observation: dict, learning_profile: dict | None = None) ->
528
  )
529
  jitter_rng = random.Random(jitter_seed)
530
  normalized *= jitter_rng.uniform(0.3, 0.7)
531
- item["policy_score"] = max(0.0, min(1.0, normalized))
532
  else:
533
  for item in scored:
534
- item["policy_score"] = 0.0
535
 
536
  # Remove hard-zero scores and normalize to probability-like values.
537
  for item in scored:
@@ -585,7 +589,7 @@ def score_hospitals(observation: dict, learning_profile: dict | None = None) ->
585
  )
586
  jitter_rng = random.Random(jitter_seed)
587
  normalized_score = jitter_rng.uniform(0.01, 0.03)
588
- item["policy_score"] = normalized_score
589
 
590
  scored.sort(key=lambda item: item["policy_score"], reverse=True)
591
 
@@ -1060,9 +1064,9 @@ def run_episode(
1060
  "seed": seed,
1061
  "result": final_result,
1062
  "success": final_result == "SUCCESS",
1063
- "score": round(final_score, 4),
1064
  "steps": steps,
1065
- "average_reward": round(total_reward / max(1, steps), 4),
1066
  },
1067
  )
1068
 
 
18
  from pathlib import Path
19
  from datetime import datetime, timezone
20
 
21
+ from typing import Any, Literal, cast, TYPE_CHECKING
22
+ if TYPE_CHECKING:
23
+ from openai import OpenAI
24
+
25
  from app.environment.core import EmergencyEnv
26
  from app.models.action import Action
27
 
28
  try:
29
  from openai import OpenAI
30
  except Exception: # pragma: no cover - fallback for missing optional dependency
31
+ OpenAI = None # type: ignore
32
 
33
  TASK_ORDER = ["acde_easy", "acde_medium", "acde_hard"]
34
  LEVEL_TO_TASK = {
 
75
  }
76
 
77
 
78
+ def require_llm_config() -> tuple[Any, str]:
79
  config = runtime_llm_config()
80
  missing = [name for name, value in config.items() if not value]
81
  if missing:
 
92
 
93
 
94
  def llm_rationale(
95
+ client: Any,
96
  model_name: str,
97
  observation: dict,
98
  chosen: dict,
 
319
  if recent_failed:
320
  value -= 0.3
321
 
322
+ return max(0.001, min(0.999, value))
323
 
324
 
325
  def score_hospitals(observation: dict, learning_profile: dict | None = None) -> list[dict]:
 
332
  last_status = str(last_arrival.get("status", "")).lower()
333
 
334
  scored: list[dict] = []
335
+ initial_limit = float(observation.get("initial_critical_time_limit_minutes") or observation.get("critical_time_limit_minutes") or 15.0)
336
+ remaining_time = float(observation.get("remaining_time_minutes") or observation.get("critical_time_limit_minutes") or 15.0)
337
  urgency = 1.0 - min(1.0, max(0.0, remaining_time / max(initial_limit, 1e-6)))
338
 
339
  patient_condition = observation.get("patient_condition", "").lower()
 
477
  "specialization": hospital["specialization"],
478
  "travel_time": travel_time,
479
  "memory_score": mem_score,
480
+ "policy_score": max(0.001, min(0.999, score)),
481
  "specialization_match": spec_match,
482
  "tie_break_score": (
483
  (distance_score * 0.35)
 
504
  )
505
  jitter_rng = random.Random(jitter_seed)
506
  normalized *= jitter_rng.uniform(0.3, 0.7)
507
+ item["policy_score"] = max(0.001, min(0.999, normalized))
508
  elif max_score > 0:
509
  for item in scored:
510
  normalized = item["policy_score"] / max_score
 
516
  )
517
  jitter_rng = random.Random(jitter_seed)
518
  normalized *= jitter_rng.uniform(0.3, 0.7)
519
+ item["policy_score"] = max(0.001, min(0.999, normalized))
520
  else:
521
  tie_min = min(item.get("tie_break_score", 0.0) for item in scored)
522
  tie_max = max(item.get("tie_break_score", 0.0) for item in scored)
 
532
  )
533
  jitter_rng = random.Random(jitter_seed)
534
  normalized *= jitter_rng.uniform(0.3, 0.7)
535
+ item["policy_score"] = max(0.001, min(0.999, normalized))
536
  else:
537
  for item in scored:
538
+ item["policy_score"] = 0.001
539
 
540
  # Remove hard-zero scores and normalize to probability-like values.
541
  for item in scored:
 
589
  )
590
  jitter_rng = random.Random(jitter_seed)
591
  normalized_score = jitter_rng.uniform(0.01, 0.03)
592
+ item["policy_score"] = max(0.001, min(0.999, normalized_score))
593
 
594
  scored.sort(key=lambda item: item["policy_score"], reverse=True)
595
 
 
1064
  "seed": seed,
1065
  "result": final_result,
1066
  "success": final_result == "SUCCESS",
1067
+ "score": max(0.001, min(0.999, round(final_score, 4))),
1068
  "steps": steps,
1069
+ "average_reward": max(0.001, min(0.999, round(total_reward / max(1, steps), 4))),
1070
  },
1071
  )
1072