Nitesh321 Claude Sonnet 4.6 commited on
Commit
d8571a4
Β·
1 Parent(s): 648f93a

fix: rewrite inference.py to match official sample key=value log format

Browse files

- Use HF_TOKEN/API_KEY instead of OPENAI_API_KEY
- Default API base URL to HuggingFace router
- Default model to Qwen/Qwen2.5-72B-Instruct
- Log format: [START] task= env= model= (not JSON blobs)
- [STEP] step= action= reward= done= error=
- [END] success= steps= score= rewards=
- [END] emitted in finally block (always runs)
- Retry logic with exponential backoff

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (1) hide show
  1. inference.py +140 -216
inference.py CHANGED
@@ -3,52 +3,39 @@ inference.py
3
  ------------
4
  Baseline LLM agent for the SQL Analytics OpenEnv environment.
5
 
6
- The agent receives a natural-language business-intelligence question,
7
- iteratively generates SQL queries using an LLM (via OpenAI-compatible
8
- API), executes them in the environment, and improves based on feedback.
9
-
10
- Logging
11
- -------
12
- Every event is printed to stdout in the strictly-required format:
13
-
14
- [START] {"task_id": ..., "task_description": ..., "difficulty": ...}
15
- [STEP] {"task_id": ..., "step": ..., "sql": ..., "reward": ...,
16
- "result_rows": ..., "error": ..., "done": ...}
17
- [END] {"task_id": ..., "final_score": ..., "total_steps": ...,
18
- "success": ..., "timestamp": ...}
19
-
20
- Environment variables
21
- ---------------------
22
- API_BASE_URL Base URL for the OpenAI-compatible LLM endpoint
23
- (default: https://api.openai.com/v1)
24
- MODEL_NAME LLM model to use (default: gpt-4o-mini)
25
- HF_TOKEN HuggingFace token when connecting to HF Spaces
26
- ENV_BASE_URL URL of the running sql_analytics_env server
27
- (default: http://localhost:8000)
28
- MAX_STEPS Max LLM turns per task (default: 8)
29
-
30
- Usage
31
- -----
32
- # Against local server started with: uvicorn server.app:app --port 8000
33
- python inference.py
34
-
35
- # Against a HuggingFace Space:
36
- ENV_BASE_URL=https://<your-space>.hf.space python inference.py
37
  """
38
 
39
  from __future__ import annotations
40
 
41
- import json
42
  import os
43
  import sys
44
  import time
45
- from datetime import datetime, timezone
46
- from typing import Any, Dict, List, Optional
47
 
48
  from openai import OpenAI, RateLimitError, APIConnectionError, APIStatusError
49
 
50
  # ── Environment client ────────────────────────────────────────────────────────
51
- # Support running inference.py both as standalone and inside the package.
52
  try:
53
  from client import SQLAnalyticsEnv
54
  from models import SQLAction
@@ -58,12 +45,13 @@ except ImportError:
58
 
59
  # ── Config ────────────────────────────────────────────────────────────────────
60
 
61
- API_BASE_URL = os.environ.get("API_BASE_URL", "https://api.openai.com/v1")
62
- MODEL_NAME = os.environ.get("MODEL_NAME", "gpt-4o-mini")
63
- HF_TOKEN = os.environ.get("HF_TOKEN") # no default β€” must be set externally
64
- LOCAL_IMAGE_NAME = os.environ.get("LOCAL_IMAGE_NAME") # optional, only for from_docker_image()
65
- ENV_BASE_URL = os.environ.get("ENV_BASE_URL", "http://localhost:8000")
66
- MAX_STEPS = int(os.environ.get("MAX_STEPS", "8"))
 
67
 
68
  ALL_TASK_IDS = [
69
  "task_001", # easy
@@ -78,58 +66,34 @@ ALL_TASK_IDS = [
78
  "task_010", # medium
79
  ]
80
 
81
- # ── Logging helpers ───────────────────────────────────────────────────────────
82
-
83
- def _emit(tag: str, payload: Dict[str, Any]) -> None:
84
- """Print a structured log line to stdout (required evaluation format)."""
85
- print(f"[{tag}] {json.dumps(payload, default=str)}", flush=True)
86
-
87
-
88
- def log_start(task_id: str, task_description: str, difficulty: str) -> None:
89
- _emit("START", {
90
- "task_id": task_id,
91
- "task_description": task_description,
92
- "difficulty": difficulty,
93
- "timestamp": datetime.now(timezone.utc).isoformat(),
94
- })
95
-
96
-
97
- def log_step(
98
- task_id: str,
99
- step: int,
100
- sql: str,
101
- reward: float,
102
- result_rows: int,
103
- error: Optional[str],
104
- done: bool,
105
- ) -> None:
106
- _emit("STEP", {
107
- "task_id": task_id,
108
- "step": step,
109
- "sql": sql,
110
- "reward": reward,
111
- "result_rows": result_rows,
112
- "error": error,
113
- "done": done,
114
- })
115
-
116
-
117
- def log_end(
118
- task_id: str,
119
- final_score: float,
120
- total_steps: int,
121
- success: bool,
122
- ) -> None:
123
- _emit("END", {
124
- "task_id": task_id,
125
- "final_score": final_score,
126
- "total_steps": total_steps,
127
- "success": success,
128
- "timestamp": datetime.now(timezone.utc).isoformat(),
129
- })
130
-
131
-
132
- # ── LLM helpers ───────────────────────────────────────────────────────────────
133
 
134
  _SYSTEM_PROMPT = """You are an expert SQL analyst. You will be given a business
135
  intelligence question about an e-commerce database and must answer it by
@@ -139,48 +103,33 @@ Rules:
139
  - Write ONLY the raw SQL query β€” no markdown, no explanation, no backticks.
140
  - The database is SQLite; use SQLite-compatible syntax.
141
  - Use ROUND(..., 2) for monetary values and ratings.
142
- - Use strftime('%m', date_column) to extract the month as a string;
143
- CAST(strftime('%m', ...) AS INTEGER) for an integer.
144
- - When asked for growth rate you may use a self-join or a subquery
145
- (SQLite does not always support window functions LAG).
146
  - Return only the columns asked for in the task description.
147
  """
148
 
149
- def _build_user_message(
150
- schema: str,
151
- task_description: str,
152
- history: List[Dict],
153
- ) -> str:
154
- """Build the user turn including schema, task, and any prior attempt feedback."""
155
- lines = [
156
- "DATABASE SCHEMA:",
157
- schema,
158
- "",
159
- "TASK:",
160
- task_description,
161
- ]
162
 
 
 
163
  if history:
164
  lines += ["", "PREVIOUS ATTEMPTS (most recent first):"]
165
- for attempt in reversed(history[-3:]): # show last 3
166
- lines.append(f" SQL: {attempt['sql']}")
167
- if attempt.get("error"):
168
- lines.append(f" Error: {attempt['error']}")
169
  else:
170
- rows = attempt.get("result_rows", 0)
171
- reward = attempt.get("reward", 0.0)
172
- msg = attempt.get("message", "")
173
- lines.append(f" Rows returned: {rows} | Reward: {reward:.2f} | Feedback: {msg}")
174
  lines += ["", "Please write an improved SQL query."]
175
  else:
176
  lines += ["", "Write the SQL query:"]
177
-
178
  return "\n".join(lines)
179
 
180
 
181
- def _extract_sql(llm_response: str) -> str:
182
- """Strip markdown fences and whitespace from the LLM output."""
183
- sql = llm_response.strip()
184
  for fence in ("```sql", "```SQL", "```"):
185
  if sql.startswith(fence):
186
  sql = sql[len(fence):]
@@ -192,126 +141,101 @@ def _extract_sql(llm_response: str) -> str:
192
 
193
  # ── Agent loop ────────────────────────────────────────────────────────────────
194
 
195
- def run_task(
196
- client: OpenAI,
197
- env: SQLAnalyticsEnv,
198
- task_id: str,
199
- ) -> float:
200
- """Run one task episode. Returns the final score (0.0–1.0)."""
201
- # Reset
202
  obs = env.reset(task_id=task_id)
203
  schema = obs.db_schema or ""
204
  task_description = obs.task_description or ""
205
- difficulty = obs.metadata.get("difficulty", "?") if obs.metadata else "?"
206
-
207
- log_start(task_id, task_description, difficulty)
208
-
209
- history: List[Dict] = []
210
- final_score = 0.0
211
- step_count = 0
212
-
213
- for step in range(1, MAX_STEPS + 1):
214
- # Ask LLM (with retry + exponential backoff)
215
- user_msg = _build_user_message(schema, task_description, history)
216
- sql = ""
217
- for attempt in range(1, 4): # up to 3 retries
218
- try:
219
- response = client.chat.completions.create(
220
- model=MODEL_NAME,
221
- messages=[
222
- {"role": "system", "content": _SYSTEM_PROMPT},
223
- {"role": "user", "content": user_msg},
224
- ],
225
- temperature=0.0,
226
- max_tokens=512,
227
- )
228
- sql = _extract_sql(response.choices[0].message.content or "")
229
- break # success
230
- except RateLimitError:
231
- wait = 2 ** attempt
232
- print(f"[WARN] Rate limited β€” waiting {wait}s (attempt {attempt}/3)", file=sys.stderr)
233
- time.sleep(wait)
234
- except APIConnectionError as exc:
235
- print(f"[WARN] Connection error on step {step}, attempt {attempt}: {exc}", file=sys.stderr)
236
- time.sleep(2)
237
- except APIStatusError as exc:
238
- print(f"[WARN] API error {exc.status_code} on step {step}: {exc.message}", file=sys.stderr)
239
- break
240
- except Exception as exc:
241
- print(f"[WARN] Unexpected LLM error on step {step}: {exc}", file=sys.stderr)
 
242
  break
243
 
244
- if not sql:
245
- print(f"[WARN] No SQL generated for step {step} β€” skipping.", file=sys.stderr)
246
- break
247
 
248
- if not sql:
249
- break
 
 
 
250
 
251
- # Execute in environment
252
- obs = env.step(SQLAction(sql=sql))
253
 
254
- result_rows = len(obs.result) if obs.result else 0
255
- reward = obs.reward or 0.0
256
- error = obs.error
257
- done = obs.done
258
- message = (obs.metadata or {}).get("message", "")
259
- step_count = step
260
 
261
- log_step(task_id, step, sql, reward, result_rows, error, done)
 
262
 
263
- history.append({
264
- "sql": sql,
265
- "error": error,
266
- "result_rows": result_rows,
267
- "reward": reward,
268
- "message": message,
269
- })
270
 
271
- final_score = reward
 
272
 
273
- if done:
274
- break
275
 
276
- success = final_score >= 0.9
277
- log_end(task_id, final_score, step_count, success)
278
- return final_score
279
 
280
 
281
  # ── Entry point ───────────────────────────────────────────────────────────────
282
 
283
  def main() -> None:
284
- # Build OpenAI client β€” api_key read from OPENAI_API_KEY env var;
285
- # falls back to "placeholder" for local/unauthenticated endpoints.
286
- llm_client = OpenAI(
287
- base_url=API_BASE_URL,
288
- api_key=os.environ.get("OPENAI_API_KEY", "placeholder"),
289
- )
290
-
291
- # HF_TOKEN is read from env automatically by the OpenEnv framework
292
- # when connecting to a HuggingFace Space
293
- env = SQLAnalyticsEnv(base_url=ENV_BASE_URL)
294
-
295
- scores: Dict[str, float] = {}
296
 
 
297
  try:
298
  for task_id in ALL_TASK_IDS:
299
- score = run_task(llm_client, env, task_id)
300
- scores[task_id] = score
301
  finally:
302
  env.close()
303
 
304
- # Summary
305
  avg = sum(scores.values()) / len(scores) if scores else 0.0
 
306
  print(
307
- json.dumps({
308
- "summary": {
309
- "scores": scores,
310
- "average_score": round(avg, 4),
311
- "tasks_solved": sum(1 for s in scores.values() if s >= 0.9),
312
- "total_tasks": len(scores),
313
- }
314
- }),
315
  flush=True,
316
  )
317
 
 
3
  ------------
4
  Baseline LLM agent for the SQL Analytics OpenEnv environment.
5
 
6
+ MANDATORY ENVIRONMENT VARIABLES
7
+ --------------------------------
8
+ API_BASE_URL The API endpoint for the LLM.
9
+ MODEL_NAME The model identifier to use for inference.
10
+ HF_TOKEN Your HuggingFace / API key.
11
+ LOCAL_IMAGE_NAME Local Docker image name (only if using from_docker_image())
12
+
13
+ STDOUT FORMAT (required by evaluator)
14
+ --------------------------------------
15
+ [START] task=<task_id> env=sql_analytics_env model=<model_name>
16
+ [STEP] step=<n> action=<sql> reward=<0.00> done=<true|false> error=<msg|null>
17
+ [END] success=<true|false> steps=<n> score=<0.000> rewards=<r1,r2,...,rn>
18
+
19
+ Rules:
20
+ - One [START] per episode, one [STEP] per step, one [END] per episode.
21
+ - reward and rewards formatted to 2 decimal places.
22
+ - score formatted to 3 decimal places.
23
+ - done and success are lowercase: true or false.
24
+ - error is the raw error string, or null if none.
25
+ - All fields on a single line with no newlines within a line.
26
+ - [END] is always emitted, even on exception.
 
 
 
 
 
 
 
 
 
 
27
  """
28
 
29
  from __future__ import annotations
30
 
 
31
  import os
32
  import sys
33
  import time
34
+ from typing import List, Optional
 
35
 
36
  from openai import OpenAI, RateLimitError, APIConnectionError, APIStatusError
37
 
38
  # ── Environment client ────────────────────────────────────────────────────────
 
39
  try:
40
  from client import SQLAnalyticsEnv
41
  from models import SQLAction
 
45
 
46
  # ── Config ────────────────────────────────────────────────────────────────────
47
 
48
+ API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY")
49
+ API_BASE_URL = os.getenv("API_BASE_URL") or "https://router.huggingface.co/v1"
50
+ MODEL_NAME = os.getenv("MODEL_NAME") or "Qwen/Qwen2.5-72B-Instruct"
51
+ LOCAL_IMAGE_NAME = os.getenv("LOCAL_IMAGE_NAME") # only needed for from_docker_image()
52
+ ENV_BASE_URL = os.getenv("ENV_BASE_URL", "http://localhost:8000")
53
+ MAX_STEPS = int(os.getenv("MAX_STEPS", "8"))
54
+ BENCHMARK = "sql_analytics_env"
55
 
56
  ALL_TASK_IDS = [
57
  "task_001", # easy
 
66
  "task_010", # medium
67
  ]
68
 
69
+ SUCCESS_SCORE_THRESHOLD = 0.8 # reward >= 0.8 counts as solved
70
+
71
+ # ── Logging helpers (required stdout format) ──────────────────────────────────
72
+
73
+ def log_start(task: str, env: str, model: str) -> None:
74
+ print(f"[START] task={task} env={env} model={model}", flush=True)
75
+
76
+
77
+ def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:
78
+ # Escape newlines in SQL so it stays on one line
79
+ action_clean = action.replace("\n", " ").replace("\r", "")
80
+ error_val = error.replace("\n", " ") if error else "null"
81
+ done_val = str(done).lower()
82
+ print(
83
+ f"[STEP] step={step} action={action_clean} reward={reward:.2f} done={done_val} error={error_val}",
84
+ flush=True,
85
+ )
86
+
87
+
88
+ def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:
89
+ rewards_str = ",".join(f"{r:.2f}" for r in rewards)
90
+ print(
91
+ f"[END] success={str(success).lower()} steps={steps} score={score:.3f} rewards={rewards_str}",
92
+ flush=True,
93
+ )
94
+
95
+
96
+ # ── LLM prompt ────────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
  _SYSTEM_PROMPT = """You are an expert SQL analyst. You will be given a business
99
  intelligence question about an e-commerce database and must answer it by
 
103
  - Write ONLY the raw SQL query β€” no markdown, no explanation, no backticks.
104
  - The database is SQLite; use SQLite-compatible syntax.
105
  - Use ROUND(..., 2) for monetary values and ratings.
106
+ - Use strftime('%m', date_column) to extract the month as a string.
107
+ - When asked for growth rate, use a self-join or subquery (SQLite does not
108
+ always support window function LAG).
 
109
  - Return only the columns asked for in the task description.
110
  """
111
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
+ def _build_user_message(schema: str, task_description: str, history: list) -> str:
114
+ lines = ["DATABASE SCHEMA:", schema, "", "TASK:", task_description]
115
  if history:
116
  lines += ["", "PREVIOUS ATTEMPTS (most recent first):"]
117
+ for h in reversed(history[-3:]):
118
+ lines.append(f" SQL: {h['sql']}")
119
+ if h.get("error"):
120
+ lines.append(f" Error: {h['error']}")
121
  else:
122
+ lines.append(
123
+ f" Rows: {h.get('result_rows', 0)} | Reward: {h.get('reward', 0.0):.2f}"
124
+ )
 
125
  lines += ["", "Please write an improved SQL query."]
126
  else:
127
  lines += ["", "Write the SQL query:"]
 
128
  return "\n".join(lines)
129
 
130
 
131
+ def _extract_sql(text: str) -> str:
132
+ sql = text.strip()
 
133
  for fence in ("```sql", "```SQL", "```"):
134
  if sql.startswith(fence):
135
  sql = sql[len(fence):]
 
141
 
142
  # ── Agent loop ────────────────────────────────────────────────────────────────
143
 
144
+ def run_task(client: OpenAI, env: SQLAnalyticsEnv, task_id: str) -> float:
145
+ """Run one episode. Returns the final score in [0, 1]."""
 
 
 
 
 
146
  obs = env.reset(task_id=task_id)
147
  schema = obs.db_schema or ""
148
  task_description = obs.task_description or ""
149
+
150
+ log_start(task=task_id, env=BENCHMARK, model=MODEL_NAME)
151
+
152
+ history: list = []
153
+ rewards: List[float] = []
154
+ steps_taken = 0
155
+ score = 0.0
156
+ success = False
157
+
158
+ try:
159
+ for step in range(1, MAX_STEPS + 1):
160
+ user_msg = _build_user_message(schema, task_description, history)
161
+ sql = ""
162
+
163
+ # LLM call with retry + exponential backoff
164
+ for attempt in range(1, 4):
165
+ try:
166
+ response = client.chat.completions.create(
167
+ model=MODEL_NAME,
168
+ messages=[
169
+ {"role": "system", "content": _SYSTEM_PROMPT},
170
+ {"role": "user", "content": user_msg},
171
+ ],
172
+ temperature=0.0,
173
+ max_tokens=512,
174
+ )
175
+ sql = _extract_sql(response.choices[0].message.content or "")
176
+ break
177
+ except RateLimitError:
178
+ time.sleep(2 ** attempt)
179
+ except APIConnectionError:
180
+ time.sleep(2)
181
+ except (APIStatusError, Exception) as exc:
182
+ print(f"[DEBUG] LLM error step={step}: {exc}", file=sys.stderr, flush=True)
183
+ break
184
+
185
+ if not sql:
186
+ print(f"[DEBUG] No SQL at step={step}, stopping.", file=sys.stderr, flush=True)
187
  break
188
 
189
+ obs = env.step(SQLAction(sql=sql))
 
 
190
 
191
+ reward = obs.reward or 0.0
192
+ done = obs.done
193
+ error = obs.error
194
+ result_rows = len(obs.result) if obs.result else 0
195
+ steps_taken = step
196
 
197
+ rewards.append(reward)
198
+ log_step(step=step, action=sql, reward=reward, done=done, error=error)
199
 
200
+ history.append({
201
+ "sql": sql,
202
+ "error": error,
203
+ "result_rows": result_rows,
204
+ "reward": reward,
205
+ })
206
 
207
+ if done:
208
+ break
209
 
210
+ score = rewards[-1] if rewards else 0.0
211
+ success = score >= SUCCESS_SCORE_THRESHOLD
 
 
 
 
 
212
 
213
+ except Exception as exc:
214
+ print(f"[DEBUG] Unhandled exception in run_task: {exc}", file=sys.stderr, flush=True)
215
 
216
+ finally:
217
+ log_end(success=success, steps=steps_taken, score=score, rewards=rewards)
218
 
219
+ return score
 
 
220
 
221
 
222
  # ── Entry point ───────────────────────────────────────────────────────────────
223
 
224
  def main() -> None:
225
+ client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
226
+ env = SQLAnalyticsEnv(base_url=ENV_BASE_URL)
 
 
 
 
 
 
 
 
 
 
227
 
228
+ scores = {}
229
  try:
230
  for task_id in ALL_TASK_IDS:
231
+ scores[task_id] = run_task(client, env, task_id)
 
232
  finally:
233
  env.close()
234
 
 
235
  avg = sum(scores.values()) / len(scores) if scores else 0.0
236
+ solved = sum(1 for s in scores.values() if s >= SUCCESS_SCORE_THRESHOLD)
237
  print(
238
+ f"[SUMMARY] tasks={len(scores)} solved={solved} avg_score={avg:.3f}",
 
 
 
 
 
 
 
239
  flush=True,
240
  )
241