File size: 6,221 Bytes
64d56f9
 
0ef0e49
 
82ce364
0ef0e49
82ce364
 
0ef0e49
 
431e294
0ef0e49
64d56f9
4fc9bec
 
 
c47ce11
 
82ce364
 
 
 
c47ce11
4fc9bec
 
0ef0e49
64d56f9
 
 
0ef0e49
64d56f9
 
82ce364
64d56f9
 
 
 
 
431e294
 
 
 
 
 
64d56f9
0ef0e49
 
4fc9bec
64d56f9
 
4fc9bec
 
 
0ef0e49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82ce364
431e294
82ce364
 
 
0ef0e49
 
 
 
 
 
 
 
 
 
82ce364
 
0ef0e49
 
 
82ce364
 
431e294
 
82ce364
431e294
0ef0e49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82ce364
431e294
 
 
 
0ef0e49
 
 
 
 
 
82ce364
 
 
431e294
82ce364
 
 
 
431e294
82ce364
 
 
 
 
 
 
 
431e294
 
82ce364
 
 
 
0ef0e49
 
431e294
82ce364
431e294
0ef0e49
 
82ce364
 
431e294
 
 
 
 
82ce364
431e294
 
82ce364
0ef0e49
 
 
 
 
 
 
 
 
 
 
 
 
82ce364
 
4fc9bec
64d56f9
4fc9bec
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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
125
126
127
128
129
130
131
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
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
"""FastAPI application for DispatchPulse.

Uses ``create_fastapi_app(...)`` from openenv-core for the standard ``/reset``,
``/step``, ``/state``, ``/health``, ``/metadata``, ``/schema``, ``/ws`` routes.

On top of that baseline we add four DispatchPulse-specific endpoints the
hackathon grader discovers:

- ``GET /``          β€” Root metadata (name, status, endpoints)
- ``GET /tasks``     β€” list the 3 graded tasks
- ``GET /tasks/{task_id}`` β€” single-task metadata lookup
- ``POST /grader``   β€” score an episode (silent run or replayed action list)
"""

from __future__ import annotations

import os
import sys
from typing import Any, Dict, List, Optional

from fastapi import HTTPException
from pydantic import BaseModel, Field

# Support both in-repo and standalone imports.
try:
    from openenv.core.env_server import create_fastapi_app

    from .environment import DispatchPulseEnvironment
except ImportError:  # pragma: no cover
    from openenv.core.env_server import create_fastapi_app
    from server.environment import DispatchPulseEnvironment

# Import project root modules
_PKG_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _PKG_ROOT not in sys.path:
    sys.path.insert(0, _PKG_ROOT)

from models import DispatchPulseAction, DispatchPulseObservation  # noqa: E402
from task_definitions import (  # noqa: E402
    TASKS,
    grade_submission,
    get_task,
    list_tasks as _list_tasks,
)

# Create the standard OpenEnv app (API-only β€” no Gradio UI).
app = create_fastapi_app(
    DispatchPulseEnvironment,
    DispatchPulseAction,
    DispatchPulseObservation,
)


# ---------------------------------------------------------------------------
# GET / β€” root metadata
# ---------------------------------------------------------------------------


@app.get("/", tags=["DispatchPulse"])
def root() -> Dict[str, Any]:
    """Root endpoint returning basic metadata about the environment."""
    return {
        "name": "dispatchpulse",
        "status": "ok",
        "description": "Emergency dispatch coordinator OpenEnv environment",
        "endpoints": [
            "/health",
            "/tasks",
            "/tasks/{task_id}",
            "/reset",
            "/step",
            "/state",
            "/grader",
        ],
        "tasks": ["easy", "medium", "hard"],
    }


# ---------------------------------------------------------------------------
# GET /tasks β€” list all graded tasks
# ---------------------------------------------------------------------------


_ACTION_SCHEMA = {
    "action_type": "string β€” one of: dispatch, classify, callback, wait, view",
    "text": "string β€” e.g. 'dispatch CALL-001 ALS-1 H1'",
    "call_id": "string (optional)",
    "unit_id": "string (optional)",
    "hospital_id": "string (optional)",
    "severity": "integer 1-5 (optional)",
    "message": "string (optional)",
    "minutes": "integer 1-5 (optional)",
}


@app.get("/tasks", tags=["DispatchPulse"])
def list_tasks_endpoint() -> Dict[str, Any]:
    """Return the list of graded tasks.

    DispatchPulse ships with exactly three deterministic tasks β€” ``easy``,
    ``medium``, ``hard`` β€” each with its own grader (``grade_submission``)
    that returns a score in [0.0, 1.0] at episode end.
    """
    task_list = _list_tasks()
    return {
        "tasks": [
            {
                "task_id": t.task_id,
                "name": t.name,
                "difficulty": t.difficulty,
                "description": t.description,
            }
            for t in task_list
        ],
        "total": len(TASKS),
        "action_schema": _ACTION_SCHEMA,
    }


@app.get("/tasks/{task_id}", tags=["DispatchPulse"])
def get_task_endpoint(task_id: str) -> Dict[str, Any]:
    """Return metadata for a single task by id."""
    try:
        task = get_task(task_id)
    except KeyError as exc:
        raise HTTPException(status_code=404, detail=str(exc)) from exc
    return {
        "task_id": task.task_id,
        "name": task.name,
        "difficulty": task.difficulty,
        "description": task.description,
    }


# ---------------------------------------------------------------------------
# POST /grader β€” score a submission
# ---------------------------------------------------------------------------


class GraderRequest(BaseModel):
    """Request body for POST /grader."""

    task_id: Optional[str] = Field(
        default=None, description="One of: easy | medium | hard"
    )
    seed: int = Field(default=42, description="Random seed for reproducibility")
    actions: Optional[List[Dict[str, Any]]] = Field(
        default=None,
        description=(
            "Ordered list of actions to replay (each item has action_type "
            "and required args). When omitted, grades a silent run."
        ),
    )


@app.post("/grader", tags=["DispatchPulse"])
def grader_endpoint(payload: GraderRequest) -> Dict[str, Any]:
    """Grade a task submission.

    Delegates to :func:`task_definitions.grade_submission` which is the
    canonical grader for DispatchPulse. Returns a dict with `task_id`,
    `score`, `passed`, and reward component breakdown.
    """
    task_id = (payload.task_id or "easy").strip().lower()
    try:
        score, details = grade_submission(
            task_id=task_id,
            actions=payload.actions,
            seed=int(payload.seed),
        )
    except KeyError as exc:
        raise HTTPException(status_code=404, detail=str(exc)) from exc

    return {
        "task_id": details["task_id"],
        "score": details["score"],
        "passed": details["passed"],
        "details": details["details"],
        "survival_score": details["survival_score"],
        "efficiency_score": details["efficiency_score"],
        "triage_accuracy": details["triage_accuracy"],
        "penalty": details["penalty"],
        "completed_calls": details["completed_calls"],
        "timed_out_calls": details["timed_out_calls"],
        "total_calls": details["total_calls"],
    }


def main() -> None:
    """Entry point for ``uv run server`` or direct execution."""
    import uvicorn

    uvicorn.run(app, host="0.0.0.0", port=8000)


if __name__ == "__main__":
    main()