""" ER_MAP/server.py ================ FastAPI wrapper around TriageEnv for OpenEnv-style deployment (HF Space / Docker / local Uvicorn). Endpoints: POST /reset -> {observation, info} POST /step -> {observation, reward, done, truncated, info} GET /state -> full internal env state GET /health -> {"status": "ok", "version": ...} GET / -> human-readable landing page Usage (local): uvicorn ER_MAP.server:app --host 0.0.0.0 --port 7860 --reload Usage (HF Space): The HF Space spec auto-launches uvicorn against this app. Environment variables: GROQ_API_KEY shared key for both nurse and patient GROQ_NURSE_API_KEY per-role override (optional) GROQ_PATIENT_API_KEY per-role override (optional) ERMAP_MODEL Groq model id (default: llama-3.3-70b-versatile) """ from __future__ import annotations import json import logging import os from typing import Any, Dict, Optional from fastapi import FastAPI, HTTPException from fastapi.responses import HTMLResponse from pydantic import BaseModel, Field from ER_MAP.envs.triage_env import TriageEnv logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", ) logger = logging.getLogger("ER_MAP.server") # --------------------------------------------------------------------------- # Request / response models # --------------------------------------------------------------------------- class ResetRequest(BaseModel): seed: Optional[int] = Field(None, description="Seed for reproducible scenarios") options: Optional[Dict[str, Any]] = Field( default=None, description='Reset options. Supports {"phase": 1|2|3, "difficulty": "easy|medium|hard"}.', ) class StepRequest(BaseModel): action: str = Field( ..., description="Doctor's JSON action string (e.g., '{\"tool\":\"read_soap\"}')" ) class StepResponse(BaseModel): observation: str reward: float done: bool truncated: bool info: Dict[str, Any] class ResetResponse(BaseModel): observation: str info: Dict[str, Any] # --------------------------------------------------------------------------- # App + singleton env # --------------------------------------------------------------------------- app = FastAPI( title="ER-MAP Triage Environment", version="1.0.0", description=( "OpenEnv-compatible FastAPI wrapper for the ER-MAP multi-agent " "medical triage environment. The Doctor RL agent communicates " "with this server via /reset and /step." ), ) _ENV: Optional[TriageEnv] = None def _get_env() -> TriageEnv: """Lazily construct a single shared TriageEnv per server process.""" global _ENV if _ENV is None: _ENV = TriageEnv( groq_api_key=((os.environ.get("GROQ_API_KEY") or os.environ.get("groq")) or os.environ.get("groq", "")), nurse_api_key=((os.environ.get("GROQ_NURSE_API_KEY") or os.environ.get("nurse")) or os.environ.get("nurse", "")), patient_api_key=((os.environ.get("GROQ_PATIENT_API_KEY") or os.environ.get("patient")) or os.environ.get("patient", "")), empathy_judge_api_key=((os.environ.get("GROQ_EMPATHY_JUDGE_API_KEY") or os.environ.get("empathy")) or os.environ.get("empathy", "")), medical_judge_api_key=((os.environ.get("GROQ_MEDICAL_JUDGE_API_KEY") or os.environ.get("medical")) or os.environ.get("medical", "")), model=os.environ.get("ERMAP_MODEL", "llama-3.3-70b-versatile"), nurse_model=os.environ.get("ERMAP_NURSE_MODEL"), patient_model=os.environ.get("ERMAP_PATIENT_MODEL"), empathy_judge_model=os.environ.get("ERMAP_EMPATHY_JUDGE_MODEL"), medical_judge_model=os.environ.get("ERMAP_MEDICAL_JUDGE_MODEL"), render_mode=None, ) logger.info("TriageEnv initialized.") return _ENV def _sanitize(info: Dict[str, Any]) -> Dict[str, Any]: """ Strip ground-truth disease from info before returning to the agent. The Doctor must NEVER see the true disease in observations or info, only the verifier output at terminal_discharge time. """ safe = dict(info) safe.pop("ground_truth_disease", None) return safe # --------------------------------------------------------------------------- # Endpoints # --------------------------------------------------------------------------- @app.get("/health") def health() -> Dict[str, Any]: return { "status": "ok", "version": app.version, "env": "ER-MAP-Triage", } @app.post("/reset", response_model=ResetResponse) def reset(req: ResetRequest) -> ResetResponse: env = _get_env() try: obs, info = env.reset(seed=req.seed, options=req.options or {}) except Exception as e: logger.exception("reset failed") raise HTTPException(status_code=500, detail=f"reset failed: {e}") return ResetResponse(observation=obs, info=_sanitize(info)) @app.post("/step", response_model=StepResponse) def step(req: StepRequest) -> StepResponse: env = _get_env() try: obs, reward, done, truncated, info = env.step(req.action) except Exception as e: logger.exception("step failed") raise HTTPException(status_code=500, detail=f"step failed: {e}") return StepResponse( observation=obs, reward=float(reward), done=bool(done), truncated=bool(truncated), info=_sanitize(info), ) @app.get("/state") def state() -> Dict[str, Any]: """ Return the full internal env state. Includes ground truth, so this endpoint is for debugging / dashboards, NOT for the RL agent. """ env = _get_env() return env.state() @app.get("/", response_class=HTMLResponse) def index() -> str: return f""" ER-MAP Triage Env

ER-MAP Triage Environment

OpenEnv-compatible FastAPI wrapper. Version {app.version}.

Endpoints

Quick start

curl -X POST $URL/reset -H 'content-type: application/json' \\
     -d '{{"options": {{"phase": 1, "difficulty": "easy"}}}}'
curl -X POST $URL/step -H 'content-type: application/json' \\
     -d '{{"action": "{{\\"tool\\": \\"read_soap\\"}}"}}'
"""