Spaces:
Sleeping
Sleeping
Commit Β·
37ba54a
1
Parent(s): 83509d4
Fix favicon 404, clean up imports
Browse files- api/server.py +40 -54
api/server.py
CHANGED
|
@@ -6,8 +6,8 @@ from contextlib import asynccontextmanager
|
|
| 6 |
|
| 7 |
from fastapi import FastAPI, HTTPException, Request
|
| 8 |
from fastapi.middleware.cors import CORSMiddleware
|
| 9 |
-
from fastapi.responses import JSONResponse
|
| 10 |
-
from pydantic import ValidationError
|
| 11 |
|
| 12 |
from env.environment import environment
|
| 13 |
from env.models import (
|
|
@@ -30,10 +30,8 @@ _startup_time = time.time()
|
|
| 30 |
|
| 31 |
@asynccontextmanager
|
| 32 |
async def lifespan(app: FastAPI):
|
| 33 |
-
# Warm up β pre-load datasets and reset environment
|
| 34 |
environment.reset(difficulty="easy")
|
| 35 |
yield
|
| 36 |
-
# Shutdown β nothing to clean up
|
| 37 |
|
| 38 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 39 |
# APP DEFINITION
|
|
@@ -44,7 +42,7 @@ app = FastAPI(
|
|
| 44 |
description = (
|
| 45 |
"An OpenEnv-compliant reinforcement learning environment where AI agents "
|
| 46 |
"learn to debug SQL queries across syntax errors, logic bugs, and performance issues. "
|
| 47 |
-
"Built for the META
|
| 48 |
),
|
| 49 |
version = "1.0.0",
|
| 50 |
lifespan = lifespan,
|
|
@@ -73,17 +71,23 @@ async def global_exception_handler(request: Request, exc: Exception):
|
|
| 73 |
)
|
| 74 |
|
| 75 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 77 |
# 1. /health β GET
|
| 78 |
-
# Must always return 200 even if env not initialized
|
| 79 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 80 |
|
| 81 |
@app.get("/health", response_model=HealthResponse, tags=["System"])
|
| 82 |
async def health():
|
| 83 |
-
"""
|
| 84 |
-
Liveness check. Always returns 200.
|
| 85 |
-
Used by HF Space health monitoring.
|
| 86 |
-
"""
|
| 87 |
return HealthResponse(
|
| 88 |
status = "ok",
|
| 89 |
version = "1.0.0",
|
|
@@ -93,14 +97,8 @@ async def health():
|
|
| 93 |
|
| 94 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 95 |
# 2. /reset β POST
|
| 96 |
-
# Starts new episode, returns Observation
|
| 97 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 98 |
|
| 99 |
-
class ResetRequest(Action.__class__):
|
| 100 |
-
pass
|
| 101 |
-
|
| 102 |
-
from pydantic import BaseModel
|
| 103 |
-
|
| 104 |
class ResetBody(BaseModel):
|
| 105 |
difficulty: Optional[str] = None
|
| 106 |
task_id: Optional[str] = None
|
|
@@ -108,9 +106,7 @@ class ResetBody(BaseModel):
|
|
| 108 |
@app.post("/reset", response_model=Observation, tags=["Environment"])
|
| 109 |
async def reset(body: ResetBody = ResetBody()):
|
| 110 |
"""
|
| 111 |
-
Starts a fresh episode.
|
| 112 |
-
Returns the initial Observation the agent sees.
|
| 113 |
-
|
| 114 |
Edge case: always returns valid Observation even if dataset issues occur.
|
| 115 |
"""
|
| 116 |
try:
|
|
@@ -127,7 +123,6 @@ async def reset(body: ResetBody = ResetBody()):
|
|
| 127 |
|
| 128 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 129 |
# 3. /step β POST
|
| 130 |
-
# Accepts Action, returns StepResponse
|
| 131 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 132 |
|
| 133 |
@app.post("/step", response_model=StepResponse, tags=["Environment"])
|
|
@@ -135,21 +130,16 @@ async def step(action: Action):
|
|
| 135 |
"""
|
| 136 |
Submits an action to the environment.
|
| 137 |
Returns (observation, reward, done, info).
|
| 138 |
-
|
| 139 |
-
Edge cases:
|
| 140 |
-
- Invalid/malformed action β reward=-0.1, done=False
|
| 141 |
-
- Episode already done β returns terminal state
|
| 142 |
-
- Null payload β graceful penalty
|
| 143 |
"""
|
| 144 |
try:
|
| 145 |
response = environment.step(action)
|
| 146 |
return response
|
| 147 |
except ValidationError as e:
|
| 148 |
-
|
| 149 |
-
obs = environment.state()
|
| 150 |
return StepResponse(
|
| 151 |
observation = environment._build_observation(),
|
| 152 |
-
reward =
|
| 153 |
score = -0.1,
|
| 154 |
breakdown = {"validation_error": -0.1},
|
| 155 |
feedback = f"Malformed action: {str(e)}"
|
|
@@ -163,7 +153,6 @@ async def step(action: Action):
|
|
| 163 |
|
| 164 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 165 |
# 4. /state β GET
|
| 166 |
-
# Returns current environment state
|
| 167 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 168 |
|
| 169 |
@app.get("/state", response_model=EpisodeState, tags=["Environment"])
|
|
@@ -171,14 +160,13 @@ async def state():
|
|
| 171 |
"""
|
| 172 |
Returns full current environment state.
|
| 173 |
Works before reset() is called β returns default empty state.
|
| 174 |
-
|
| 175 |
"""
|
| 176 |
return environment.state()
|
| 177 |
|
| 178 |
|
| 179 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 180 |
# 5. /tasks β GET
|
| 181 |
-
# Lists all tasks + action schema
|
| 182 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 183 |
|
| 184 |
@app.get("/tasks", response_model=TaskListResponse, tags=["Tasks"])
|
|
@@ -197,28 +185,22 @@ async def tasks():
|
|
| 197 |
|
| 198 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 199 |
# 6. /grader β POST
|
| 200 |
-
# Grades a completed episode
|
| 201 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 202 |
|
| 203 |
@app.post("/grader", response_model=GraderResponse, tags=["Grading"])
|
| 204 |
async def grader(request: GraderRequest):
|
| 205 |
"""
|
| 206 |
-
Grades a completed episode.
|
| 207 |
-
Returns float score
|
| 208 |
-
|
| 209 |
-
Edge cases:
|
| 210 |
-
- Null/empty episode β returns 0.0, never crashes
|
| 211 |
-
- Unknown task_id β returns 0.0 with explanation
|
| 212 |
"""
|
| 213 |
try:
|
| 214 |
-
# Edge case: null action in request
|
| 215 |
if request.action is None:
|
| 216 |
return GraderResponse(
|
| 217 |
score = 0.0,
|
| 218 |
feedback = "No action provided for grading.",
|
| 219 |
breakdown = {"error": "null_action"}
|
| 220 |
)
|
| 221 |
-
|
| 222 |
score, breakdown, feedback = grade(request.action, request.task_id)
|
| 223 |
return GraderResponse(
|
| 224 |
score = score,
|
|
@@ -226,7 +208,6 @@ async def grader(request: GraderRequest):
|
|
| 226 |
breakdown = breakdown
|
| 227 |
)
|
| 228 |
except Exception as e:
|
| 229 |
-
# Never crash β return 0.0
|
| 230 |
return GraderResponse(
|
| 231 |
score = 0.0,
|
| 232 |
feedback = f"Grader error: {str(e)}",
|
|
@@ -236,8 +217,6 @@ async def grader(request: GraderRequest):
|
|
| 236 |
|
| 237 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 238 |
# 7. /baseline β POST
|
| 239 |
-
# Runs baseline inference, returns scores
|
| 240 |
-
# Must complete within 60 seconds
|
| 241 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 242 |
|
| 243 |
@app.post("/baseline", response_model=BaselineResponse, tags=["Baseline"])
|
|
@@ -245,38 +224,45 @@ async def baseline():
|
|
| 245 |
"""
|
| 246 |
Runs the baseline agent against all 3 difficulty levels.
|
| 247 |
Returns scores JSON. Must complete within 60 seconds.
|
| 248 |
-
|
| 249 |
-
Edge case: OPENAI_API_KEY not set β returns error scores without crashing.
|
| 250 |
"""
|
| 251 |
try:
|
| 252 |
-
# Import here to avoid circular imports
|
| 253 |
import baseline as baseline_module
|
| 254 |
results = await asyncio.wait_for(
|
| 255 |
asyncio.to_thread(baseline_module.run_baseline),
|
| 256 |
-
timeout=55.0
|
| 257 |
)
|
| 258 |
return results
|
| 259 |
except asyncio.TimeoutError:
|
| 260 |
-
# Return partial results on timeout
|
| 261 |
return BaselineResponse(
|
| 262 |
results=[
|
| 263 |
-
BaselineResult(
|
| 264 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
],
|
| 266 |
average_score=0.0
|
| 267 |
)
|
| 268 |
except Exception as e:
|
| 269 |
return BaselineResponse(
|
| 270 |
results=[
|
| 271 |
-
BaselineResult(
|
| 272 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
],
|
| 274 |
average_score=0.0
|
| 275 |
)
|
| 276 |
|
| 277 |
|
| 278 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 279 |
-
# ROOT β
|
| 280 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 281 |
|
| 282 |
@app.get("/", tags=["System"])
|
|
@@ -287,7 +273,7 @@ async def root():
|
|
| 287 |
"docs": "/docs",
|
| 288 |
"health": "/health",
|
| 289 |
"endpoints": ["/reset", "/step", "/state", "/tasks", "/grader", "/baseline", "/health"],
|
| 290 |
-
"hackathon": "META
|
| 291 |
"domain": "SQL Query Debugging",
|
| 292 |
"tasks_count": 15,
|
| 293 |
}
|
|
|
|
| 6 |
|
| 7 |
from fastapi import FastAPI, HTTPException, Request
|
| 8 |
from fastapi.middleware.cors import CORSMiddleware
|
| 9 |
+
from fastapi.responses import JSONResponse, Response
|
| 10 |
+
from pydantic import BaseModel, ValidationError
|
| 11 |
|
| 12 |
from env.environment import environment
|
| 13 |
from env.models import (
|
|
|
|
| 30 |
|
| 31 |
@asynccontextmanager
|
| 32 |
async def lifespan(app: FastAPI):
|
|
|
|
| 33 |
environment.reset(difficulty="easy")
|
| 34 |
yield
|
|
|
|
| 35 |
|
| 36 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 37 |
# APP DEFINITION
|
|
|
|
| 42 |
description = (
|
| 43 |
"An OpenEnv-compliant reinforcement learning environment where AI agents "
|
| 44 |
"learn to debug SQL queries across syntax errors, logic bugs, and performance issues. "
|
| 45 |
+
"Built for the META x PyTorch x SST OpenEnv Hackathon."
|
| 46 |
),
|
| 47 |
version = "1.0.0",
|
| 48 |
lifespan = lifespan,
|
|
|
|
| 71 |
)
|
| 72 |
|
| 73 |
|
| 74 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 75 |
+
# FAVICON β fix 404
|
| 76 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 77 |
+
|
| 78 |
+
@app.get("/favicon.ico", include_in_schema=False)
|
| 79 |
+
async def favicon():
|
| 80 |
+
"""Returns 204 No Content instead of 404 for favicon requests."""
|
| 81 |
+
return Response(status_code=204)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 85 |
# 1. /health β GET
|
|
|
|
| 86 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 87 |
|
| 88 |
@app.get("/health", response_model=HealthResponse, tags=["System"])
|
| 89 |
async def health():
|
| 90 |
+
"""Liveness check. Always returns 200. Used by HF Space health monitoring."""
|
|
|
|
|
|
|
|
|
|
| 91 |
return HealthResponse(
|
| 92 |
status = "ok",
|
| 93 |
version = "1.0.0",
|
|
|
|
| 97 |
|
| 98 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 99 |
# 2. /reset β POST
|
|
|
|
| 100 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 101 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
class ResetBody(BaseModel):
|
| 103 |
difficulty: Optional[str] = None
|
| 104 |
task_id: Optional[str] = None
|
|
|
|
| 106 |
@app.post("/reset", response_model=Observation, tags=["Environment"])
|
| 107 |
async def reset(body: ResetBody = ResetBody()):
|
| 108 |
"""
|
| 109 |
+
Starts a fresh episode. Returns the initial Observation the agent sees.
|
|
|
|
|
|
|
| 110 |
Edge case: always returns valid Observation even if dataset issues occur.
|
| 111 |
"""
|
| 112 |
try:
|
|
|
|
| 123 |
|
| 124 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 125 |
# 3. /step β POST
|
|
|
|
| 126 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 127 |
|
| 128 |
@app.post("/step", response_model=StepResponse, tags=["Environment"])
|
|
|
|
| 130 |
"""
|
| 131 |
Submits an action to the environment.
|
| 132 |
Returns (observation, reward, done, info).
|
| 133 |
+
Edge cases: null action, malformed payload, episode already done.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
"""
|
| 135 |
try:
|
| 136 |
response = environment.step(action)
|
| 137 |
return response
|
| 138 |
except ValidationError as e:
|
| 139 |
+
from env.models import Reward
|
|
|
|
| 140 |
return StepResponse(
|
| 141 |
observation = environment._build_observation(),
|
| 142 |
+
reward = Reward(
|
| 143 |
score = -0.1,
|
| 144 |
breakdown = {"validation_error": -0.1},
|
| 145 |
feedback = f"Malformed action: {str(e)}"
|
|
|
|
| 153 |
|
| 154 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 155 |
# 4. /state β GET
|
|
|
|
| 156 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 157 |
|
| 158 |
@app.get("/state", response_model=EpisodeState, tags=["Environment"])
|
|
|
|
| 160 |
"""
|
| 161 |
Returns full current environment state.
|
| 162 |
Works before reset() is called β returns default empty state.
|
| 163 |
+
Always JSON-serializable. Never crashes.
|
| 164 |
"""
|
| 165 |
return environment.state()
|
| 166 |
|
| 167 |
|
| 168 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 169 |
# 5. /tasks β GET
|
|
|
|
| 170 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 171 |
|
| 172 |
@app.get("/tasks", response_model=TaskListResponse, tags=["Tasks"])
|
|
|
|
| 185 |
|
| 186 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 187 |
# 6. /grader β POST
|
|
|
|
| 188 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 189 |
|
| 190 |
@app.post("/grader", response_model=GraderResponse, tags=["Grading"])
|
| 191 |
async def grader(request: GraderRequest):
|
| 192 |
"""
|
| 193 |
+
Grades a completed episode action.
|
| 194 |
+
Returns float score 0.0-1.0. Never crashes.
|
| 195 |
+
Edge cases: null action β 0.0, unknown task β 0.0.
|
|
|
|
|
|
|
|
|
|
| 196 |
"""
|
| 197 |
try:
|
|
|
|
| 198 |
if request.action is None:
|
| 199 |
return GraderResponse(
|
| 200 |
score = 0.0,
|
| 201 |
feedback = "No action provided for grading.",
|
| 202 |
breakdown = {"error": "null_action"}
|
| 203 |
)
|
|
|
|
| 204 |
score, breakdown, feedback = grade(request.action, request.task_id)
|
| 205 |
return GraderResponse(
|
| 206 |
score = score,
|
|
|
|
| 208 |
breakdown = breakdown
|
| 209 |
)
|
| 210 |
except Exception as e:
|
|
|
|
| 211 |
return GraderResponse(
|
| 212 |
score = 0.0,
|
| 213 |
feedback = f"Grader error: {str(e)}",
|
|
|
|
| 217 |
|
| 218 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 219 |
# 7. /baseline β POST
|
|
|
|
|
|
|
| 220 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 221 |
|
| 222 |
@app.post("/baseline", response_model=BaselineResponse, tags=["Baseline"])
|
|
|
|
| 224 |
"""
|
| 225 |
Runs the baseline agent against all 3 difficulty levels.
|
| 226 |
Returns scores JSON. Must complete within 60 seconds.
|
| 227 |
+
Edge case: OPENAI_API_KEY not set β continues with rule-based agent.
|
|
|
|
| 228 |
"""
|
| 229 |
try:
|
|
|
|
| 230 |
import baseline as baseline_module
|
| 231 |
results = await asyncio.wait_for(
|
| 232 |
asyncio.to_thread(baseline_module.run_baseline),
|
| 233 |
+
timeout=55.0
|
| 234 |
)
|
| 235 |
return results
|
| 236 |
except asyncio.TimeoutError:
|
|
|
|
| 237 |
return BaselineResponse(
|
| 238 |
results=[
|
| 239 |
+
BaselineResult(
|
| 240 |
+
task_id = "timeout",
|
| 241 |
+
difficulty = DifficultyLevel.EASY,
|
| 242 |
+
score = 0.0,
|
| 243 |
+
steps = 0,
|
| 244 |
+
feedback = "Baseline timed out after 55 seconds."
|
| 245 |
+
)
|
| 246 |
],
|
| 247 |
average_score=0.0
|
| 248 |
)
|
| 249 |
except Exception as e:
|
| 250 |
return BaselineResponse(
|
| 251 |
results=[
|
| 252 |
+
BaselineResult(
|
| 253 |
+
task_id = "error",
|
| 254 |
+
difficulty = DifficultyLevel.EASY,
|
| 255 |
+
score = 0.0,
|
| 256 |
+
steps = 0,
|
| 257 |
+
feedback = f"Baseline error: {str(e)}"
|
| 258 |
+
)
|
| 259 |
],
|
| 260 |
average_score=0.0
|
| 261 |
)
|
| 262 |
|
| 263 |
|
| 264 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 265 |
+
# ROOT β project info
|
| 266 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 267 |
|
| 268 |
@app.get("/", tags=["System"])
|
|
|
|
| 273 |
"docs": "/docs",
|
| 274 |
"health": "/health",
|
| 275 |
"endpoints": ["/reset", "/step", "/state", "/tasks", "/grader", "/baseline", "/health"],
|
| 276 |
+
"hackathon": "META x PyTorch x SST OpenEnv Hackathon",
|
| 277 |
"domain": "SQL Query Debugging",
|
| 278 |
"tasks_count": 15,
|
| 279 |
}
|