Spaces:
Sleeping
Sleeping
| """FastAPI app wrapper for running Bug Triage OpenEnv in containerized environments.""" | |
| from __future__ import annotations | |
| import json | |
| from pathlib import Path | |
| from typing import Optional | |
| from fastapi import FastAPI, HTTPException, Request | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import FileResponse, HTMLResponse, Response | |
| from fastapi.staticfiles import StaticFiles | |
| from pydantic import BaseModel | |
| from .env import BugTriageEnv | |
| from .models import ActionModel | |
| from .policy import suggest_action | |
| from .tasks import list_tasks | |
| app = FastAPI( | |
| title="Bug Triage OpenEnv", | |
| version="0.1.0", | |
| description="Real-world OpenEnv environment for bug triage training", | |
| ) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| UI_DIR = Path(__file__).parent / "ui" | |
| if UI_DIR.exists(): | |
| app.mount("/ui", StaticFiles(directory=str(UI_DIR)), name="ui") | |
| env = BugTriageEnv() | |
| BASELINE_ARTIFACT = Path(__file__).resolve().parents[1] / "artifacts" / "baseline_scores.json" | |
| FALLBACK_BASELINE = { | |
| "model": "offline-heuristic", | |
| "offline_mode": True, | |
| "mean_score": 0.7698, | |
| "results": [ | |
| {"task_id": "bug_triage_easy", "score": 0.9920, "passed": True}, | |
| {"task_id": "bug_triage_medium", "score": 0.6715, "passed": False}, | |
| {"task_id": "bug_triage_hard", "score": 0.6460, "passed": False}, | |
| ], | |
| } | |
| class ResetRequest(BaseModel): | |
| task_id: Optional[str] = None | |
| seed: Optional[int] = None | |
| class StepRequest(BaseModel): | |
| action: ActionModel | |
| def _app_base_path(request: Request) -> str: | |
| """Return proxy-aware base path without a trailing slash.""" | |
| root_path = (request.scope.get("root_path") or "").rstrip("/") | |
| return root_path | |
| def index(request: Request): | |
| """Serve the frontend with proxy-aware asset URLs.""" | |
| index_file = UI_DIR / "index.html" | |
| if index_file.exists(): | |
| html = index_file.read_text(encoding="utf-8") | |
| html = html.replace("__APP_BASE__", _app_base_path(request)) | |
| return HTMLResponse(content=html) | |
| return {"message": "Bug Triage OpenEnv API", "docs": "/docs"} | |
| def favicon() -> Response: | |
| """Serve a small favicon so browser startup stays clean.""" | |
| icon_file = UI_DIR / "favicon.svg" | |
| if icon_file.exists(): | |
| return FileResponse(icon_file, media_type="image/svg+xml") | |
| return Response(status_code=204) | |
| def health() -> dict[str, str]: | |
| return { | |
| "status": "ok", | |
| "environment": "bug-triage-openenv", | |
| "version": app.version, | |
| } | |
| def tasks() -> dict: | |
| registry = list_tasks() | |
| return { | |
| "total": len(registry), | |
| "tasks": [ | |
| { | |
| "id": task_id, | |
| "difficulty": meta["difficulty"], | |
| "description": meta["description"], | |
| } | |
| for task_id, meta in registry.items() | |
| ], | |
| } | |
| def baseline() -> dict: | |
| if BASELINE_ARTIFACT.exists(): | |
| try: | |
| return json.loads(BASELINE_ARTIFACT.read_text(encoding="utf-8")) | |
| except (OSError, json.JSONDecodeError): | |
| pass | |
| return FALLBACK_BASELINE | |
| def suggest_current_action() -> dict: | |
| observation = env._get_observation() | |
| history: list[str] = [] | |
| if env.current_task is not None and env.current_ticket_index < len(env.ticket_states): | |
| history = list(env.ticket_states[env.current_ticket_index]["actions_taken"]) | |
| action, reason = suggest_action(observation=observation, action_history=history) | |
| return { | |
| "action": action.model_dump(mode="json", exclude_none=True), | |
| "reason": reason, | |
| "history": history, | |
| } | |
| def reset_get(task_id: Optional[str] = None, seed: Optional[int] = None) -> dict: | |
| """Validator-friendly reset endpoint (GET).""" | |
| try: | |
| observation = env.reset(task_id=task_id, seed=seed) | |
| except (FileNotFoundError, ValueError) as exc: | |
| raise HTTPException(status_code=400, detail=str(exc)) from exc | |
| return observation.model_dump(mode="json") | |
| def reset_post(req: ResetRequest) -> dict: | |
| """Typed reset endpoint (POST).""" | |
| try: | |
| observation = env.reset(task_id=req.task_id, seed=req.seed) | |
| except (FileNotFoundError, ValueError) as exc: | |
| raise HTTPException(status_code=400, detail=str(exc)) from exc | |
| return observation.model_dump(mode="json") | |
| def step(req: StepRequest) -> dict: | |
| try: | |
| observation, reward, done, info = env.step(req.action) | |
| except RuntimeError as exc: | |
| raise HTTPException(status_code=400, detail=str(exc)) from exc | |
| return { | |
| "observation": observation.model_dump(mode="json"), | |
| "reward": reward.model_dump(mode="json"), | |
| "done": done, | |
| "info": info, | |
| } | |
| def state() -> dict: | |
| if env.current_task is None: | |
| return { | |
| "initialized": False, | |
| "current_task_id": None, | |
| "current_ticket_index": 0, | |
| "total_tickets": 0, | |
| "tickets_state": [], | |
| "steps_used": 0, | |
| "steps_remaining": 0, | |
| "cumulative_reward": 0.0, | |
| "episode_done": False, | |
| } | |
| current_state = env.state() | |
| payload = current_state.model_dump(mode="json") | |
| payload["initialized"] = True | |
| return payload | |