AmitSJ/github-issue-triage
0
1# app.py2# The FastAPI web server — exposes the environment over HTTP3#4# This file does 3 things:5# 1. Creates the FastAPI application6# 2. Creates ONE shared instance of GitHubTriageEnvironment7# 3. Defines HTTP endpoints: /reset, /step, /state8#9# Once this runs, any AI agent can connect via HTTP and use the environment10# It also auto-generates a Swagger UI at /docs for visual testing11 12from fastapi import FastAPI, HTTPException13from fastapi.staticfiles import StaticFiles14from fastapi.responses import FileResponse, JSONResponse, RedirectResponse15from fastapi.middleware.cors import CORSMiddleware16from pydantic import BaseModel17import os18 19from env.environment import GitHubTriageEnvironment20from env.models import TriageAction, ResetResult, StepResult, EpisodeState21 22# ─────────────────────────────────────────────23# Create the FastAPI app24# The title and description appear in Swagger UI at /docs25# ─────────────────────────────────────────────26 27app = FastAPI(28 title = "GitHub Issue Triage Environment",29 description = (30 "An OpenEnv-compatible reinforcement learning environment "31 "where AI agents learn to triage GitHub issues. "32 "Agents classify issue types, assign priorities, route to teams, "33 "and estimate effort — earning rewards for correct decisions.\n\n"34 "**Tasks:**\n"35 "- task_1 (Easy): Issue Classification\n"36 "- task_2 (Medium): Priority Assignment\n"37 "- task_3 (Hard): Full Triage\n\n"38 "**Usage:** Call /reset to start, /step to submit actions, /state for metadata."39 ),40 version = "1.0.0",41)42 43# ─────────────────────────────────────────────44# ONE shared environment instance45# All HTTP requests use this same instance46# This way the episode state is preserved across calls47# (reset() sets state, step() reads it, state() reads it)48# ─────────────────────────────────────────────49 50env = GitHubTriageEnvironment()51 52# Allow all origins so automated checkers and agents can access the API53app.add_middleware(54 CORSMiddleware,55 allow_origins=["*"],56 allow_methods=["*"],57 allow_headers=["*"],58)59 60 61# ─────────────────────────────────────────────62# Request body model for /reset63# Agent sends: { "task_id": "task_1" }64# ─────────────────────────────────────────────65 66class ResetRequest(BaseModel):67 task_id: str = "task_1" # Default to easiest task if not specified68 69 70# ─────────────────────────────────────────────71# ENDPOINTS72# ─────────────────────────────────────────────73 74@app.get("/", summary="Welcome", include_in_schema=False)75def root():76 """Redirects to the visual dashboard."""77 return RedirectResponse(url="/dashboard")78 79 80@app.post("/reset", response_model=ResetResult, summary="Start a new episode")81def reset(request: ResetRequest = None):82 """83 **Start a new training episode.**84 - Clears all previous episode state85 - Returns the first GitHub issue for the agent to triage86 - Body is optional — defaults to task_1 if not provided87 88 **Request body (optional):**89 ```json90 { "task_id": "task_1" }91 ```92 task_id options: task_1 (easy) | task_2 (medium) | task_3 (hard)93 """94 try:95 task_id = request.task_id if request else "task_1"96 result = env.reset(task_id=task_id)97 return result98 except ValueError as e:99 raise HTTPException(status_code=400, detail=str(e))100 101 102@app.post("/step", response_model=StepResult, summary="Submit a triage action")103def step(action: TriageAction):104 """105 **Submit the agent's triage decision for the current issue.**106 107 - Scores the action against the correct answer108 - Returns reward (0.0–1.0) + feedback + next issue109 - Returns is_done=True when episode ends110 111 **Request body:**112 ```json113 {114 "issue_type" : "bug",115 "priority" : "P1",116 "team" : "backend",117 "estimated_effort" : "small"118 }119 ```120 """121 try:122 result = env.step(action)123 return result124 except RuntimeError as e:125 # step() called before reset()126 raise HTTPException(status_code=400, detail=str(e))127 128 129@app.get("/state", response_model=EpisodeState, summary="Get current episode state")130def state():131 """132 **Get metadata about the current episode.**133 134 Returns information like:135 - episode_id, current_task136 - step_count and max_steps137 - total_reward accumulated so far138 - is_active (whether an episode is running)139 140 Does NOT advance the episode — purely informational.141 """142 return env.state()143 144 145@app.get("/health", summary="Health check")146def health():147 """Simple health check endpoint — used by Docker and Hugging Face to verify server is running."""148 return {"status": "healthy", "environment": "github-issue-triage"}149 150 151# ─────────────────────────────────────────────152# Dashboard endpoint — serves the visual HTML UI153# We will build static/dashboard.html in Phase 4154# ─────────────────────────────────────────────155 156@app.get("/dashboard", summary="Visual dashboard", include_in_schema=False)157def dashboard():158 """Serves the visual web dashboard for demo purposes."""159 dashboard_path = os.path.join("static", "dashboard.html")160 if os.path.exists(dashboard_path):161 return FileResponse(dashboard_path)162 return JSONResponse(163 status_code = 200,164 content = {"message": "Dashboard coming soon! Use /docs for now."}165 )166 167 168# ─────────────────────────────────────────────169# Run the server170# Only executes when file is run directly: python app.py171# (Not when imported by other files)172# ─────────────────────────────────────────────173 174def main():175 """Named entry point for pyproject.toml scripts — runs the server."""176 import uvicorn177 uvicorn.run(178 "app:app",179 host = "0.0.0.0",180 port = 7860,181 reload = False,182 )183 184if __name__ == "__main__":185 main()186 