junaid0600 commited on
Commit
37ba54a
Β·
1 Parent(s): 83509d4

Fix favicon 404, clean up imports

Browse files
Files changed (1) hide show
  1. 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 Γ— PyTorch Γ— SST OpenEnv Hackathon."
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
- # Malformed action β€” return penalty reward, never crash
149
- obs = environment.state()
150
  return StepResponse(
151
  observation = environment._build_observation(),
152
- reward = __import__("env.models", fromlist=["Reward"]).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
- Must always be JSON-serializable.
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 between 0.0 and 1.0.
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 # 5s buffer before 60s limit
257
  )
258
  return results
259
  except asyncio.TimeoutError:
260
- # Return partial results on timeout
261
  return BaselineResponse(
262
  results=[
263
- BaselineResult(task_id="timeout", difficulty=DifficultyLevel.EASY,
264
- score=0.0, steps=0, feedback="Baseline timed out after 55 seconds.")
 
 
 
 
 
265
  ],
266
  average_score=0.0
267
  )
268
  except Exception as e:
269
  return BaselineResponse(
270
  results=[
271
- BaselineResult(task_id="error", difficulty=DifficultyLevel.EASY,
272
- score=0.0, steps=0, feedback=f"Baseline error: {str(e)}")
 
 
 
 
 
273
  ],
274
  average_score=0.0
275
  )
276
 
277
 
278
  # ─────────────────────────────────────────────
279
- # ROOT β€” redirect to docs
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 Γ— PyTorch Γ— SST OpenEnv Hackathon",
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
  }