Souravdanyal commited on
Commit
2785b89
·
1 Parent(s): b464fa4

Add timeout protection against infinite loops, fix 500 errors

Browse files
Files changed (2) hide show
  1. server/app.py +60 -23
  2. server/graders/grader_easy.py +25 -4
server/app.py CHANGED
@@ -1,4 +1,7 @@
1
  # server/app.py
 
 
 
2
  from fastapi import FastAPI, HTTPException
3
  from fastapi.middleware.cors import CORSMiddleware
4
  from fastapi.responses import HTMLResponse
@@ -7,8 +10,7 @@ from pydantic import BaseModel
7
  import os
8
 
9
  from server.environment import CodeDebugEnvironment
10
- from models import DebugAction
11
- from fastapi.responses import HTMLResponse, Response
12
 
13
  app = FastAPI(
14
  title="Code Debug Environment",
@@ -26,29 +28,15 @@ app.add_middleware(
26
  allow_headers=["*"],
27
  )
28
 
 
 
29
  env = CodeDebugEnvironment()
30
 
31
 
32
- @app.get("/favicon.ico", include_in_schema=False)
33
- async def favicon():
34
- # Simple 1x1 transparent icon
35
- return Response(content=b"", media_type="image/x-icon")
36
-
37
- @app.get("/", response_class=HTMLResponse)
38
- async def root():
39
- """Homepage with live tester UI."""
40
- html_path = os.path.join(os.path.dirname(__file__), "static", "index.html")
41
- with open(html_path, "r", encoding="utf-8") as f:
42
- return f.read()
43
-
44
-
45
- @app.get("/health")
46
- async def health():
47
- return {"status": "ok", "environment": "code-debug-env", "version": "1.0.0"}
48
-
49
 
50
  class ResetRequest(BaseModel):
51
- difficulty: Optional[str] = None
52
 
53
 
54
  class StepRequest(BaseModel):
@@ -56,47 +44,96 @@ class StepRequest(BaseModel):
56
  explanation: Optional[str] = None
57
 
58
 
 
 
59
  class StepResponse(BaseModel):
60
  observation: dict
61
  reward: float
62
  done: bool
63
 
64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  @app.post("/reset")
66
  async def reset(request: ResetRequest = ResetRequest()) -> dict:
 
 
 
 
67
  try:
68
  observation = env.reset(difficulty=request.difficulty)
69
- return {"observation": observation.model_dump(), "reward": 0.0, "done": False}
 
 
 
 
70
  except Exception as e:
71
  raise HTTPException(status_code=500, detail=f"Reset failed: {str(e)}")
72
 
73
 
74
  @app.post("/step")
75
  async def step(request: StepRequest) -> StepResponse:
 
 
 
 
76
  if not request.fixed_code or not request.fixed_code.strip():
77
  raise HTTPException(status_code=400, detail="fixed_code must not be empty.")
 
78
  try:
79
- action = DebugAction(fixed_code=request.fixed_code, explanation=request.explanation)
 
 
 
80
  observation = env.step(action)
81
  return StepResponse(
82
  observation=observation.model_dump(),
83
  reward=observation.reward or 0.0,
84
  done=observation.done,
85
  )
 
 
 
 
 
 
 
 
 
 
 
 
86
  except Exception as e:
87
  raise HTTPException(status_code=500, detail=f"Step failed: {str(e)}")
88
 
89
 
90
  @app.get("/state")
91
  async def state() -> dict:
 
92
  try:
93
- return env.state.model_dump()
 
94
  except Exception as e:
95
  raise HTTPException(status_code=500, detail=f"State failed: {str(e)}")
96
 
97
 
98
  @app.get("/tasks")
99
  async def list_tasks() -> dict:
 
100
  from server.tasks.task_easy import EASY_TASKS
101
  from server.tasks.task_medium import MEDIUM_TASKS
102
  from server.tasks.task_hard import HARD_TASKS
 
1
  # server/app.py
2
+ # FastAPI server exposing the OpenEnv standard endpoints.
3
+ # Port 7860 required for Hugging Face Spaces.
4
+
5
  from fastapi import FastAPI, HTTPException
6
  from fastapi.middleware.cors import CORSMiddleware
7
  from fastapi.responses import HTMLResponse
 
10
  import os
11
 
12
  from server.environment import CodeDebugEnvironment
13
+ from models import DebugAction, DebugObservation, DebugState
 
14
 
15
  app = FastAPI(
16
  title="Code Debug Environment",
 
28
  allow_headers=["*"],
29
  )
30
 
31
+ # One global environment instance (single session)
32
+ # For concurrent sessions, instantiate per-request with a session dict
33
  env = CodeDebugEnvironment()
34
 
35
 
36
+ # ─── Request Models ─────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
  class ResetRequest(BaseModel):
39
+ difficulty: Optional[str] = None # "easy" | "medium" | "hard" | None (random)
40
 
41
 
42
  class StepRequest(BaseModel):
 
44
  explanation: Optional[str] = None
45
 
46
 
47
+ # ─── Response wrapper matching OpenEnv StepResult shape ──────────────────────
48
+
49
  class StepResponse(BaseModel):
50
  observation: dict
51
  reward: float
52
  done: bool
53
 
54
 
55
+ # ─── Endpoints ───────────────────────────────────────────────────────────────
56
+
57
+ @app.get("/", response_class=HTMLResponse)
58
+ async def root():
59
+ """Homepage — shows environment info and API endpoints."""
60
+ html_path = os.path.join(os.path.dirname(__file__), "static", "index.html")
61
+ with open(html_path, "r") as f:
62
+ return f.read()
63
+
64
+
65
+ @app.get("/health")
66
+ async def health():
67
+ """Health check endpoint — must return 200 for submission validation."""
68
+ return {"status": "ok", "environment": "code-debug-env", "version": "1.0.0"}
69
+
70
+
71
  @app.post("/reset")
72
  async def reset(request: ResetRequest = ResetRequest()) -> dict:
73
+ """
74
+ Reset the environment to start a new episode.
75
+ Optionally pass difficulty: 'easy' | 'medium' | 'hard'
76
+ """
77
  try:
78
  observation = env.reset(difficulty=request.difficulty)
79
+ return {
80
+ "observation": observation.model_dump(),
81
+ "reward": 0.0,
82
+ "done": False,
83
+ }
84
  except Exception as e:
85
  raise HTTPException(status_code=500, detail=f"Reset failed: {str(e)}")
86
 
87
 
88
  @app.post("/step")
89
  async def step(request: StepRequest) -> StepResponse:
90
+ """
91
+ Submit a code fix (and optional explanation for hard tasks).
92
+ Returns observation with reward (0.0-1.0), feedback, and done flag.
93
+ """
94
  if not request.fixed_code or not request.fixed_code.strip():
95
  raise HTTPException(status_code=400, detail="fixed_code must not be empty.")
96
+
97
  try:
98
+ action = DebugAction(
99
+ fixed_code=request.fixed_code,
100
+ explanation=request.explanation,
101
+ )
102
  observation = env.step(action)
103
  return StepResponse(
104
  observation=observation.model_dump(),
105
  reward=observation.reward or 0.0,
106
  done=observation.done,
107
  )
108
+ except TimeoutError:
109
+ # Code execution timed out — return 0 reward instead of 500
110
+ return StepResponse(
111
+ observation={"task_id": "unknown", "difficulty": "unknown",
112
+ "buggy_code": "", "instructions": "",
113
+ "test_cases_description": "", "reward": 0.0,
114
+ "passed_tests": 0, "total_tests": 3,
115
+ "feedback": "TimeoutError: Code execution timed out. Possible infinite loop.",
116
+ "done": False},
117
+ reward=0.0,
118
+ done=False,
119
+ )
120
  except Exception as e:
121
  raise HTTPException(status_code=500, detail=f"Step failed: {str(e)}")
122
 
123
 
124
  @app.get("/state")
125
  async def state() -> dict:
126
+ """Return the current episode state."""
127
  try:
128
+ s = env.state
129
+ return s.model_dump()
130
  except Exception as e:
131
  raise HTTPException(status_code=500, detail=f"State failed: {str(e)}")
132
 
133
 
134
  @app.get("/tasks")
135
  async def list_tasks() -> dict:
136
+ """List available task IDs per difficulty (for inspection)."""
137
  from server.tasks.task_easy import EASY_TASKS
138
  from server.tasks.task_medium import MEDIUM_TASKS
139
  from server.tasks.task_hard import HARD_TASKS
server/graders/grader_easy.py CHANGED
@@ -3,13 +3,19 @@
3
  # Reward is proportional to tests passed (0.33, 0.66, 1.0).
4
 
5
  import traceback
 
6
  from typing import Tuple, List
7
 
8
 
 
 
 
 
9
  def _run_code_safely(code: str, func_name: str, test_input):
10
  """
11
  Executes the submitted code in an isolated namespace and calls the function.
12
  Returns (output, error_message).
 
13
  """
14
  namespace = {}
15
  try:
@@ -21,27 +27,42 @@ def _run_code_safely(code: str, func_name: str, test_input):
21
 
22
  func = namespace.get(func_name)
23
  if func is None:
24
- # Try to find any callable
25
  funcs = [v for v in namespace.values() if callable(v) and not v.__name__.startswith("_")]
26
  if not funcs:
27
  return None, "No callable function found in submitted code."
28
  func = funcs[0]
29
 
30
  try:
 
 
 
 
 
 
 
31
  if isinstance(test_input, list) and len(test_input) > 0 and isinstance(test_input[0], list):
32
- # List of lists = multiple arguments e.g. [[1,2,3], 2] → func([1,2,3], 2)
33
  result = func(*test_input)
34
  elif isinstance(test_input, list):
35
- # Try passing as single list argument first
36
  try:
37
  result = func(test_input)
38
  except TypeError:
39
- # Fallback: unpack as multiple args
40
  result = func(*test_input)
41
  else:
42
  result = func(test_input)
 
 
 
 
 
 
43
  return result, None
 
 
44
  except Exception as e:
 
 
 
 
45
  return None, f"RuntimeError: {traceback.format_exc(limit=2)}"
46
 
47
 
 
3
  # Reward is proportional to tests passed (0.33, 0.66, 1.0).
4
 
5
  import traceback
6
+ import signal
7
  from typing import Tuple, List
8
 
9
 
10
+ def _timeout_handler(signum, frame):
11
+ raise TimeoutError("Code execution timed out (infinite loop or slow code)")
12
+
13
+
14
  def _run_code_safely(code: str, func_name: str, test_input):
15
  """
16
  Executes the submitted code in an isolated namespace and calls the function.
17
  Returns (output, error_message).
18
+ Times out after 5 seconds to prevent infinite loops.
19
  """
20
  namespace = {}
21
  try:
 
27
 
28
  func = namespace.get(func_name)
29
  if func is None:
 
30
  funcs = [v for v in namespace.values() if callable(v) and not v.__name__.startswith("_")]
31
  if not funcs:
32
  return None, "No callable function found in submitted code."
33
  func = funcs[0]
34
 
35
  try:
36
+ # Set 5 second timeout to catch infinite loops
37
+ try:
38
+ signal.signal(signal.SIGALRM, _timeout_handler)
39
+ signal.alarm(5)
40
+ except (AttributeError, OSError):
41
+ pass # Windows doesn't support SIGALRM, skip timeout
42
+
43
  if isinstance(test_input, list) and len(test_input) > 0 and isinstance(test_input[0], list):
 
44
  result = func(*test_input)
45
  elif isinstance(test_input, list):
 
46
  try:
47
  result = func(test_input)
48
  except TypeError:
 
49
  result = func(*test_input)
50
  else:
51
  result = func(test_input)
52
+
53
+ try:
54
+ signal.alarm(0) # Cancel timeout
55
+ except (AttributeError, OSError):
56
+ pass
57
+
58
  return result, None
59
+ except TimeoutError as e:
60
+ return None, f"TimeoutError: {e}"
61
  except Exception as e:
62
+ try:
63
+ signal.alarm(0)
64
+ except (AttributeError, OSError):
65
+ pass
66
  return None, f"RuntimeError: {traceback.format_exc(limit=2)}"
67
 
68