tns Claude Opus 4.6 commited on
Commit
32c2a13
·
1 Parent(s): 4f3e2c1

Rewrite inference.py to use OpenEnv client and BENCHMARK_URL

Browse files

- Use DataCleanEnv client instead of raw HTTP requests
- Default BENCHMARK_URL to HF Space (not localhost:8000)
- Add try/finally to guarantee [END] is always printed
- Match structured output format from reference implementations

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

Files changed (1) hide show
  1. inference.py +135 -132
inference.py CHANGED
@@ -9,17 +9,25 @@ MANDATORY
9
 
10
  - The inference script must be named `inference.py` and placed in the root directory of the project
11
  - Participants must use OpenAI Client for all LLM calls using above variables
 
 
 
 
 
12
  """
13
 
 
 
14
  import json
15
  import os
16
  import re
17
- import sys
18
  import textwrap
19
  from typing import Any, Dict, List, Optional
20
 
21
  from openai import OpenAI
22
- import requests
 
 
23
 
24
  # ---------------------------------------------------------------------------
25
  # Config
@@ -27,10 +35,42 @@ import requests
27
  API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
28
  API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY", "")
29
  MODEL_NAME = os.getenv("MODEL_NAME", "")
30
- ENV_URL = os.getenv("ENV_URL", "http://localhost:8000")
 
 
 
 
 
31
 
32
  TASKS = ["customer_contacts", "sales_records", "employee_records", "financial_transactions"]
33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  # ---------------------------------------------------------------------------
35
  # System prompt — Conservative plan-then-execute
36
  # ---------------------------------------------------------------------------
@@ -68,25 +108,6 @@ PLANNING_PROMPT = textwrap.dedent("""\
68
  """)
69
 
70
 
71
- # ---------------------------------------------------------------------------
72
- # HTTP helpers
73
- # ---------------------------------------------------------------------------
74
- def env_reset(task_id: str) -> Dict[str, Any]:
75
- resp = requests.post(f"{ENV_URL}/reset", json={"task_id": task_id}, timeout=30)
76
- resp.raise_for_status()
77
- return resp.json()
78
-
79
-
80
- def env_step(command: str) -> Dict[str, Any]:
81
- resp = requests.post(
82
- f"{ENV_URL}/step",
83
- json={"action": {"command": command}},
84
- timeout=30,
85
- )
86
- resp.raise_for_status()
87
- return resp.json()
88
-
89
-
90
  # ---------------------------------------------------------------------------
91
  # JSON plan extraction
92
  # ---------------------------------------------------------------------------
@@ -157,36 +178,31 @@ def extract_action(response_text: str) -> str:
157
 
158
 
159
  # ---------------------------------------------------------------------------
160
- # Main Plan-Then-Execute
161
  # ---------------------------------------------------------------------------
162
- def main() -> None:
163
- client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
164
- results = {}
 
 
165
 
166
- for task_id in TASKS:
167
- print(f"\n{'=' * 60}")
168
- print(f"Task: {task_id}")
169
- print(f"{'=' * 60}")
170
- print(f"[START] task={task_id}", flush=True)
171
 
172
- obs = env_reset(task_id)
173
- if "observation" in obs:
174
- obs = obs["observation"]
 
 
175
 
176
- step_count = 0
177
- done = obs.get("done", False)
178
  if done:
179
- score = obs.get("current_score", 0.0)
180
- results[task_id] = score
181
- print(f"[END] task={task_id} score={score} steps=0", flush=True)
182
- continue
183
 
184
- total_issues = obs.get("total_issues", 0)
185
 
186
  # --- Phase 1: Auto-inspect all columns ---
187
  columns = []
188
- col_info = obs.get("column_info", "")
189
- for line in col_info.strip().splitlines():
190
  line = line.strip()
191
  if ":" in line:
192
  col_name = line.split(":")[0].strip()
@@ -199,41 +215,34 @@ def main() -> None:
199
  break
200
  step_count += 1
201
  cmd = f'inspect("{col}")'
202
- print(f" Step {step_count}: {cmd}")
203
- obs = env_step(cmd)
204
- if "observation" in obs:
205
- obs = obs["observation"]
206
- done = obs.get("done", False)
207
- reward = obs.get("current_score", 0.0)
208
- print(f"[STEP] step={step_count} reward={reward}", flush=True)
209
- feedback = obs.get("feedback", "")
210
- inspection_results[col] = feedback
211
 
212
  if done:
213
- score = obs.get("current_score", 0.0)
214
- results[task_id] = score
215
- print(f" Done during inspection. Score: {score:.4f}")
216
- print(f"[END] task={task_id} score={score} steps={step_count}", flush=True)
217
- continue
218
 
219
  # --- Phase 1.5: Filter to only columns WITH issues ---
220
  flagged_inspections = {}
221
  for col, feedback in inspection_results.items():
222
- # Extract "Issues remaining in this column: N"
223
  m = re.search(r"Issues remaining in this column:\s*(\d+)", feedback)
224
  issue_count = int(m.group(1)) if m else 0
225
  if issue_count > 0:
226
  flagged_inspections[col] = feedback
227
 
228
- # Also check for suspicious values in inspection even if issue count is 0
229
  for col, feedback in inspection_results.items():
230
  if col not in flagged_inspections and "Suspicious:" in feedback:
231
  flagged_inspections[col] = feedback
232
 
233
- print(f" Columns with issues: {list(flagged_inspections.keys())} ({len(flagged_inspections)}/{len(columns)})")
234
-
235
  # --- Phase 2: Ask LLM to plan fixes ---
236
- # Only show the LLM columns that have issues + the data table
237
  if flagged_inspections:
238
  inspection_text = "\n\n".join(
239
  f"[{col}]\n{fb}" for col, fb in flagged_inspections.items()
@@ -242,18 +251,17 @@ def main() -> None:
242
  inspection_text = "(No specific column issues flagged. Check for duplicate rows.)"
243
 
244
  planning_message = (
245
- f"Task: {obs.get('task_id', '?')} ({obs.get('difficulty', '?')})\n"
246
  f"Total issues to find and fix: {total_issues}\n\n"
247
- f"Task description:\n{obs.get('task_description', '')}\n\n"
248
- f"Column definitions:\n{obs.get('column_info', '')}\n\n"
249
  f"FLAGGED COLUMNS (only fix cells in these columns or duplicate rows):\n{inspection_text}\n\n"
250
- f"Current data:\n{obs.get('data_preview', '')}\n\n"
251
  f"Produce a JSON array with EXACTLY the fixes needed. "
252
  f"Expected: around {total_issues} actions (fixes + deletes). "
253
  f"Do NOT produce more than {total_issues + 3} actions."
254
  )
255
 
256
- print(f" Calling LLM for fix plan...")
257
  try:
258
  completion = client.chat.completions.create(
259
  model=MODEL_NAME,
@@ -267,26 +275,26 @@ def main() -> None:
267
  )
268
  plan_text = completion.choices[0].message.content or ""
269
  except Exception as exc:
270
- print(f" LLM error: {exc}. Submitting.")
271
  step_count += 1
272
- obs = env_step("submit()")
273
- if "observation" in obs:
274
- obs = obs["observation"]
275
- score = obs.get("current_score", 0.0)
276
- results[task_id] = score
277
- print(f"[STEP] step={step_count} reward={score}", flush=True)
278
- print(f"[END] task={task_id} score={score} steps={step_count}", flush=True)
279
- continue
 
280
 
281
  plan = extract_json_plan(plan_text)
282
 
283
  # --- Sanity check: reject bloated plans ---
284
  if plan and len(plan) > total_issues + 5:
285
- print(f" Plan too large ({len(plan)} actions for {total_issues} issues). Trimming to {total_issues + 3}.")
286
  plan = plan[:total_issues + 3]
287
 
288
  if not plan:
289
- print(f" Failed to parse JSON plan. Falling back to single-action mode.")
290
  fallback_messages = [
291
  {"role": "system", "content": (
292
  "You are a data quality analyst. Respond with EXACTLY ONE command per turn.\n"
@@ -296,7 +304,7 @@ def main() -> None:
296
  )},
297
  {"role": "user", "content": planning_message},
298
  ]
299
- remaining = obs.get("actions_remaining", 0)
300
  while not done and remaining > 0:
301
  try:
302
  comp = client.chat.completions.create(
@@ -312,74 +320,69 @@ def main() -> None:
312
  action_cmd = extract_action(resp_text)
313
  fallback_messages.append({"role": "assistant", "content": action_cmd})
314
  step_count += 1
315
- print(f" Step {step_count} (fallback): {action_cmd}")
316
- obs = env_step(action_cmd)
317
- if "observation" in obs:
318
- obs = obs["observation"]
319
- done = obs.get("done", False)
320
- reward = obs.get("current_score", 0.0)
321
- print(f"[STEP] step={step_count} reward={reward}", flush=True)
322
- remaining = obs.get("actions_remaining", 0)
323
  if not done:
324
- fb = obs.get("feedback", "")
325
- fallback_messages.append({"role": "user", "content": f"Result: {fb}\nFixed: {obs.get('issues_fixed',0)}/{obs.get('total_issues',0)}. Remaining steps: {remaining}."})
326
  if len(fallback_messages) > 30:
327
  fallback_messages = [fallback_messages[0]] + fallback_messages[-28:]
328
- score = obs.get("current_score", 0.0)
329
- results[task_id] = score
330
- print(f" Final score for {task_id}: {score:.4f}")
331
- print(f"[END] task={task_id} score={score} steps={step_count}", flush=True)
332
- continue
333
-
334
- print(f" Plan has {len(plan)} actions (expected ~{total_issues}).")
335
 
336
  # --- Phase 3: Execute plan ---
337
- remaining = obs.get("actions_remaining", 0)
338
- for i, action_item in enumerate(plan):
339
  if done or remaining <= 1:
340
  break
341
-
342
  cmd = plan_to_command(action_item)
343
  if not cmd:
344
  continue
345
-
346
  step_count += 1
347
- print(f" Step {step_count}: {cmd}")
348
- obs = env_step(cmd)
349
- if "observation" in obs:
350
- obs = obs["observation"]
351
- done = obs.get("done", False)
352
- reward = obs.get("current_score", 0.0)
353
- print(f"[STEP] step={step_count} reward={reward}", flush=True)
354
- remaining = obs.get("actions_remaining", 0)
355
-
356
- feedback = obs.get("feedback", "")
357
- if "not yet resolved" in feedback.lower() and not done:
358
- print(f" Warning: {feedback[:80]}")
359
 
360
  # --- Phase 4: Submit ---
361
  if not done:
362
  step_count += 1
363
- print(f" Step {step_count}: submit()")
364
- obs = env_step("submit()")
365
- if "observation" in obs:
366
- obs = obs["observation"]
367
- reward = obs.get("current_score", 0.0)
368
- print(f"[STEP] step={step_count} reward={reward}", flush=True)
369
-
370
- score = obs.get("current_score", 0.0)
371
- results[task_id] = score
372
- print(f" Final score for {task_id}: {score:.4f}")
373
- print(f"[END] task={task_id} score={score} steps={step_count}", flush=True)
374
-
375
- # --- Results Summary ---
376
- print(f"\n{'=' * 60}")
377
- print("RESULTS SUMMARY")
378
- print(f"{'=' * 60}")
379
- for task_id, score in results.items():
380
- print(f" {task_id}: {score:.4f}")
381
- avg = sum(results.values()) / len(results) if results else 0.0
382
- print(f" Average: {avg:.4f}")
 
 
 
 
 
 
 
383
 
384
 
385
  if __name__ == "__main__":
 
9
 
10
  - The inference script must be named `inference.py` and placed in the root directory of the project
11
  - Participants must use OpenAI Client for all LLM calls using above variables
12
+
13
+ This script emits exactly these stdout line types:
14
+ - [START] ...
15
+ - [STEP] ... (one per step)
16
+ - [END] ... (always)
17
  """
18
 
19
+ from __future__ import annotations
20
+
21
  import json
22
  import os
23
  import re
 
24
  import textwrap
25
  from typing import Any, Dict, List, Optional
26
 
27
  from openai import OpenAI
28
+
29
+ from client import DataCleanEnv
30
+ from models import DataCleanAction, DataCleanObservation
31
 
32
  # ---------------------------------------------------------------------------
33
  # Config
 
35
  API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
36
  API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY", "")
37
  MODEL_NAME = os.getenv("MODEL_NAME", "")
38
+
39
+ BENCHMARK_URL = os.getenv(
40
+ "BENCHMARK_URL",
41
+ "https://tns-openenv-data-clean.hf.space",
42
+ )
43
+ BENCHMARK = os.getenv("BENCHMARK", "data_clean_env")
44
 
45
  TASKS = ["customer_contacts", "sales_records", "employee_records", "financial_transactions"]
46
 
47
+ # ---------------------------------------------------------------------------
48
+ # Structured logging
49
+ # ---------------------------------------------------------------------------
50
+ def log_start(task: str, env: str, model: str) -> None:
51
+ print(f"[START] task={task} env={env} model={model}", flush=True)
52
+
53
+
54
+ def log_step(step: int, action: str, reward: float, done: bool, error: str | None) -> None:
55
+ err = _single_line(error) if error else "null"
56
+ print(
57
+ f"[STEP] step={step} action={action} reward={reward:.2f} done={str(done).lower()} error={err}",
58
+ flush=True,
59
+ )
60
+
61
+
62
+ def log_end(success: bool, steps: int, score: float, rewards: list[float]) -> None:
63
+ reward_csv = ",".join(f"{r:.2f}" for r in rewards) if rewards else "0.00"
64
+ print(
65
+ f"[END] success={str(success).lower()} steps={steps} score={score:.3f} rewards={reward_csv}",
66
+ flush=True,
67
+ )
68
+
69
+
70
+ def _single_line(text: str | None) -> str:
71
+ return (text or "").replace("\n", " ").replace("\r", " ").strip()
72
+
73
+
74
  # ---------------------------------------------------------------------------
75
  # System prompt — Conservative plan-then-execute
76
  # ---------------------------------------------------------------------------
 
108
  """)
109
 
110
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  # ---------------------------------------------------------------------------
112
  # JSON plan extraction
113
  # ---------------------------------------------------------------------------
 
178
 
179
 
180
  # ---------------------------------------------------------------------------
181
+ # Run a single task
182
  # ---------------------------------------------------------------------------
183
+ def run_task(client: OpenAI, env, task_id: str) -> None:
184
+ rewards: list[float] = []
185
+ step_count = 0
186
+ score = 0.0
187
+ success = False
188
 
189
+ log_start(task=task_id, env=BENCHMARK, model=MODEL_NAME)
 
 
 
 
190
 
191
+ try:
192
+ # --- Reset ---
193
+ result = env.reset(task_id=task_id)
194
+ obs = result.observation
195
+ done = result.done
196
 
 
 
197
  if done:
198
+ score = obs.current_score
199
+ return
 
 
200
 
201
+ total_issues = obs.total_issues
202
 
203
  # --- Phase 1: Auto-inspect all columns ---
204
  columns = []
205
+ for line in obs.column_info.strip().splitlines():
 
206
  line = line.strip()
207
  if ":" in line:
208
  col_name = line.split(":")[0].strip()
 
215
  break
216
  step_count += 1
217
  cmd = f'inspect("{col}")'
218
+ result = env.step(DataCleanAction(command=cmd))
219
+ obs = result.observation
220
+ done = result.done
221
+ reward = float(result.reward or 0.0)
222
+ rewards.append(reward)
223
+
224
+ log_step(step=step_count, action=cmd, reward=reward, done=done, error=None)
225
+
226
+ inspection_results[col] = obs.feedback
227
 
228
  if done:
229
+ score = obs.current_score
230
+ success = score >= 0.5
231
+ return
 
 
232
 
233
  # --- Phase 1.5: Filter to only columns WITH issues ---
234
  flagged_inspections = {}
235
  for col, feedback in inspection_results.items():
 
236
  m = re.search(r"Issues remaining in this column:\s*(\d+)", feedback)
237
  issue_count = int(m.group(1)) if m else 0
238
  if issue_count > 0:
239
  flagged_inspections[col] = feedback
240
 
 
241
  for col, feedback in inspection_results.items():
242
  if col not in flagged_inspections and "Suspicious:" in feedback:
243
  flagged_inspections[col] = feedback
244
 
 
 
245
  # --- Phase 2: Ask LLM to plan fixes ---
 
246
  if flagged_inspections:
247
  inspection_text = "\n\n".join(
248
  f"[{col}]\n{fb}" for col, fb in flagged_inspections.items()
 
251
  inspection_text = "(No specific column issues flagged. Check for duplicate rows.)"
252
 
253
  planning_message = (
254
+ f"Task: {obs.task_id} ({obs.difficulty})\n"
255
  f"Total issues to find and fix: {total_issues}\n\n"
256
+ f"Task description:\n{obs.task_description}\n\n"
257
+ f"Column definitions:\n{obs.column_info}\n\n"
258
  f"FLAGGED COLUMNS (only fix cells in these columns or duplicate rows):\n{inspection_text}\n\n"
259
+ f"Current data:\n{obs.data_preview}\n\n"
260
  f"Produce a JSON array with EXACTLY the fixes needed. "
261
  f"Expected: around {total_issues} actions (fixes + deletes). "
262
  f"Do NOT produce more than {total_issues + 3} actions."
263
  )
264
 
 
265
  try:
266
  completion = client.chat.completions.create(
267
  model=MODEL_NAME,
 
275
  )
276
  plan_text = completion.choices[0].message.content or ""
277
  except Exception as exc:
278
+ # LLM error submit immediately
279
  step_count += 1
280
+ cmd = "submit()"
281
+ result = env.step(DataCleanAction(command=cmd))
282
+ obs = result.observation
283
+ done = result.done
284
+ reward = float(result.reward or 0.0)
285
+ rewards.append(reward)
286
+ log_step(step=step_count, action=cmd, reward=reward, done=True, error=_single_line(str(exc)))
287
+ score = obs.current_score
288
+ return
289
 
290
  plan = extract_json_plan(plan_text)
291
 
292
  # --- Sanity check: reject bloated plans ---
293
  if plan and len(plan) > total_issues + 5:
 
294
  plan = plan[:total_issues + 3]
295
 
296
  if not plan:
297
+ # --- Fallback: single-action mode ---
298
  fallback_messages = [
299
  {"role": "system", "content": (
300
  "You are a data quality analyst. Respond with EXACTLY ONE command per turn.\n"
 
304
  )},
305
  {"role": "user", "content": planning_message},
306
  ]
307
+ remaining = obs.actions_remaining
308
  while not done and remaining > 0:
309
  try:
310
  comp = client.chat.completions.create(
 
320
  action_cmd = extract_action(resp_text)
321
  fallback_messages.append({"role": "assistant", "content": action_cmd})
322
  step_count += 1
323
+ result = env.step(DataCleanAction(command=action_cmd))
324
+ obs = result.observation
325
+ done = result.done
326
+ reward = float(result.reward or 0.0)
327
+ rewards.append(reward)
328
+ log_step(step=step_count, action=action_cmd, reward=reward, done=done, error=None)
329
+ remaining = obs.actions_remaining
 
330
  if not done:
331
+ fb = obs.feedback
332
+ fallback_messages.append({"role": "user", "content": f"Result: {fb}\nFixed: {obs.issues_fixed}/{obs.total_issues}. Remaining steps: {remaining}."})
333
  if len(fallback_messages) > 30:
334
  fallback_messages = [fallback_messages[0]] + fallback_messages[-28:]
335
+ score = obs.current_score
336
+ success = score >= 0.5
337
+ return
 
 
 
 
338
 
339
  # --- Phase 3: Execute plan ---
340
+ remaining = obs.actions_remaining
341
+ for action_item in plan:
342
  if done or remaining <= 1:
343
  break
 
344
  cmd = plan_to_command(action_item)
345
  if not cmd:
346
  continue
 
347
  step_count += 1
348
+ result = env.step(DataCleanAction(command=cmd))
349
+ obs = result.observation
350
+ done = result.done
351
+ reward = float(result.reward or 0.0)
352
+ rewards.append(reward)
353
+ log_step(step=step_count, action=cmd, reward=reward, done=done, error=None)
354
+ remaining = obs.actions_remaining
 
 
 
 
 
355
 
356
  # --- Phase 4: Submit ---
357
  if not done:
358
  step_count += 1
359
+ cmd = "submit()"
360
+ result = env.step(DataCleanAction(command=cmd))
361
+ obs = result.observation
362
+ reward = float(result.reward or 0.0)
363
+ rewards.append(reward)
364
+ log_step(step=step_count, action=cmd, reward=reward, done=True, error=None)
365
+
366
+ score = obs.current_score
367
+ success = score >= 0.5
368
+
369
+ except Exception as exc:
370
+ log_step(step=step_count + 1, action="error", reward=0.0, done=True, error=_single_line(str(exc)))
371
+ success = False
372
+ finally:
373
+ log_end(success=success, steps=step_count, score=score, rewards=rewards)
374
+
375
+
376
+ # ---------------------------------------------------------------------------
377
+ # Main
378
+ # ---------------------------------------------------------------------------
379
+ def main() -> None:
380
+ client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
381
+
382
+ env_client = DataCleanEnv(base_url=BENCHMARK_URL)
383
+ with env_client.sync() as env:
384
+ for task_id in TASKS:
385
+ run_task(client, env, task_id)
386
 
387
 
388
  if __name__ == "__main__":