PROTHAM commited on
Commit
96617e2
Β·
1 Parent(s): 9ec21a6

fix: change body to empty dict to prevent Pydantic 422 Unprocessable Entity errors during validation

Browse files
Files changed (1) hide show
  1. inference.py +191 -71
inference.py CHANGED
@@ -1,3 +1,12 @@
 
 
 
 
 
 
 
 
 
1
  import os
2
  import json
3
  import asyncio
@@ -5,17 +14,29 @@ import httpx
5
  from typing import List, Optional
6
  from openai import OpenAI
7
 
 
8
  API_BASE_URL = os.getenv("API_BASE_URL") or "https://router.huggingface.co/v1"
9
- MODEL_NAME = os.getenv("MODEL_NAME") or "mistralai/Mistral-7B-Instruct-v0.3"
10
- API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY") or "dummy"
11
- ENV_URL = os.getenv("ENV_URL", "http://localhost:7860").rstrip("/")
 
12
  BENCHMARK = "api_debug_env"
13
- MAX_STEPS = 5
 
 
 
 
 
 
 
 
14
  SUCCESS_SCORE_THRESHOLD = 0.8
15
 
 
16
  llm = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
17
 
18
- SYSTEM_PROMPT = """You are an HTTP API debugger. You receive a broken request and must fix it to get HTTP 200.
 
19
  Respond ONLY in valid JSON with exactly these fields:
20
  {
21
  "method": "GET",
@@ -25,146 +46,245 @@ Respond ONLY in valid JSON with exactly these fields:
25
  "query_params": {}
26
  }
27
  Rules:
28
- - method must be uppercase
29
  - url must start with /mock_api/
30
- - body must be null for GET requests
31
  - always include Content-Type: application/json when body is not null
32
- - do not add any text outside the JSON"""
 
33
 
34
 
 
35
  def log_start(task: str, env: str, model: str) -> None:
36
  print(f"[START] task={task} env={env} model={model}", flush=True)
37
 
38
 
39
- def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:
 
40
  error_val = error if error else "null"
41
- done_val = str(done).lower()
42
  print(
43
- f"[STEP] step={step} action={action} reward={reward:.2f} done={done_val} error={error_val}",
 
44
  flush=True,
45
  )
46
 
47
 
48
- def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:
 
49
  rewards_str = ",".join(f"{r:.2f}" for r in rewards)
50
- print(f"[END] success={str(success).lower()} steps={steps} score={score:.3f} rewards={rewards_str}", flush=True)
 
 
 
 
51
 
52
 
53
- def call_llm(task_description, broken_request, last_status, last_body, feedback):
 
 
 
 
 
54
  user_msg = (
55
- f"Task: {task_description}\n\n"
56
- f"Broken request:\n{json.dumps(broken_request, indent=2)}\n\n"
57
  f"Last response status: {last_status}\n"
58
- f"Last response body: {str(last_body)[:300]}\n\n"
59
- f"Feedback: {feedback}\n\n"
60
- f"Return the fixed request as JSON:"
 
61
  )
62
  try:
63
  resp = llm.chat.completions.create(
64
  model=MODEL_NAME,
65
  messages=[
66
  {"role": "system", "content": SYSTEM_PROMPT},
67
- {"role": "user", "content": user_msg},
68
  ],
69
- max_tokens=300,
70
- temperature=0.1,
71
  )
72
- raw = resp.choices[0].message.content.strip()
73
- raw = raw.replace("```json", "").replace("```", "").strip()
 
 
 
 
 
74
  return json.loads(raw)
 
 
 
 
 
 
 
 
75
  except Exception:
76
- return broken_request
 
 
 
77
 
78
 
 
79
  async def env_reset(client: httpx.AsyncClient, task_id: str) -> dict:
80
- resp = await client.post("/reset", json={"task_id": task_id}, timeout=30)
81
- resp.raise_for_status()
82
- return resp.json()
 
 
 
 
 
 
 
 
 
 
 
83
 
84
 
85
  async def env_step(client: httpx.AsyncClient, action: dict) -> dict:
86
- resp = await client.post("/step", json={"action": action}, timeout=30)
87
- resp.raise_for_status()
88
- return resp.json()
89
-
90
-
91
- async def env_state(client: httpx.AsyncClient) -> dict:
92
- resp = await client.get("/state", timeout=10)
93
- resp.raise_for_status()
94
- return resp.json()
 
 
 
 
 
95
 
96
 
 
97
  async def run_task(client: httpx.AsyncClient, task_id: str) -> float:
98
- rewards: List[float] = []
99
- steps_taken = 0
100
- score = 0.0
101
- success = False
 
 
 
 
102
 
103
  log_start(task=task_id, env=BENCHMARK, model=MODEL_NAME)
104
 
105
  try:
106
- obs = await env_reset(client, task_id)
107
- state = await env_state(client)
108
- max_steps = state.get("max_steps", MAX_STEPS)
109
- done = obs.get("done", False)
110
 
 
111
  for step in range(1, max_steps + 1):
112
  if done:
113
  break
114
 
 
 
 
 
 
 
 
 
115
  fixed = call_llm(
116
- obs.get("task_description", ""),
117
- obs.get("broken_request", {}),
118
- obs.get("last_status_code", 0),
119
- obs.get("last_response_body", ""),
120
- obs.get("step_feedback", ""),
121
  )
122
 
 
 
 
 
 
 
 
 
 
123
  action = {
124
- "method": str(fixed.get("method", "GET")),
125
- "url": str(fixed.get("url", "/mock_api/users")),
126
- "headers": dict(fixed.get("headers") or {}),
127
- "body": fixed.get("body") or None,
128
  "query_params": dict(fixed.get("query_params") or {}),
129
  }
130
 
131
- action_str = f"{action['method']}:{action['url']}"
132
- result = await env_step(client, action)
133
 
134
- reward = float(result.get("reward", 0.0))
135
- done = bool(result.get("done", False))
136
- obs = result.get("observation", result)
 
 
 
 
 
 
 
 
137
 
138
  rewards.append(reward)
139
  steps_taken = step
140
 
141
- log_step(step=step, action=action_str, reward=reward, done=done, error=None)
 
 
 
 
 
 
 
142
 
143
  if done:
144
  break
145
 
146
- score = max(rewards) if rewards else 0.0
 
 
 
147
  success = score >= SUCCESS_SCORE_THRESHOLD
148
 
149
- except Exception as e:
150
- print(f"[DEBUG] Task {task_id} error: {e}", flush=True)
151
- log_step(step=steps_taken, action="error", reward=0.0, done=True, error=str(e))
 
 
 
 
 
 
 
152
 
153
  finally:
154
- log_end(success=success, steps=steps_taken, score=score, rewards=rewards)
 
155
 
156
  return score
157
 
158
 
159
- async def main():
 
160
  async with httpx.AsyncClient(base_url=ENV_URL, timeout=30.0) as client:
161
- # Verify environment is reachable
 
162
  try:
163
- health = await client.get("/health")
164
  health.raise_for_status()
165
- except Exception as e:
166
- print(f"[DEBUG] Environment not reachable at {ENV_URL}: {e}", flush=True)
167
- raise
 
 
 
 
168
 
169
  for task_id in ["easy", "medium", "hard"]:
170
  await run_task(client, task_id)
 
1
+ """
2
+ Inference Script for API Debug Environment
3
+ ===========================================
4
+ Mandatory stdout format per Phase 2 validator:
5
+ [START] task=<task_name> env=<benchmark> model=<model_name>
6
+ [STEP] step=<n> action=<action_str> reward=<0.00> done=<true|false> error=<msg|null>
7
+ [END] success=<true|false> steps=<n> score=<score> rewards=<r1,r2,...,rn>
8
+ """
9
+
10
  import os
11
  import json
12
  import asyncio
 
14
  from typing import List, Optional
15
  from openai import OpenAI
16
 
17
+ # ── Environment variables ──────────────────────────────────────────────────────
18
  API_BASE_URL = os.getenv("API_BASE_URL") or "https://router.huggingface.co/v1"
19
+ MODEL_NAME = os.getenv("MODEL_NAME") or "mistralai/Mistral-7B-Instruct-v0.3"
20
+ API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY") or "dummy"
21
+ ENV_URL = os.getenv("ENV_URL", "http://localhost:7860").rstrip("/")
22
+
23
  BENCHMARK = "api_debug_env"
24
+
25
+ # Max steps per difficulty (mirrors openenv.yaml config so we don't rely on /state)
26
+ TASK_MAX_STEPS = {
27
+ "easy": 5,
28
+ "medium": 5,
29
+ "hard": 10,
30
+ "expert": 10,
31
+ }
32
+ DEFAULT_MAX_STEPS = 5
33
  SUCCESS_SCORE_THRESHOLD = 0.8
34
 
35
+ # ── LLM client ─────────────────────────────────────────────────────────────────
36
  llm = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
37
 
38
+ SYSTEM_PROMPT = """\
39
+ You are an HTTP API debugger. You receive a broken HTTP request and must fix it to get HTTP 200.
40
  Respond ONLY in valid JSON with exactly these fields:
41
  {
42
  "method": "GET",
 
46
  "query_params": {}
47
  }
48
  Rules:
49
+ - method must be uppercase (GET, POST, DELETE, etc.)
50
  - url must start with /mock_api/
51
+ - body must be null for GET/DELETE requests
52
  - always include Content-Type: application/json when body is not null
53
+ - do not add any text outside the JSON
54
+ - read the task description carefully and produce the correct fix on the first attempt"""
55
 
56
 
57
+ # ── Logging helpers ────────────────────────────────────────────────────────────
58
  def log_start(task: str, env: str, model: str) -> None:
59
  print(f"[START] task={task} env={env} model={model}", flush=True)
60
 
61
 
62
+ def log_step(step: int, action: str, reward: float, done: bool,
63
+ error: Optional[str]) -> None:
64
  error_val = error if error else "null"
65
+ done_val = str(done).lower()
66
  print(
67
+ f"[STEP] step={step} action={action} reward={reward:.2f}"
68
+ f" done={done_val} error={error_val}",
69
  flush=True,
70
  )
71
 
72
 
73
+ def log_end(success: bool, steps: int, score: float,
74
+ rewards: List[float]) -> None:
75
  rewards_str = ",".join(f"{r:.2f}" for r in rewards)
76
+ print(
77
+ f"[END] success={str(success).lower()} steps={steps}"
78
+ f" score={score:.3f} rewards={rewards_str}",
79
+ flush=True,
80
+ )
81
 
82
 
83
+ # ── LLM call ──────────────────────────────────────────────────────────────────
84
+ def call_llm(task_description: str, broken_request: dict,
85
+ last_status: int, last_body: str, feedback: str,
86
+ history: List[str]) -> dict:
87
+ """Ask the LLM to fix the broken request. Returns a request dict."""
88
+ history_str = "\n".join(history[-3:]) if history else "None"
89
  user_msg = (
90
+ f"Task description:\n{task_description}\n\n"
91
+ f"Broken request (JSON):\n{json.dumps(broken_request, indent=2)}\n\n"
92
  f"Last response status: {last_status}\n"
93
+ f"Last response body: {str(last_body)[:400]}\n\n"
94
+ f"Server feedback: {feedback}\n\n"
95
+ f"Previous attempts (latest first):\n{history_str}\n\n"
96
+ f"Return the corrected request as JSON:"
97
  )
98
  try:
99
  resp = llm.chat.completions.create(
100
  model=MODEL_NAME,
101
  messages=[
102
  {"role": "system", "content": SYSTEM_PROMPT},
103
+ {"role": "user", "content": user_msg},
104
  ],
105
+ max_tokens=350,
106
+ temperature=0.0, # deterministic for API debugging
107
  )
108
+ raw = (resp.choices[0].message.content or "").strip()
109
+ # Strip markdown code fences if present
110
+ if raw.startswith("```"):
111
+ raw = raw.split("```")[1]
112
+ if raw.startswith("json"):
113
+ raw = raw[4:]
114
+ raw = raw.strip()
115
  return json.loads(raw)
116
+ except json.JSONDecodeError:
117
+ # LLM returned non-JSON β€” try to extract the first {...} block
118
+ try:
119
+ start = raw.index("{")
120
+ end = raw.rindex("}") + 1
121
+ return json.loads(raw[start:end])
122
+ except Exception:
123
+ pass
124
  except Exception:
125
+ pass
126
+ # Last-resort fallback: return broken_request unchanged so the loop
127
+ # can still emit a [STEP] line with error context
128
+ return broken_request
129
 
130
 
131
+ # ── Environment HTTP helpers ───────────────────────────────────────────────────
132
  async def env_reset(client: httpx.AsyncClient, task_id: str) -> dict:
133
+ """POST /reset and return the parsed JSON body."""
134
+ try:
135
+ resp = await client.post(
136
+ "/reset",
137
+ json={"task_id": task_id},
138
+ timeout=30,
139
+ )
140
+ resp.raise_for_status()
141
+ return resp.json()
142
+ except httpx.HTTPStatusError as exc:
143
+ raise RuntimeError(f"env_reset HTTP error {exc.response.status_code}: "
144
+ f"{exc.response.text[:200]}") from exc
145
+ except Exception as exc:
146
+ raise RuntimeError(f"env_reset failed: {exc}") from exc
147
 
148
 
149
  async def env_step(client: httpx.AsyncClient, action: dict) -> dict:
150
+ """POST /step and return the parsed JSON body."""
151
+ try:
152
+ resp = await client.post(
153
+ "/step",
154
+ json={"action": action},
155
+ timeout=30,
156
+ )
157
+ resp.raise_for_status()
158
+ return resp.json()
159
+ except httpx.HTTPStatusError as exc:
160
+ raise RuntimeError(f"env_step HTTP error {exc.response.status_code}: "
161
+ f"{exc.response.text[:200]}") from exc
162
+ except Exception as exc:
163
+ raise RuntimeError(f"env_step failed: {exc}") from exc
164
 
165
 
166
+ # ── Per-task episode runner ────────────────────────────────────────────────────
167
  async def run_task(client: httpx.AsyncClient, task_id: str) -> float:
168
+ rewards: List[float] = []
169
+ steps_taken: int = 0
170
+ score: float = 0.001 # default: strictly > 0 for Phase 2
171
+ success: bool = False
172
+ history: List[str] = []
173
+
174
+ # Per-task max steps (matches openenv.yaml; /state doesn't expose these)
175
+ max_steps = TASK_MAX_STEPS.get(task_id, DEFAULT_MAX_STEPS)
176
 
177
  log_start(task=task_id, env=BENCHMARK, model=MODEL_NAME)
178
 
179
  try:
180
+ # ── Reset ────────────────────────────────────────────────────────────
181
+ reset_resp = await env_reset(client, task_id)
182
+ obs = reset_resp.get("observation", reset_resp)
183
+ done = bool(reset_resp.get("done", False))
184
 
185
+ # ── Step loop ─────────────────────────────────────────────────────────
186
  for step in range(1, max_steps + 1):
187
  if done:
188
  break
189
 
190
+ # Build LLM prompt context
191
+ task_desc = obs.get("task_description", "")
192
+ broken_req = obs.get("broken_request", {})
193
+ last_status = obs.get("last_status_code", 0)
194
+ last_body = obs.get("last_response_body", "")
195
+ feedback = obs.get("step_feedback", "")
196
+
197
+ # Ask LLM to fix the request
198
  fixed = call_llm(
199
+ task_desc, broken_req, last_status, last_body,
200
+ feedback, history,
 
 
 
201
  )
202
 
203
+ # Sanitise the LLM output into a clean action dict
204
+ method = str(fixed.get("method", "GET")).upper()
205
+ url = str(fixed.get("url", "/mock_api/users"))
206
+
207
+ # Body must be None for GET/DELETE; otherwise keep what LLM says
208
+ body = fixed.get("body")
209
+ if method in ("GET", "DELETE", "HEAD"):
210
+ body = None
211
+
212
  action = {
213
+ "method": method,
214
+ "url": url,
215
+ "headers": dict(fixed.get("headers") or {}),
216
+ "body": body if body is not None else {},
217
  "query_params": dict(fixed.get("query_params") or {}),
218
  }
219
 
220
+ action_str = f"{method}:{url}"
 
221
 
222
+ # ── Step ──────────────────────────────────────────────────────────
223
+ try:
224
+ result = await env_step(client, action)
225
+ reward = float(result.get("reward", 0.0))
226
+ done = bool(result.get("done", False))
227
+ obs = result.get("observation", result)
228
+ error_str: Optional[str] = None
229
+ except RuntimeError as exc:
230
+ reward = 0.0
231
+ done = True # abort episode on env error
232
+ error_str = str(exc)[:120]
233
 
234
  rewards.append(reward)
235
  steps_taken = step
236
 
237
+ log_step(step=step, action=action_str, reward=reward,
238
+ done=done, error=error_str)
239
+
240
+ history.append(
241
+ f"step={step} action={action_str} "
242
+ f"status={obs.get('last_status_code', '?')} "
243
+ f"reward={reward:.2f}"
244
+ )
245
 
246
  if done:
247
  break
248
 
249
+ # Score: best single-step reward achieved during the episode.
250
+ # Phase 2 requires score strictly in (0, 1) β€” clamp away from boundaries.
251
+ _raw = max(rewards) if rewards else 0.0
252
+ score = max(0.001, min(0.999, _raw))
253
  success = score >= SUCCESS_SCORE_THRESHOLD
254
 
255
+ except Exception as exc:
256
+ # Unexpected outer exception β€” still must emit [END]
257
+ print(f"[DEBUG] run_task({task_id}) outer exception: {exc}", flush=True)
258
+ if steps_taken == 0:
259
+ # We haven't emitted a single [STEP]; emit a dummy one so the
260
+ # Phase 2 validator can parse a proper log
261
+ log_step(step=1, action="error", reward=0.001, done=True,
262
+ error=str(exc)[:120])
263
+ steps_taken = 1
264
+ rewards = [0.001]
265
 
266
  finally:
267
+ log_end(success=success, steps=steps_taken,
268
+ score=score, rewards=rewards)
269
 
270
  return score
271
 
272
 
273
+ # ── Entry point ───────────────────────────────────────────────────────────────
274
+ async def main() -> None:
275
  async with httpx.AsyncClient(base_url=ENV_URL, timeout=30.0) as client:
276
+ # Verify env is reachable β€” log warning but ALWAYS proceed so that
277
+ # mandatory [START]/[END] lines are emitted for every task.
278
  try:
279
+ health = await client.get("/health", timeout=10)
280
  health.raise_for_status()
281
+ print(f"[DEBUG] Environment reachable at {ENV_URL}", flush=True)
282
+ except Exception as exc:
283
+ print(
284
+ f"[DEBUG] Environment not reachable at {ENV_URL}: {exc}",
285
+ flush=True,
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)