File size: 9,212 Bytes
fe0c391
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4b98131
fe0c391
 
 
 
 
 
 
 
 
 
 
4b98131
fe0c391
 
 
 
 
 
 
 
 
 
4b98131
fe0c391
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4b98131
fe0c391
 
 
 
 
 
 
4b98131
fe0c391
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4b98131
fe0c391
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4b98131
fe0c391
 
 
 
 
 
 
 
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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

"""
FastAPI application for the Smart Emergency Environment.

Endpoints:
    POST /reset    β€” Reset the environment, start a new episode
    POST /step     β€” Submit an action, receive next observation + reward
    GET  /state    β€” Current episode state
    GET  /health   β€” Health check
    GET  /tasks    β€” Available difficulty tasks
    POST /grader   β€” Score a completed episode (call after done=True)
    GET  /baseline β€” Run rule-based agent across all 3 tasks
    WS   /ws       β€” WebSocket for persistent low-latency sessions
    GET  /docs     β€” Swagger UI (auto-generated)
"""

from openenv.core.env_server.http_server import create_app

try:
    from ..models import SmartEmergencyAction, SmartEmergencyObservation, RerouteAction
    from .smart_emergency_environment import SmartEmergencyEnvironment
except (ImportError, ModuleNotFoundError):
    from models import SmartEmergencyAction, SmartEmergencyObservation, RerouteAction
    from server.smart_emergency_environment import SmartEmergencyEnvironment


# App

# We use create_app so OpenEnv can automatically mount its Gradio web UI at / and /web
# when deployed to Hugging Face Spaces.
app = create_app(
    SmartEmergencyEnvironment,
    SmartEmergencyAction,
    SmartEmergencyObservation,
    env_name="smart_emergency",
    max_concurrent_envs=1,
)

# Health

@app.get("/health")
def health():
    return {
        "status": "healthy",
        "environment": "smart-emergency-dispatch911",
        "version": "1.0.0",
    }


# Tasks

@app.get("/tasks")
def tasks():
    """List available difficulty tasks."""
    return {
        "tasks": [
            {
                "id": 1,
                "name": "Basic Dispatch",
                "difficulty": "easy",
                "description": "10 steps, 3 vehicles per type, 10% duplicates. Focus on severity and vehicle type.",
                "reward_max": 6.7,
            },
            {
                "id": 2,
                "name": "Scarce Resources",
                "difficulty": "medium",
                "description": "15 steps, 2 vehicles per type, 30% duplicates. Must handle holds and pick nearest units.",
                "reward_max": 6.7,
            },
            {
                "id": 3,
                "name": "Full Disaster Response",
                "difficulty": "hard",
                "description": "20 steps, 1 vehicle per type, 50% duplicates. Requires reroutes and optimal triage.",
                "reward_max": 6.7,
            },
        ]
    }


# Grader

@app.post("/grader")
def grader():
    """
    Score the completed episode. Call this after done=True.

    Returns cumulative reward breakdown, per-component averages,
    and a normalized 0-1 score suitable for hackathon leaderboards.
    """
    steps = SmartEmergencyEnvironment.latest_steps

    if steps == 0:
        raise HTTPException(
            status_code=400,
            detail="No episode in progress. Call POST /reset first.",
        )

    # Collect reward history from the class-level tracker
    history = SmartEmergencyEnvironment.latest_history
    if not history:
        raise HTTPException(
            status_code=400,
            detail=(
                "Episode not yet complete or no steps taken. "
                "Keep calling POST /step until observation.done == true."
            ),
        )

    # Aggregate per-component averages
    keys = ["severity", "duplicate", "vehicle_type", "vehicle_choice", "reroute", "total"]
    component_totals = {k: 0.0 for k in keys}
    raw_cumulative = 0.0
    for breakdown in history:
        for k in keys:
            component_totals[k] += breakdown.get(k, 0.0)
        raw_cumulative += breakdown.get("raw_total", breakdown.get("total", 0.0))

    n = max(1, len(history))
    component_avgs = {k: round(v / n, 4) for k, v in component_totals.items()}
    cumulative = round(component_totals["total"], 4)

    # Normalize using raw total (before baseline subtraction) for a fair 0–1 score
    MAX_PER_STEP = 6.7
    score = round(max(0.0, min(1.0, raw_cumulative / (MAX_PER_STEP * n))), 4)

    return {
        "score": score,
        "cumulative_reward": cumulative,
        "raw_cumulative_reward": round(raw_cumulative, 4),
        "steps": steps,
        "episode_id": SmartEmergencyEnvironment.latest_episode_id,
        "reward_components": {
            "severity_avg": component_avgs["severity"],
            "duplicate_avg": component_avgs["duplicate"],
            "vehicle_type_avg": component_avgs["vehicle_type"],
            "vehicle_choice_avg": component_avgs["vehicle_choice"],
            "reroute_avg": component_avgs["reroute"],
        },
        "per_step_total_avg": component_avgs["total"],
    }


# Baseline

@app.get("/baseline")
def baseline():
    """
    Run a keyword-heuristic rule-based agent across all 3 tasks.
    Returns per-task scores and an overall average.
    Required for hackathon submission.
    """

    def _classify_severity(transcript: str) -> int:
        t = transcript.lower()
        if any(w in t for w in ["not breathing", "collapsed", "not responding",
                                  "active shooter", "trapped", "mass incident",
                                  "massive fire", "whole block", "not moving"]):
            return 5
        if any(w in t for w in ["won't wake", "unconscious", "not responding",
                                  "gunshots", "flipped", "blood everywhere",
                                  "people yelling", "pileup"]):
            return 4
        if any(w in t for w in ["chest pain", "fight", "mugged", "knife",
                                  "crash", "hurt", "bleeding", "fire at",
                                  "flames", "cyclist"]):
            return 3
        if any(w in t for w in ["fainted", "break-in", "dumpster", "fender",
                                  "small fire", "ankle"]):
            return 2
        return 1

    def _classify_vehicle(transcript: str) -> str:
        t = transcript.lower()
        if any(w in t for w in ["fire", "flames", "smoke", "burning", "gas"]):
            return "fire"
        if any(w in t for w in ["shooter", "gunshot", "mugged", "knife",
                                  "break-in", "fight", "shoplifter", "crime"]):
            return "police"
        return "ambulance"

    def _pick_vehicle(env: SmartEmergencyEnvironment, vtype: str):
        if env._city is None:
            return None
        for v in env._city.vehicles:
            if v.vehicle_type == vtype and v.status == "FREE":
                return v.unit_id
        return None

    def _rule_agent(env: SmartEmergencyEnvironment, obs) -> SmartEmergencyAction:
        call = env._current_call
        if call is None:
            return SmartEmergencyAction(
                action_type="dispatch",
                severity_pred=1,
                is_duplicate=False,
                vehicle_type="police",
            )

        # Check for duplicates heuristically
        if obs.active_event_ids and env._current_call and env._current_call.is_duplicate_of:
            dup_id = env._current_call.is_duplicate_of
            return SmartEmergencyAction(
                action_type="duplicate",
                severity_pred=call.severity,
                is_duplicate=True,
                duplicate_of_event_id=dup_id,
            )

        transcript = obs.prompt
        sev = _classify_severity(transcript)
        vtype = _classify_vehicle(transcript)
        vid = _pick_vehicle(env, vtype)

        return SmartEmergencyAction(
            action_type="dispatch",
            severity_pred=sev,
            is_duplicate=False,
            vehicle_type=vtype,
            vehicle_id=vid,
        )

    all_scores = {}
    for task_id in [1, 2, 3]:
        env = SmartEmergencyEnvironment()
        obs = env.reset()
        total_reward = 0.0
        steps = 0
        MAX_STEPS = 20

        while not obs.done and steps < MAX_STEPS:
            action = _rule_agent(env, obs)
            try:
                obs = env.step(action)
                total_reward += obs.reward_breakdown.get("raw_total", obs.reward_breakdown.get("total", 0.0))
            except Exception:
                break
            steps += 1

        MAX_PER_STEP = 6.7
        score = round(max(0.0, min(1.0, total_reward / (MAX_PER_STEP * max(1, steps)))), 4)

        all_scores[f"task_{task_id}"] = {
            "score": score,
            "cumulative_reward": round(total_reward, 4),
            "steps": steps,
            "difficulty": ["easy", "medium", "hard"][task_id - 1],
        }

    avg = round(sum(v["score"] for v in all_scores.values()) / 3, 4)
    return {
        "baseline_agent": "keyword-heuristic rule-based",
        "average_score": avg,
        "tasks": all_scores,
    }


# Entry point

def main(host: str = "0.0.0.0", port: int = 8000):
    import uvicorn
    uvicorn.run(app, host=host, port=port)


if __name__ == "__main__":
    main()