PROTHAM commited on
Commit
3424779
·
1 Parent(s): 96617e2
__pycache__/client.cpython-312.pyc ADDED
Binary file (868 Bytes). View file
 
__pycache__/inference.cpython-312.pyc ADDED
Binary file (12.6 kB). View file
 
__pycache__/models.cpython-312.pyc ADDED
Binary file (2.73 kB). View file
 
api_debug_env.egg-info/PKG-INFO ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Metadata-Version: 2.4
2
+ Name: api-debug-env
3
+ Version: 1.0.0
4
+ Summary: OpenEnv environment for AI-powered HTTP API debugging
5
+ Requires-Python: >=3.10
6
+ License-File: LICENSE
7
+ Requires-Dist: openenv-core>=0.2.3
8
+ Requires-Dist: fastapi>=0.104.0
9
+ Requires-Dist: uvicorn>=0.24.0
10
+ Requires-Dist: pydantic>=2.0.0
11
+ Requires-Dist: httpx>=0.27.0
12
+ Requires-Dist: python-dotenv>=1.0.0
13
+ Requires-Dist: openai>=1.0.0
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest>=7.0; extra == "dev"
16
+ Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
17
+ Dynamic: license-file
api_debug_env.egg-info/SOURCES.txt ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ api_debug_env.egg-info/PKG-INFO
5
+ api_debug_env.egg-info/SOURCES.txt
6
+ api_debug_env.egg-info/dependency_links.txt
7
+ api_debug_env.egg-info/entry_points.txt
8
+ api_debug_env.egg-info/requires.txt
9
+ api_debug_env.egg-info/top_level.txt
10
+ graders/__init__.py
11
+ graders/grader.py
12
+ server/__init__.py
13
+ server/api_debug_environment.py
14
+ server/app.py
15
+ server/mock_api.py
16
+ tasks/__init__.py
17
+ tasks/easy.py
18
+ tasks/expert.py
19
+ tasks/hard.py
20
+ tasks/medium.py
21
+ tasks/registry.py
api_debug_env.egg-info/dependency_links.txt ADDED
@@ -0,0 +1 @@
 
 
1
+
api_debug_env.egg-info/entry_points.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ [console_scripts]
2
+ server = server.app:main
api_debug_env.egg-info/requires.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ openenv-core>=0.2.3
2
+ fastapi>=0.104.0
3
+ uvicorn>=0.24.0
4
+ pydantic>=2.0.0
5
+ httpx>=0.27.0
6
+ python-dotenv>=1.0.0
7
+ openai>=1.0.0
8
+
9
+ [dev]
10
+ pytest>=7.0
11
+ pytest-asyncio>=0.21
api_debug_env.egg-info/top_level.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ graders
2
+ outputs
3
+ server
4
+ tasks
graders/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (165 Bytes). View file
 
graders/__pycache__/grader.cpython-312.pyc ADDED
Binary file (3.55 kB). View file
 
graders/grader.py CHANGED
@@ -14,12 +14,13 @@ class APIGrader:
14
  # Correct status always scores at least 0.25; attempt bonus tapers reward slightly
15
  base_score = max(0.25, schema_score)
16
  attempt_bonus = max(0.0, 1.0 - (attempt / max_steps) * 0.15)
17
- return round(min(1.0, max(0.0, base_score * attempt_bonus)), 4)
 
18
 
19
  def _partial(self, status):
20
  # Partial credit for wrong-status responses — signals progress toward the solution
21
  mapping = {
22
- 0: 0.0, # connection error / no response
23
  401: 0.05, # reached auth wall
24
  403: 0.05, # reached permission wall
25
  404: 0.05, # wrong endpoint
@@ -33,8 +34,17 @@ class APIGrader:
33
  def _schema_match(self, body, schema):
34
  if not schema:
35
  return 1.0
36
- matched = sum(1 for k, v in schema.items() if k in body and type(body[k]) == type(v))
37
- return matched / len(schema)
 
 
 
 
 
 
 
 
 
38
 
39
  def get_feedback(self, status, expected_status):
40
  if status == expected_status:
 
14
  # Correct status always scores at least 0.25; attempt bonus tapers reward slightly
15
  base_score = max(0.25, schema_score)
16
  attempt_bonus = max(0.0, 1.0 - (attempt / max_steps) * 0.15)
17
+ raw_score = base_score * attempt_bonus
18
+ return round(min(0.999, max(0.001, raw_score)), 4)
19
 
20
  def _partial(self, status):
21
  # Partial credit for wrong-status responses — signals progress toward the solution
22
  mapping = {
23
+ 0: 0.001, # connection error / no response
24
  401: 0.05, # reached auth wall
25
  403: 0.05, # reached permission wall
26
  404: 0.05, # wrong endpoint
 
34
  def _schema_match(self, body, schema):
35
  if not schema:
36
  return 1.0
37
+
38
+ schemas = schema if isinstance(schema, list) else [schema]
39
+ best_match = 0.0
40
+
41
+ for s in schemas:
42
+ matched = sum(1 for k, v in s.items() if k in body and type(body[k]) == type(v))
43
+ score = matched / len(s) if len(s) > 0 else 1.0
44
+ if score > best_match:
45
+ best_match = score
46
+
47
+ return best_match
48
 
49
  def get_feedback(self, status, expected_status):
50
  if status == expected_status:
inference.py CHANGED
@@ -286,7 +286,7 @@ async def main() -> None:
286
  )
287
  # Do NOT return — we must still run tasks to emit required log lines
288
 
289
- for task_id in ["easy", "medium", "hard"]:
290
  await run_task(client, task_id)
291
 
292
 
 
286
  )
287
  # Do NOT return — we must still run tasks to emit required log lines
288
 
289
+ for task_id in ["easy", "medium", "hard", "expert", "auto"]:
290
  await run_task(client, task_id)
291
 
292
 
models.py CHANGED
@@ -31,9 +31,13 @@ class APIObservation(Observation):
31
  attempt: int = Field(default=0)
32
  reward: float = Field(default=0.0)
33
  done: bool = Field(default=False)
 
34
 
35
 
36
  class APIState(State):
37
  task_id: str = Field(default="easy")
38
  max_steps: int = Field(default=10)
39
  solved: bool = Field(default=False)
 
 
 
 
31
  attempt: int = Field(default=0)
32
  reward: float = Field(default=0.0)
33
  done: bool = Field(default=False)
34
+ current_step_index: int = Field(default=0)
35
 
36
 
37
  class APIState(State):
38
  task_id: str = Field(default="easy")
39
  max_steps: int = Field(default=10)
40
  solved: bool = Field(default=False)
41
+ current_step_index: int = Field(default=0)
42
+ visited_endpoints: set = Field(default_factory=set)
43
+ curriculum_level: int = Field(default=0)
server/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (164 Bytes). View file
 
server/__pycache__/api_debug_environment.cpython-312.pyc ADDED
Binary file (7.85 kB). View file
 
server/__pycache__/app.cpython-312.pyc ADDED
Binary file (1.14 kB). View file
 
server/__pycache__/mock_api.cpython-312.pyc ADDED
Binary file (9.37 kB). View file
 
server/api_debug_environment.py CHANGED
@@ -17,20 +17,59 @@ MOCK_BASE = "http://localhost:7860"
17
  class APIDebugEnvironment(Environment):
18
 
19
  def __init__(self):
20
- self._state = APIState(episode_id="init", step_count=0, task_id="easy", max_steps=5, solved=False)
21
  self._current_task = None
22
 
23
- def reset(self, task_id: str = "easy", **kwargs) -> APIObservation:
24
- if task_id not in TASK_REGISTRY:
25
- task_id = "easy"
26
- self._current_task = random.choice(TASK_REGISTRY[task_id])
27
- max_steps = self._current_task.get("max_steps", 5)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  self._state = APIState(
29
  episode_id=f"ep_{random.randint(10000, 99999)}",
30
  step_count=0,
31
  task_id=task_id,
32
  max_steps=max_steps,
33
  solved=False,
 
 
 
34
  )
35
  return APIObservation(
36
  task_id=task_id,
@@ -40,15 +79,16 @@ class APIDebugEnvironment(Environment):
40
  last_response_headers={},
41
  last_response_body="",
42
  step_feedback="Episode started. Read the task description and fix the broken request.",
43
- current_score=0.0,
44
  attempt=0,
45
- reward=0.0,
46
  done=False,
 
47
  )
48
 
49
  def step(self, action: APIAction, **kwargs) -> APIObservation:
50
  if self._current_task is None:
51
- self.reset(task_id="easy")
52
 
53
  self._state.step_count += 1
54
  task = self._current_task
@@ -70,19 +110,42 @@ class APIDebugEnvironment(Environment):
70
  resp_headers = {}
71
  resp_body = f"Request error: {str(e)}"
72
 
 
 
 
 
 
 
 
73
  reward = GRADER.grade(
74
  response_status=status,
75
  response_body=resp_body,
76
- expected_status=task["expected_status"],
77
- expected_schema=task["expected_schema"],
78
  attempt=self._state.step_count,
79
  max_steps=self._state.max_steps,
80
  )
81
 
82
- feedback = GRADER.get_feedback(status, task["expected_status"])
83
- solved = status == task["expected_status"] and reward >= 0.8
84
- self._state.solved = solved
85
- done = solved or self._state.step_count >= self._state.max_steps
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
  return APIObservation(
88
  task_id=self._state.task_id,
@@ -96,6 +159,7 @@ class APIDebugEnvironment(Environment):
96
  attempt=self._state.step_count,
97
  reward=reward,
98
  done=done,
 
99
  )
100
 
101
  @property
 
17
  class APIDebugEnvironment(Environment):
18
 
19
  def __init__(self):
20
+ self._state = APIState(episode_id="init", step_count=0, task_id="easy", max_steps=10, solved=False, current_step_index=0, curriculum_level=0)
21
  self._current_task = None
22
 
23
+ def reset(self, task_id: str = "auto", **kwargs) -> APIObservation:
24
+ import copy
25
+ import uuid
26
+
27
+ old_level = getattr(self._state, "curriculum_level", 0)
28
+ if getattr(self._state, "solved", False):
29
+ old_level += 1
30
+
31
+ effective_task_id = task_id
32
+ if task_id == "auto":
33
+ levels = ["easy", "medium", "hard", "expert"]
34
+ effective_task_id = levels[min(old_level, len(levels) - 1)]
35
+
36
+ if effective_task_id not in TASK_REGISTRY:
37
+ effective_task_id = "easy"
38
+
39
+ task = copy.deepcopy(random.choice(TASK_REGISTRY[effective_task_id]))
40
+
41
+ dyn_config = {
42
+ "demo_token": f"tok_{uuid.uuid4().hex[:8]}",
43
+ "client_id": f"cli_{uuid.uuid4().hex[:8]}",
44
+ "client_secret": f"sec_{uuid.uuid4().hex[:8]}"
45
+ }
46
+
47
+ try:
48
+ with httpx.Client(base_url=MOCK_BASE, timeout=2.0) as http:
49
+ http.post("/mock_api/_admin/reset", json=dyn_config)
50
+ except Exception:
51
+ pass
52
+
53
+ try:
54
+ task["description"] = task["description"].format(**dyn_config)
55
+ req = task["broken_request"]
56
+ if "headers" in req:
57
+ req["headers"] = {k: (v.format(**dyn_config) if isinstance(v, str) else v) for k, v in req["headers"].items()}
58
+ except Exception:
59
+ pass
60
+
61
+ self._current_task = task
62
+ max_steps = self._current_task.get("max_steps", 10)
63
+
64
  self._state = APIState(
65
  episode_id=f"ep_{random.randint(10000, 99999)}",
66
  step_count=0,
67
  task_id=task_id,
68
  max_steps=max_steps,
69
  solved=False,
70
+ current_step_index=0,
71
+ visited_endpoints=set(),
72
+ curriculum_level=old_level
73
  )
74
  return APIObservation(
75
  task_id=task_id,
 
79
  last_response_headers={},
80
  last_response_body="",
81
  step_feedback="Episode started. Read the task description and fix the broken request.",
82
+ current_score=0.001,
83
  attempt=0,
84
+ reward=0.001,
85
  done=False,
86
+ current_step_index=0
87
  )
88
 
89
  def step(self, action: APIAction, **kwargs) -> APIObservation:
90
  if self._current_task is None:
91
+ self.reset(task_id="auto")
92
 
93
  self._state.step_count += 1
94
  task = self._current_task
 
110
  resp_headers = {}
111
  resp_body = f"Request error: {str(e)}"
112
 
113
+ expected_schema = task["expected_schema"]
114
+ expected_status = task["expected_status"]
115
+ is_chain = isinstance(expected_schema, list)
116
+
117
+ target_schema = expected_schema[self._state.current_step_index] if is_chain else expected_schema
118
+ target_status = expected_status[self._state.current_step_index] if isinstance(expected_status, list) else expected_status
119
+
120
  reward = GRADER.grade(
121
  response_status=status,
122
  response_body=resp_body,
123
+ expected_status=target_status,
124
+ expected_schema=target_schema,
125
  attempt=self._state.step_count,
126
  max_steps=self._state.max_steps,
127
  )
128
 
129
+ feedback = GRADER.get_feedback(status, target_status)
130
+ step_solved = status == target_status and reward >= 0.8
131
+
132
+ if is_chain and step_solved:
133
+ self._state.current_step_index += 1
134
+ if self._state.current_step_index >= len(expected_schema):
135
+ self._state.solved = True
136
+ feedback += " Chain completed successfully!"
137
+ else:
138
+ feedback += f" Step {self._state.current_step_index} complete. Moving to next step."
139
+ reward = 0.5 # Partial reward for finishing a step
140
+ else:
141
+ self._state.solved = step_solved if not is_chain else False
142
+
143
+ if action.url not in self._state.visited_endpoints and status > 0 and status < 500:
144
+ self._state.visited_endpoints.add(action.url)
145
+ reward += 0.05
146
+ reward = round(min(0.999, max(0.001, reward)), 4)
147
+
148
+ done = self._state.solved or self._state.step_count >= self._state.max_steps
149
 
150
  return APIObservation(
151
  task_id=self._state.task_id,
 
159
  attempt=self._state.step_count,
160
  reward=reward,
161
  done=done,
162
+ current_step_index=self._state.current_step_index
163
  )
164
 
165
  @property
server/mock_api.py CHANGED
@@ -10,6 +10,13 @@ _issued_tokens = set()
10
  _request_log = {}
11
  _items_db = {}
12
 
 
 
 
 
 
 
 
13
  LOG_PAGES = {
14
  None: {"items": list(range(10)), "next_cursor": "cur_abc", "has_more": True},
15
  "cur_abc": {"items": list(range(10, 20)), "next_cursor": "cur_def", "has_more": True},
@@ -17,10 +24,12 @@ LOG_PAGES = {
17
  }
18
 
19
  @router.post("/_admin/reset")
20
- async def admin_reset():
21
  _issued_tokens.clear()
22
  _request_log.clear()
23
  _items_db.clear()
 
 
24
  return {"status": "reset_successful"}
25
 
26
 
@@ -30,7 +39,7 @@ class TokenRequest(BaseModel):
30
 
31
  @router.post("/auth/token")
32
  async def get_token(body: TokenRequest):
33
- if body.client_id == "abc" and body.client_secret == "xyz":
34
  token = f"tok_{int(time.time())}"
35
  _issued_tokens.add(token)
36
  return {"access_token": token, "expires_in": 60}
@@ -39,11 +48,12 @@ async def get_token(body: TokenRequest):
39
 
40
  @router.get("/users")
41
  async def get_users(authorization: Optional[str] = Header(default=None)):
 
42
  if not authorization or not authorization.startswith("Bearer "):
43
- raise HTTPException(status_code=401, detail="Authorization header missing or malformed. Expected format: Bearer <token>. Use 'demo_token_123' for testing.")
44
  token = authorization.split(" ")[1]
45
- if token not in _issued_tokens and token != "demo_token_123":
46
- raise HTTPException(status_code=401, detail="Invalid token. Try 'demo_token_123'.")
47
  return {"users": [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]}
48
 
49
 
@@ -53,11 +63,12 @@ async def create_item(request: Request):
53
  if "application/json" not in ct:
54
  raise HTTPException(status_code=415, detail="Content-Type must be application/json")
55
  body = await request.json()
56
- if "name" not in body:
57
- raise HTTPException(status_code=422, detail=[{"loc": ["body", "name"], "msg": "field required"}])
 
58
  item_id = str(uuid.uuid4())
59
- _items_db[item_id] = body["name"]
60
- return {"item_id": item_id, "name": body["name"], "created": True}
61
 
62
  @router.delete("/items/{item_id}")
63
  async def delete_item(item_id: str):
 
10
  _request_log = {}
11
  _items_db = {}
12
 
13
+ DYNAMIC_CONFIG = {
14
+ "demo_token": "demo_token_123",
15
+ "client_id": "abc",
16
+ "client_secret": "xyz",
17
+ "dynamic_item_field": "name"
18
+ }
19
+
20
  LOG_PAGES = {
21
  None: {"items": list(range(10)), "next_cursor": "cur_abc", "has_more": True},
22
  "cur_abc": {"items": list(range(10, 20)), "next_cursor": "cur_def", "has_more": True},
 
24
  }
25
 
26
  @router.post("/_admin/reset")
27
+ async def admin_reset(config: Optional[dict] = None):
28
  _issued_tokens.clear()
29
  _request_log.clear()
30
  _items_db.clear()
31
+ if config:
32
+ DYNAMIC_CONFIG.update(config)
33
  return {"status": "reset_successful"}
34
 
35
 
 
39
 
40
  @router.post("/auth/token")
41
  async def get_token(body: TokenRequest):
42
+ if body.client_id == DYNAMIC_CONFIG["client_id"] and body.client_secret == DYNAMIC_CONFIG["client_secret"]:
43
  token = f"tok_{int(time.time())}"
44
  _issued_tokens.add(token)
45
  return {"access_token": token, "expires_in": 60}
 
48
 
49
  @router.get("/users")
50
  async def get_users(authorization: Optional[str] = Header(default=None)):
51
+ dt = DYNAMIC_CONFIG["demo_token"]
52
  if not authorization or not authorization.startswith("Bearer "):
53
+ raise HTTPException(status_code=401, detail=f"Authorization header missing or malformed. Expected format: Bearer <token>. Use '{dt}' for testing.")
54
  token = authorization.split(" ")[1]
55
+ if token not in _issued_tokens and token != dt:
56
+ raise HTTPException(status_code=401, detail=f"Invalid token. Try '{dt}'.")
57
  return {"users": [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]}
58
 
59
 
 
63
  if "application/json" not in ct:
64
  raise HTTPException(status_code=415, detail="Content-Type must be application/json")
65
  body = await request.json()
66
+ field = DYNAMIC_CONFIG.get("dynamic_item_field", "name")
67
+ if field not in body:
68
+ raise HTTPException(status_code=422, detail=[{"loc": ["body", field], "msg": "field required"}])
69
  item_id = str(uuid.uuid4())
70
+ _items_db[item_id] = body[field]
71
+ return {"item_id": item_id, "name": body[field], "created": True}
72
 
73
  @router.delete("/items/{item_id}")
74
  async def delete_item(item_id: str):
tasks/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (163 Bytes). View file
 
tasks/__pycache__/easy.cpython-312.pyc ADDED
Binary file (1.36 kB). View file
 
tasks/__pycache__/expert.cpython-312.pyc ADDED
Binary file (1.24 kB). View file
 
tasks/__pycache__/hard.cpython-312.pyc ADDED
Binary file (1.62 kB). View file
 
tasks/__pycache__/medium.cpython-312.pyc ADDED
Binary file (1.26 kB). View file
 
tasks/__pycache__/registry.cpython-312.pyc ADDED
Binary file (437 Bytes). View file
 
tasks/easy.py CHANGED
@@ -1,7 +1,7 @@
1
  EASY_TASKS = [
2
  {
3
  "id": "easy_auth",
4
- "description": "Access the protected resource at GET /mock_api/users to get a 200 OK. The current request is failing because the Authorization header is missing. Add 'Authorization: Bearer demo_token_123' to the headers.",
5
  "broken_request": {
6
  "method": "GET",
7
  "url": "/mock_api/users",
 
1
  EASY_TASKS = [
2
  {
3
  "id": "easy_auth",
4
+ "description": "Access the protected resource at GET /mock_api/users to get a 200 OK. The current request is failing because the Authorization header is missing. Add 'Authorization: Bearer {demo_token}' to the headers.",
5
  "broken_request": {
6
  "method": "GET",
7
  "url": "/mock_api/users",
tasks/expert.py CHANGED
@@ -15,16 +15,19 @@ EXPERT_TASKS = [
15
  },
16
  {
17
  "id": "expert_stateful_chain",
18
- "description": "First, create an item with name 'temporary_item' via POST /mock_api/items to get an auto-generated item_id. Then, safely clean it up by sending a DELETE request to /mock_api/items/{item_id}. The current request is a placeholder.",
19
  "broken_request": {
20
  "method": "POST",
21
  "url": "/mock_api/items",
22
  "headers": {"Content-Type": "application/json"},
23
- "body": {"name": "temporary_item"},
24
  "query_params": {},
25
  },
26
- "expected_status": 200,
27
- "expected_schema": {"status": "", "item_id": ""},
 
 
 
28
  "max_steps": 10,
29
  },
30
  ]
 
15
  },
16
  {
17
  "id": "expert_stateful_chain",
18
+ "description": "First, create an item with field '{dynamic_item_field}' set to 'temporary_item' via POST /mock_api/items. Then, safely clean it up by sending a DELETE request to /mock_api/items/{{item_id}}. The current request is a placeholder.",
19
  "broken_request": {
20
  "method": "POST",
21
  "url": "/mock_api/items",
22
  "headers": {"Content-Type": "application/json"},
23
+ "body": {"{dynamic_item_field}": "temporary_item"},
24
  "query_params": {},
25
  },
26
+ "expected_status": [200, 200],
27
+ "expected_schema": [
28
+ {"item_id": "", "{dynamic_item_field}": "", "created": True},
29
+ {"status": "", "item_id": ""}
30
+ ],
31
  "max_steps": 10,
32
  },
33
  ]
tasks/hard.py CHANGED
@@ -3,7 +3,7 @@ HARD_TASKS = [
3
  "id": "hard_token_exchange",
4
  "description": (
5
  "The protected endpoint needs a fresh token. "
6
- "Step 1: POST to /mock_api/auth/token with JSON body {\"client_id\": \"abc\", \"client_secret\": \"xyz\"} "
7
  "to get an access_token from the response. "
8
  "Step 2: Use that token as Authorization: Bearer <token> to GET /mock_api/protected. "
9
  "The token in the broken request is expired and will return 401."
 
3
  "id": "hard_token_exchange",
4
  "description": (
5
  "The protected endpoint needs a fresh token. "
6
+ "Step 1: POST to /mock_api/auth/token with JSON body {{\"client_id\": \"{client_id}\", \"client_secret\": \"{client_secret}\"}} "
7
  "to get an access_token from the response. "
8
  "Step 2: Use that token as Authorization: Bearer <token> to GET /mock_api/protected. "
9
  "The token in the broken request is expired and will return 401."