Commit ·
74f461a
1
Parent(s): 65d4590
Fix 8 issues spotted in code review: Dockerfile path, task descriptions, duplicate function, fallback score consistency, twin constants comment, has_data_lag docs
Browse files- README.md +4 -4
- baseline/evaluator.py +1 -10
- baseline/policy.py +19 -0
- inference.py +4 -11
- server/constants.py +4 -1
README.md
CHANGED
|
@@ -147,11 +147,11 @@ new_infection = current + (spread_rate − natural_recovery − intervention) +
|
|
| 147 |
| --- | --- | --- | --- | --- | --- |
|
| 148 |
| **easy** | 2 | 10 | 10 | None | Single outbreak; D1 starts infected, D0 is clean |
|
| 149 |
| **medium** | 4 | 15 | 8 | None | Two simultaneous outbreaks; forced triage between competing threats |
|
| 150 |
-
| **hard** | 6 | 15 | 7 | **3 days** | Six
|
| 151 |
|
| 152 |
**Easy** — D1 starts at 0.50 infection, D0 is clean. The agent must observe and target the correct district. A fixed-target agent ignoring observations scores ~43% and breaches hospitals 60% of the time.
|
| 153 |
|
| 154 |
-
**Medium** — D0
|
| 155 |
|
| 156 |
**Hard** — 3-day information lag means the agent sees infection rates from 3 days ago. The `growth_rate_hint` provides a noisy signal to estimate current state. Structural uncertainty — not testable around.
|
| 157 |
|
|
@@ -179,7 +179,7 @@ Fully deterministic — no randomness, no LLM calls. Identical trajectories alwa
|
|
| 179 |
| --- | --- | --- |
|
| 180 |
| **Hospital score** | 45% | Average capacity preserved; ×0.6 multiplier if any district collapsed |
|
| 181 |
| **Containment score** | 30% | Fraction of district-days below infection threshold (first 2 steps excluded) |
|
| 182 |
-
| **Efficiency score** | 15% | Fraction of resource actions targeting highest-infected district (uses pre-action state) |
|
| 183 |
| **Speed score** | 10% | `1 − (steps / max_steps)` if episode ends early; else 0 |
|
| 184 |
|
| 185 |
Hospital is weighted highest because system capacity preservation is the primary operational constraint in real outbreak response — a functioning healthcare system is the prerequisite for everything else.
|
|
@@ -309,7 +309,7 @@ python baseline/run.py
|
|
| 309 |
### Docker
|
| 310 |
|
| 311 |
```bash
|
| 312 |
-
docker build -
|
| 313 |
docker run -p 7860:7860 cascade-containment
|
| 314 |
```
|
| 315 |
|
|
|
|
| 147 |
| --- | --- | --- | --- | --- | --- |
|
| 148 |
| **easy** | 2 | 10 | 10 | None | Single outbreak; D1 starts infected, D0 is clean |
|
| 149 |
| **medium** | 4 | 15 | 8 | None | Two simultaneous outbreaks; forced triage between competing threats |
|
| 150 |
+
| **hard** | 6 | 15 | 7 | **3 days** | Six seeded infections (only D2 and D4 above safe threshold); 3-day data lag; scarce resources |
|
| 151 |
|
| 152 |
**Easy** — D1 starts at 0.50 infection, D0 is clean. The agent must observe and target the correct district. A fixed-target agent ignoring observations scores ~43% and breaches hospitals 60% of the time.
|
| 153 |
|
| 154 |
+
**Medium** — D0 starts above the infection threshold (0.42); D2 is in the warning zone (0.38, below the 0.40 threshold). D1 and D3 start low but grow into crisis within 4–6 steps via spillover. With 8 resources across 4 districts over 15 steps, genuine triage is required.
|
| 155 |
|
| 156 |
**Hard** — 3-day information lag means the agent sees infection rates from 3 days ago. The `growth_rate_hint` provides a noisy signal to estimate current state. Structural uncertainty — not testable around.
|
| 157 |
|
|
|
|
| 179 |
| --- | --- | --- |
|
| 180 |
| **Hospital score** | 45% | Average capacity preserved; ×0.6 multiplier if any district collapsed |
|
| 181 |
| **Containment score** | 30% | Fraction of district-days below infection threshold (first 2 steps excluded) |
|
| 182 |
+
| **Efficiency score** | 15% | Fraction of resource actions targeting highest-infected district (grader uses pre-action state to avoid penalising successful treatments) |
|
| 183 |
| **Speed score** | 10% | `1 − (steps / max_steps)` if episode ends early; else 0 |
|
| 184 |
|
| 185 |
Hospital is weighted highest because system capacity preservation is the primary operational constraint in real outbreak response — a functioning healthcare system is the prerequisite for everything else.
|
|
|
|
| 309 |
### Docker
|
| 310 |
|
| 311 |
```bash
|
| 312 |
+
docker build -t cascade-containment .
|
| 313 |
docker run -p 7860:7860 cascade-containment
|
| 314 |
```
|
| 315 |
|
baseline/evaluator.py
CHANGED
|
@@ -5,7 +5,7 @@ from openai import OpenAI
|
|
| 5 |
|
| 6 |
from client import CascadeContainmentEnv
|
| 7 |
from models import ContainmentAction, CityObservation
|
| 8 |
-
from baseline.policy import get_client, build_prompt, call_llm, parse_action
|
| 9 |
from core.trajectory import EpisodicMemory
|
| 10 |
from core.reward import normalise_score
|
| 11 |
from core.policy_update import compute_advantage, update_memory
|
|
@@ -18,15 +18,6 @@ N_ROLLOUTS = {
|
|
| 18 |
}
|
| 19 |
|
| 20 |
|
| 21 |
-
def build_prompt_with_memory(obs: CityObservation, memory: EpisodicMemory) -> str:
|
| 22 |
-
base = build_prompt(obs)
|
| 23 |
-
memory_block = memory.retrieve(obs)
|
| 24 |
-
if not memory_block:
|
| 25 |
-
return base
|
| 26 |
-
injection = "\n" + memory_block + "\nApply these lessons to your current decision.\n"
|
| 27 |
-
return base.replace("Your response:", injection + "Your response:")
|
| 28 |
-
|
| 29 |
-
|
| 30 |
def run_rollout(
|
| 31 |
env: Any, task_name: str, client: OpenAI,
|
| 32 |
memory: EpisodicMemory, verbose: bool = True,
|
|
|
|
| 5 |
|
| 6 |
from client import CascadeContainmentEnv
|
| 7 |
from models import ContainmentAction, CityObservation
|
| 8 |
+
from baseline.policy import get_client, build_prompt, call_llm, parse_action, build_prompt_with_memory
|
| 9 |
from core.trajectory import EpisodicMemory
|
| 10 |
from core.reward import normalise_score
|
| 11 |
from core.policy_update import compute_advantage, update_memory
|
|
|
|
| 18 |
}
|
| 19 |
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
def run_rollout(
|
| 22 |
env: Any, task_name: str, client: OpenAI,
|
| 23 |
memory: EpisodicMemory, verbose: bool = True,
|
baseline/policy.py
CHANGED
|
@@ -13,6 +13,11 @@ def get_client() -> OpenAI:
|
|
| 13 |
|
| 14 |
def build_prompt(obs: CityObservation) -> str:
|
| 15 |
num_districts = len(obs.districts)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
has_data_lag = num_districts == 6
|
| 17 |
|
| 18 |
sorted_districts = sorted(
|
|
@@ -169,3 +174,17 @@ def get_action(obs: CityObservation, client: OpenAI) -> ContainmentAction:
|
|
| 169 |
prompt = build_prompt(obs)
|
| 170 |
response = call_llm(prompt, client)
|
| 171 |
return parse_action(response, len(obs.districts))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
def build_prompt(obs: CityObservation) -> str:
|
| 15 |
num_districts = len(obs.districts)
|
| 16 |
+
# Infers data lag from district count because CityObservation does not expose
|
| 17 |
+
# data_lag_days directly. This works because hard (6 districts) is currently the
|
| 18 |
+
# only task with a data lag. If a new task is added with 6 districts and no lag,
|
| 19 |
+
# or a lag task with fewer districts, this will silently misbehave.
|
| 20 |
+
# Fix: expose data_lag_days in CityObservation and read it directly.
|
| 21 |
has_data_lag = num_districts == 6
|
| 22 |
|
| 23 |
sorted_districts = sorted(
|
|
|
|
| 174 |
prompt = build_prompt(obs)
|
| 175 |
response = call_llm(prompt, client)
|
| 176 |
return parse_action(response, len(obs.districts))
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def build_prompt_with_memory(obs: CityObservation, memory) -> str:
|
| 180 |
+
"""
|
| 181 |
+
Builds the LLM prompt augmented with relevant past decisions from episodic memory.
|
| 182 |
+
Injects memory block just before the 'Your response:' line so the model sees
|
| 183 |
+
prior high-reward decisions as concrete examples before making its choice.
|
| 184 |
+
"""
|
| 185 |
+
base = build_prompt(obs)
|
| 186 |
+
memory_block = memory.retrieve(obs)
|
| 187 |
+
if not memory_block:
|
| 188 |
+
return base
|
| 189 |
+
injection = "\n" + memory_block + "\nApply these lessons to your current decision.\n"
|
| 190 |
+
return base.replace("Your response:", injection + "Your response:")
|
inference.py
CHANGED
|
@@ -10,9 +10,10 @@ import requests as http_requests
|
|
| 10 |
|
| 11 |
from client import CascadeContainmentEnv
|
| 12 |
from models import ContainmentAction
|
| 13 |
-
from baseline.policy import get_client, build_prompt, call_llm, parse_action
|
| 14 |
from core.trajectory import EpisodicMemory
|
| 15 |
from core.policy_update import compute_advantage, update_memory
|
|
|
|
| 16 |
|
| 17 |
API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
|
| 18 |
MODEL_NAME = os.getenv("MODEL_NAME", "meta-llama/Llama-3.1-8B-Instruct")
|
|
@@ -48,15 +49,6 @@ def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> No
|
|
| 48 |
)
|
| 49 |
|
| 50 |
|
| 51 |
-
def build_prompt_with_memory(obs, memory: EpisodicMemory) -> str:
|
| 52 |
-
base = build_prompt(obs)
|
| 53 |
-
memory_block = memory.retrieve(obs)
|
| 54 |
-
if not memory_block:
|
| 55 |
-
return base
|
| 56 |
-
injection = "\n" + memory_block + "\nApply these lessons to your current decision.\n"
|
| 57 |
-
return base.replace("Your response:", injection + "Your response:")
|
| 58 |
-
|
| 59 |
-
|
| 60 |
def run_rollout(
|
| 61 |
env,
|
| 62 |
task_name: str,
|
|
@@ -108,7 +100,8 @@ def run_rollout(
|
|
| 108 |
if grade_resp.status_code == 200:
|
| 109 |
score = grade_resp.json().get("final_score", 0.0)
|
| 110 |
except Exception:
|
| 111 |
-
|
|
|
|
| 112 |
|
| 113 |
success = score >= 0.40
|
| 114 |
|
|
|
|
| 10 |
|
| 11 |
from client import CascadeContainmentEnv
|
| 12 |
from models import ContainmentAction
|
| 13 |
+
from baseline.policy import get_client, build_prompt, call_llm, parse_action, build_prompt_with_memory
|
| 14 |
from core.trajectory import EpisodicMemory
|
| 15 |
from core.policy_update import compute_advantage, update_memory
|
| 16 |
+
from core.reward import normalise_score
|
| 17 |
|
| 18 |
API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
|
| 19 |
MODEL_NAME = os.getenv("MODEL_NAME", "meta-llama/Llama-3.1-8B-Instruct")
|
|
|
|
| 49 |
)
|
| 50 |
|
| 51 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
def run_rollout(
|
| 53 |
env,
|
| 54 |
task_name: str,
|
|
|
|
| 100 |
if grade_resp.status_code == 200:
|
| 101 |
score = grade_resp.json().get("final_score", 0.0)
|
| 102 |
except Exception:
|
| 103 |
+
num_districts = {"easy": 2, "medium": 4, "hard": 6}.get(task_name, 2)
|
| 104 |
+
score = normalise_score(total_reward, step, num_districts)
|
| 105 |
|
| 106 |
success = score >= 0.40
|
| 107 |
|
server/constants.py
CHANGED
|
@@ -8,7 +8,10 @@ TASK_CONFIG = {
|
|
| 8 |
}
|
| 9 |
|
| 10 |
INFECTION_THRESHOLD = 0.40 # above this → district is in the danger zone
|
| 11 |
-
SAFE_THRESHOLD = 0.20 # below this → district is considered contained
|
|
|
|
|
|
|
|
|
|
| 12 |
LOW_THRESHOLD = 0.20 # restricting below this threshold earns a penalty
|
| 13 |
# real ICU overflow and triage failure kick in well before zero capacity
|
| 14 |
HOSPITAL_BREACH_POINT = 0.10
|
|
|
|
| 8 |
}
|
| 9 |
|
| 10 |
INFECTION_THRESHOLD = 0.40 # above this → district is in the danger zone
|
| 11 |
+
SAFE_THRESHOLD = 0.20 # below this → district is considered contained (grader + terminal check)
|
| 12 |
+
# LOW_THRESHOLD and SAFE_THRESHOLD are intentionally separate even though both equal 0.20.
|
| 13 |
+
# SAFE_THRESHOLD is a containment concept; LOW_THRESHOLD is a restriction-penalty concept.
|
| 14 |
+
# Keeping them separate means they can diverge independently if the design changes.
|
| 15 |
LOW_THRESHOLD = 0.20 # restricting below this threshold earns a penalty
|
| 16 |
# real ICU overflow and triage failure kick in well before zero capacity
|
| 17 |
HOSPITAL_BREACH_POINT = 0.10
|