Mishika Sardana commited on
Commit
8db2aab
Β·
unverified Β·
1 Parent(s): b25c5d6

Refactor inference.py for improved validation and error handling

Browse files

Refactor inference.py to improve task validation and error handling. Implemented score clamping and server readiness checks, ensuring consistent output format.

Files changed (1) hide show
  1. inference.py +107 -282
inference.py CHANGED
@@ -1,122 +1,26 @@
1
  """
2
  DataClean OpenEnv β€” inference.py
3
  Required output format: [START] / [STEP] / [END] structured blocks.
4
-
5
- FIX SUMMARY (Phase 2 Task Validation failure):
6
- ROOT CAUSE: sys.exit(1) when HF_TOKEN is missing causes NO [END] lines to be
7
- printed. The validator then defaults missing task scores to 0.0, which is NOT
8
- strictly in (0, 1) β†’ Task Validation fails even though Output Parsing passes.
9
-
10
- FIX 1: Removed sys.exit(1). When HF_TOKEN/LLM is unavailable, inference.py
11
- falls back to a deterministic rule-based agent that achieves 0.93+ without
12
- any external API. [END] lines are ALWAYS printed for all 4 tasks.
13
-
14
- FIX 2: Triple-layer score clamping: server clamps, inference clamps, log_end
15
- clamps β€” ensuring score is never exactly 0.0 or 1.0 at any output point.
16
-
17
- FIX 3: Added server readiness wait loop β€” if the Space just woke up, requests
18
- retry for up to 60s instead of failing instantly with a connection error.
19
-
20
- Env vars:
21
- ENV_URL β€” environment base URL (default: http://localhost:7860)
22
- HF_TOKEN β€” LLM API key (optional; falls back to rule-based if missing)
23
- API_BASE_URL β€” LLM base URL (default: https://api.openai.com/v1)
24
- MODEL_NAME β€” LLM model name (default: gpt-4o-mini)
25
  """
26
  import os, json, time, sys
27
  import requests
 
28
  from concurrent.futures import ThreadPoolExecutor, as_completed
29
- from typing import Dict, Optional, Tuple
30
 
31
- ENV_URL = os.environ.get("ENV_URL", "http://localhost:7860")
32
- API_KEY = os.environ.get("HF_TOKEN", "")
33
- BASE_URL = os.environ.get("API_BASE_URL", "https://api.openai.com/v1")
34
- MODEL = os.environ.get("MODEL_NAME", "gpt-4o-mini")
35
 
36
  TASK_MAX_STEPS = {
37
- "task1": 10,
38
- "task2": 20,
39
- "task3": 30,
40
  "task4_data_drift": 40,
41
  }
42
 
43
- # ── Deterministic rule-based strategies (no LLM needed) ──────────────────────
44
- # Each strategy is a list of actions to execute in order.
45
- # The agent follows these regardless of LLM availability.
46
- # These are the exact sequences that achieve 0.93+ baseline scores.
47
-
48
- RULE_STRATEGIES: Dict[str, list] = {
49
- "task1": [
50
- {"operation": "fill_nulls", "column": "age", "strategy": "median", "table_name": "main"},
51
- {"operation": "cast_column", "column": "age", "dtype": "int", "table_name": "main"},
52
- {"operation": "fill_nulls", "column": "salary", "strategy": "mean", "table_name": "main"},
53
- {"operation": "submit"},
54
- ],
55
- "task2": [
56
- {"operation": "remove_duplicates", "table_name": "main"},
57
- {"operation": "normalize_values", "column": "country", "method": "upper", "table_name": "main"},
58
- {"operation": "cast_column", "column": "order_date", "dtype": "datetime", "table_name": "main"},
59
- {"operation": "fill_nulls", "column": "amount", "strategy": "mean", "table_name": "main"},
60
- {"operation": "submit"},
61
- ],
62
- "task3": [
63
- {"operation": "merge_tables", "left_table": "orders", "right_table": "customers",
64
- "on": "customer_id", "output_table": "merged"},
65
- {"operation": "fill_nulls", "column": "age", "strategy": "median", "table_name": "merged"},
66
- {"operation": "cast_column", "column": "age", "dtype": "int", "table_name": "merged"},
67
- {"operation": "filter_outliers", "column": "amount", "method": "iqr", "threshold": 1.5,
68
- "table_name": "merged"},
69
- {"operation": "add_derived_column", "column_name": "order_year", "source_column": "order_date",
70
- "transform": "year_from_date", "table_name": "merged"},
71
- {"operation": "submit"},
72
- ],
73
- "task4_data_drift": [
74
- # Cleaning cycle β€” repeated after every drift injection (every 5 steps)
75
- {"operation": "filter_outliers", "column": "amount", "method": "iqr", "threshold": 1.5, "table_name": "stream"},
76
- {"operation": "fill_nulls", "column": "amount", "strategy": "mean", "table_name": "stream"},
77
- {"operation": "cast_column", "column": "amount", "dtype": "float", "table_name": "stream"},
78
- {"operation": "fill_nulls", "column": "category", "strategy": "mode", "table_name": "stream"},
79
- {"operation": "fill_nulls", "column": "region", "strategy": "mode", "table_name": "stream"},
80
- {"operation": "cast_column", "column": "event_ts", "dtype": "datetime", "table_name": "stream"},
81
- # Drift injected at step 5, 10, 15, 20, 25, 30, 35 β€” re-clean each time
82
- {"operation": "filter_outliers", "column": "amount", "method": "iqr", "threshold": 1.5, "table_name": "stream"},
83
- {"operation": "fill_nulls", "column": "amount", "strategy": "mean", "table_name": "stream"},
84
- {"operation": "cast_column", "column": "amount", "dtype": "float", "table_name": "stream"},
85
- {"operation": "fill_nulls", "column": "category", "strategy": "mode", "table_name": "stream"},
86
- {"operation": "fill_nulls", "column": "region", "strategy": "mode", "table_name": "stream"},
87
- {"operation": "cast_column", "column": "event_ts", "dtype": "datetime", "table_name": "stream"},
88
- # Cycle 3
89
- {"operation": "filter_outliers", "column": "amount", "method": "iqr", "threshold": 1.5, "table_name": "stream"},
90
- {"operation": "fill_nulls", "column": "amount", "strategy": "mean", "table_name": "stream"},
91
- {"operation": "cast_column", "column": "amount", "dtype": "float", "table_name": "stream"},
92
- {"operation": "fill_nulls", "column": "category", "strategy": "mode", "table_name": "stream"},
93
- {"operation": "fill_nulls", "column": "region", "strategy": "mode", "table_name": "stream"},
94
- {"operation": "cast_column", "column": "event_ts", "dtype": "datetime", "table_name": "stream"},
95
- # Cycle 4
96
- {"operation": "filter_outliers", "column": "amount", "method": "iqr", "threshold": 1.5, "table_name": "stream"},
97
- {"operation": "fill_nulls", "column": "amount", "strategy": "mean", "table_name": "stream"},
98
- {"operation": "cast_column", "column": "amount", "dtype": "float", "table_name": "stream"},
99
- {"operation": "fill_nulls", "column": "category", "strategy": "mode", "table_name": "stream"},
100
- {"operation": "fill_nulls", "column": "region", "strategy": "mode", "table_name": "stream"},
101
- {"operation": "cast_column", "column": "event_ts", "dtype": "datetime", "table_name": "stream"},
102
- # Cycle 5
103
- {"operation": "filter_outliers", "column": "amount", "method": "iqr", "threshold": 1.5, "table_name": "stream"},
104
- {"operation": "fill_nulls", "column": "amount", "strategy": "mean", "table_name": "stream"},
105
- {"operation": "cast_column", "column": "amount", "dtype": "float", "table_name": "stream"},
106
- {"operation": "fill_nulls", "column": "category", "strategy": "mode", "table_name": "stream"},
107
- {"operation": "fill_nulls", "column": "region", "strategy": "mode", "table_name": "stream"},
108
- {"operation": "cast_column", "column": "event_ts", "dtype": "datetime", "table_name": "stream"},
109
- # Cycle 6
110
- {"operation": "filter_outliers", "column": "amount", "method": "iqr", "threshold": 1.5, "table_name": "stream"},
111
- {"operation": "fill_nulls", "column": "amount", "strategy": "mean", "table_name": "stream"},
112
- {"operation": "cast_column", "column": "amount", "dtype": "float", "table_name": "stream"},
113
- {"operation": "fill_nulls", "column": "category", "strategy": "mode", "table_name": "stream"},
114
- {"operation": "fill_nulls", "column": "region", "strategy": "mode", "table_name": "stream"},
115
- {"operation": "cast_column", "column": "event_ts", "dtype": "datetime", "table_name": "stream"},
116
- {"operation": "submit"},
117
- ],
118
- }
119
-
120
  SYSTEM_PROMPT = """You are an expert data cleaning agent. Respond ONLY with valid JSON β€” no prose, no markdown.
121
 
122
  Operations:
@@ -136,146 +40,73 @@ Task strategies:
136
  task4_data_drift: filter_outliers(amount,iqr,1.5,stream)->fill_nulls(amount,mean,stream)->cast_column(amount,float,stream)->fill_nulls(category,mode,stream)->fill_nulls(region,mode,stream)->cast_column(event_ts,datetime,stream)->submit
137
  """
138
 
139
- # ── LLM client (optional) ─────────────────────────────────────────────────────
140
 
141
- _llm_available: Optional[bool] = None # None = not yet tested
142
 
143
- def _test_llm() -> bool:
144
- """Test if LLM is reachable. Called once, result cached."""
145
- global _llm_available
146
- if _llm_available is not None:
147
- return _llm_available
148
- if not API_KEY:
149
- print("[INFO] HF_TOKEN not set β€” using rule-based fallback agent", flush=True)
150
- _llm_available = False
151
- return False
152
  try:
153
- from openai import OpenAI
154
- c = OpenAI(api_key=API_KEY, base_url=BASE_URL)
155
- c.chat.completions.create(
156
- model=MODEL,
157
- messages=[{"role": "user", "content": "ping"}],
158
- max_tokens=1,
159
- timeout=10,
160
- )
161
- _llm_available = True
162
- print(f"[INFO] LLM available: {MODEL} @ {BASE_URL}", flush=True)
163
- except Exception as e:
164
- print(f"[INFO] LLM unavailable ({e}) β€” using rule-based fallback agent", flush=True)
165
- _llm_available = False
166
- return _llm_available
167
-
168
-
169
- def _llm_action(obs: dict, task_id: str) -> Optional[dict]:
170
- """Get action from LLM. Returns None if LLM fails."""
171
- if not _test_llm():
172
- return None
173
- try:
174
- from openai import OpenAI
175
- client = OpenAI(api_key=API_KEY, base_url=BASE_URL)
176
- drift_note = ""
177
- if task_id == "task4_data_drift":
178
- drift_note = f"\nSTREAM ROW COUNT: {obs.get('row_count',{}).get('stream','?')}"
179
- prompt = (
180
- f"Task: {obs['task_id']}\n"
181
- f"Step: {obs['step_count']}/{obs['max_steps']}\n"
182
- f"Score: {obs['partial_score']:.4f}\n"
183
- f"Last message: {obs['message']}\n"
184
- f"Schema errors: {obs['schema_errors'][:5]}\n"
185
- f"Column dtypes: {json.dumps(obs['column_dtypes'])}\n"
186
- f"Null counts: {json.dumps(obs['null_counts'])}\n"
187
- f"Duplicate counts: {obs['duplicate_count']}\n"
188
- f"Row counts: {obs['row_count']}\n"
189
- f"Available ops: {obs['available_operations']}"
190
- f"{drift_note}\n\nNext action JSON:"
191
- )
192
- response = client.chat.completions.create(
193
- model=MODEL,
194
- messages=[
195
- {"role": "system", "content": SYSTEM_PROMPT},
196
- {"role": "user", "content": prompt},
197
- ],
198
- temperature=0.0,
199
- max_tokens=300,
200
- )
201
- raw = response.choices[0].message.content.strip()
202
- if "```" in raw:
203
- raw = raw.split("```")[1]
204
- if raw.startswith("json"):
205
- raw = raw[4:]
206
- return json.loads(raw)
207
  except Exception:
208
- return None
209
 
210
 
211
- # ── Required structured output ────────────────────────────────────────────────
212
 
213
  def log_start(task_id: str):
214
  print(f"[START] task={task_id}", flush=True)
215
 
216
-
217
  def log_step(step: int, action: str, reward: float, done: bool, error=None):
218
  error_val = error if error else "null"
219
- print(
220
- f"[STEP] step={step} action={action} reward={reward:.4f} "
221
- f"done={str(done).lower()} error={error_val}",
222
- flush=True,
223
- )
224
-
225
 
226
  def log_end(task_id: str, score: float, steps: int, success: bool):
227
- # Triple-clamp: score must be STRICTLY in (0, 1), never 0.0 or 1.0
228
- clipped = round(min(0.9990, max(0.0010, float(score))), 4)
229
- print(
230
- f"[END] task={task_id} score={clipped:.4f} steps={steps} "
231
- f"success={str(success).lower()}",
232
- flush=True,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  )
234
 
235
 
236
- # ── Server readiness check ────────────────────────────────────────────────────
237
-
238
- def _wait_for_server(timeout: int = 90) -> bool:
239
- """
240
- Poll /health until the server responds or timeout is reached.
241
- Handles the case where the HF Space is still waking up.
242
- """
243
- deadline = time.time() + timeout
244
- while time.time() < deadline:
245
- try:
246
- r = requests.get(f"{ENV_URL}/health", timeout=10)
247
- if r.status_code == 200:
248
- data = r.json()
249
- # Make sure it's actually our FastAPI JSON, not HF HTML
250
- if isinstance(data, dict) and "status" in data:
251
- print(f"[INFO] Server ready: {data}", flush=True)
252
- return True
253
- except Exception:
254
- pass
255
- time.sleep(5)
256
- return False
257
-
258
-
259
  # ── Episode runner ────────────────────────────────────────────────────────────
260
 
261
  def run_episode(task_id: str, seed: int = 42) -> Tuple[str, float, float]:
262
- """
263
- Run one full episode for task_id.
264
- Uses LLM if available, falls back to deterministic rule-based agent.
265
- ALWAYS prints [START], [STEP]s, and [END] β€” never exits early.
266
- Returns (task_id, final_score, elapsed_seconds).
267
- """
268
  session_id = f"inference_{task_id}_{seed}"
 
269
  t0 = time.time()
270
  max_steps = TASK_MAX_STEPS[task_id]
271
  step_num = 0
272
- score = 0.0010 # safe floor β€” never 0.0
273
 
274
  log_start(task_id)
275
 
276
- # ── Reset ─────────────────────────────────────────────────────────────────
277
- obs = None
278
- done = False
279
  try:
280
  resp = requests.post(
281
  f"{ENV_URL}/reset",
@@ -285,41 +116,45 @@ def run_episode(task_id: str, seed: int = 42) -> Tuple[str, float, float]:
285
  resp.raise_for_status()
286
  obs = resp.json()
287
  done = obs.get("done", False)
288
- raw_score = float(obs.get("partial_score", 0.0010))
289
- score = round(min(0.9990, max(0.0010, raw_score)), 4)
290
  except Exception as e:
291
- print(f"[INFO] Reset failed for {task_id}: {e}", flush=True)
292
- log_end(task_id, score, 0, False)
293
- return task_id, score, round(time.time() - t0, 2)
294
-
295
- # ── Rule-based strategy index ─────────────────────────────────────────────
296
- rule_actions = RULE_STRATEGIES.get(task_id, [{"operation": "submit"}])
297
- rule_idx = 0
298
 
299
- # ── Episode loop ──────────────────────────────────────────────────────────
300
  for step_num in range(1, max_steps + 1):
301
  if done:
302
  break
303
 
304
- # Decide action: try LLM first, fall back to rule
305
- action = None
 
 
 
306
  action_str = "submit"
307
- error_msg = None
308
-
309
- if obs is not None:
310
- action = _llm_action(obs, task_id)
311
-
312
- if action is None:
313
- # Rule-based fallback
314
- if rule_idx < len(rule_actions):
315
- action = rule_actions[rule_idx]
316
- rule_idx += 1
317
- else:
318
- action = {"operation": "submit"}
319
-
320
- action_str = action.get("operation", "submit")
321
-
322
- # ── Execute step ───────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
323
  reward = 0.0
324
  try:
325
  step_resp = requests.post(
@@ -332,50 +167,45 @@ def run_episode(task_id: str, seed: int = 42) -> Tuple[str, float, float]:
332
  obs = data["observation"]
333
  done = data["done"]
334
  reward = float(data.get("reward", 0.0))
335
- raw_s = float(obs.get("partial_score", 0.0010))
336
- score = round(min(0.9990, max(0.0010, raw_s)), 4)
 
337
  except Exception as e:
338
  error_msg = str(e)[:80]
339
  done = True
340
 
341
  log_step(step_num, action_str, reward, done, error_msg)
342
- time.sleep(0.2)
343
 
344
- # Guard: step_num stays 0 only if loop body never ran (done=True on reset)
 
345
  if step_num == 0:
346
  step_num = 1
347
 
348
- # Final score: triple-clamped to strictly (0, 1)
349
- score = round(min(0.9990, max(0.0010, score)), 4)
350
  success = score >= 0.5
351
  log_end(task_id, score, step_num, success)
352
  return task_id, score, round(time.time() - t0, 2)
353
 
354
 
355
- # ── Main ──────────────────────────────────────────────────────────────────────
356
 
357
  def main():
358
- # Server readiness β€” wait up to 90s for HF Space to wake
359
- if not _wait_for_server(timeout=90):
360
- # Even if server unreachable, we MUST print [END] for all tasks
361
- # so the validator doesn't see score=0.0 (which is out of range)
362
- print("[ERROR] Server unreachable β€” printing safe fallback [END] lines", flush=True)
363
- for task_id in TASK_MAX_STEPS:
364
- log_start(task_id)
365
- log_end(task_id, 0.0010, 0, False)
366
- scores = {t: 0.0010 for t in TASK_MAX_STEPS}
367
- mean = round(sum(scores.values()) / len(scores), 4)
368
- print(json.dumps({**scores, "mean": mean}, indent=2), flush=True)
369
- return
370
-
371
- # Test LLM once (caches result globally)
372
- _test_llm()
373
 
374
  tasks = list(TASK_MAX_STEPS.keys())
375
  scores: Dict[str, float] = {}
376
  elapsed: Dict[str, float] = {}
377
 
378
- # All 4 tasks run in parallel β€” ThreadPoolExecutor, each with own session_id
379
  with ThreadPoolExecutor(max_workers=len(tasks)) as pool:
380
  futures = {
381
  pool.submit(run_episode, task_id, 42): task_id
@@ -385,22 +215,17 @@ def main():
385
  task_id = futures[future]
386
  try:
387
  tid, score, secs = future.result()
388
- scores[tid] = round(min(0.9990, max(0.0010, score)), 4)
389
  elapsed[tid] = secs
390
  except Exception as exc:
391
- print(f"[INFO] Task {task_id} raised exception: {exc}", flush=True)
392
- # Even on exception, print [END] with safe score
393
- log_end(task_id, 0.0010, 0, False)
394
- scores[task_id] = 0.0010
395
  elapsed[task_id] = -1.0
 
396
 
397
- mean = round(
398
- sum(scores.values()) / len(scores), 4
399
- ) if scores else 0.0010
400
-
401
- summary = {**scores, "mean": mean, "elapsed_seconds": elapsed}
402
- print(json.dumps(summary, indent=2), flush=True)
403
 
404
 
405
  if __name__ == "__main__":
406
- main()
 
1
  """
2
  DataClean OpenEnv β€” inference.py
3
  Required output format: [START] / [STEP] / [END] structured blocks.
4
+ Uses: HF_TOKEN, API_BASE_URL, MODEL_NAME env vars.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  """
6
  import os, json, time, sys
7
  import requests
8
+ from openai import OpenAI
9
  from concurrent.futures import ThreadPoolExecutor, as_completed
10
+ from typing import Dict, Tuple
11
 
12
+ ENV_URL = os.environ.get("ENV_URL", "http://localhost:7860")
13
+ API_KEY = os.environ.get("HF_TOKEN", "")
14
+ BASE_URL = os.environ.get("API_BASE_URL", "https://api.openai.com/v1")
15
+ MODEL = os.environ.get("MODEL_NAME", "gpt-4o-mini")
16
 
17
  TASK_MAX_STEPS = {
18
+ "task1": 10,
19
+ "task2": 20,
20
+ "task3": 30,
21
  "task4_data_drift": 40,
22
  }
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  SYSTEM_PROMPT = """You are an expert data cleaning agent. Respond ONLY with valid JSON β€” no prose, no markdown.
25
 
26
  Operations:
 
40
  task4_data_drift: filter_outliers(amount,iqr,1.5,stream)->fill_nulls(amount,mean,stream)->cast_column(amount,float,stream)->fill_nulls(category,mode,stream)->fill_nulls(region,mode,stream)->cast_column(event_ts,datetime,stream)->submit
41
  """
42
 
 
43
 
44
+ # ── Score contract helper ─────────────────────────────────────────────────────
45
 
46
+ def _clamp_score(v) -> float:
47
+ """Enforce 0.001–0.999 at the inference layer as a last-resort safety net."""
 
 
 
 
 
 
 
48
  try:
49
+ f = float(v)
50
+ if not (f == f): # NaN check
51
+ f = 0.001
52
+ return round(min(0.999, max(0.001, f)), 4)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  except Exception:
54
+ return 0.001
55
 
56
 
57
+ # ── Required structured output helpers ───────────────────────────────────────
58
 
59
  def log_start(task_id: str):
60
  print(f"[START] task={task_id}", flush=True)
61
 
 
62
  def log_step(step: int, action: str, reward: float, done: bool, error=None):
63
  error_val = error if error else "null"
64
+ print(f"[STEP] step={step} action={action} reward={reward:.4f} done={str(done).lower()} error={error_val}", flush=True)
 
 
 
 
 
65
 
66
  def log_end(task_id: str, score: float, steps: int, success: bool):
67
+ clipped = _clamp_score(score)
68
+ print(f"[END] task={task_id} score={clipped:.4f} steps={steps} success={str(success).lower()}", flush=True)
69
+
70
+
71
+ # ── LLM client ───────────────────────────────────────────────────────────────
72
+
73
+ def _make_client():
74
+ return OpenAI(api_key=API_KEY, base_url=BASE_URL)
75
+
76
+
77
+ def _build_prompt(obs: dict, task_id: str) -> str:
78
+ drift_note = ""
79
+ if task_id == "task4_data_drift":
80
+ drift_note = f"\nSTREAM ROW COUNT: {obs.get('row_count',{}).get('stream','?')}"
81
+ return (
82
+ f"Task: {obs['task_id']}\n"
83
+ f"Step: {obs['step_count']}/{obs['max_steps']}\n"
84
+ f"Score: {obs['partial_score']:.4f}\n"
85
+ f"Last message: {obs['message']}\n"
86
+ f"Schema errors: {obs['schema_errors'][:5]}\n"
87
+ f"Column dtypes: {json.dumps(obs['column_dtypes'])}\n"
88
+ f"Null counts: {json.dumps(obs['null_counts'])}\n"
89
+ f"Duplicate counts: {obs['duplicate_count']}\n"
90
+ f"Row counts: {obs['row_count']}\n"
91
+ f"Available ops: {obs['available_operations']}"
92
+ f"{drift_note}\n\nNext action JSON:"
93
  )
94
 
95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  # ── Episode runner ────────────────────────────────────────────────────────────
97
 
98
  def run_episode(task_id: str, seed: int = 42) -> Tuple[str, float, float]:
99
+ # FIX: unique session_id per task+seed prevents collision in parallel runs
 
 
 
 
 
100
  session_id = f"inference_{task_id}_{seed}"
101
+ client = _make_client()
102
  t0 = time.time()
103
  max_steps = TASK_MAX_STEPS[task_id]
104
  step_num = 0
105
+ score = 0.001
106
 
107
  log_start(task_id)
108
 
109
+ # Reset
 
 
110
  try:
111
  resp = requests.post(
112
  f"{ENV_URL}/reset",
 
116
  resp.raise_for_status()
117
  obs = resp.json()
118
  done = obs.get("done", False)
 
 
119
  except Exception as e:
120
+ log_end(task_id, 0.001, 0, False)
121
+ return task_id, 0.001, 0.0
 
 
 
 
 
122
 
123
+ # Episode loop
124
  for step_num in range(1, max_steps + 1):
125
  if done:
126
  break
127
 
128
+ # FIX: initialise error_msg BEFORE the LLM block so a stale error
129
+ # from a previous LLM failure is never logged against a clean step.
130
+ error_msg = None
131
+
132
+ # Get action from LLM
133
  action_str = "submit"
134
+ try:
135
+ prompt = _build_prompt(obs, task_id)
136
+ response = client.chat.completions.create(
137
+ model=MODEL,
138
+ messages=[
139
+ {"role": "system", "content": SYSTEM_PROMPT},
140
+ {"role": "user", "content": prompt},
141
+ ],
142
+ temperature=0.0,
143
+ max_tokens=300,
144
+ )
145
+ raw = response.choices[0].message.content.strip()
146
+ if "```" in raw:
147
+ raw = raw.split("```")[1]
148
+ if raw.startswith("json"):
149
+ raw = raw[4:]
150
+ action = json.loads(raw)
151
+ action_str = action.get("operation", "submit")
152
+ except Exception as e:
153
+ action = {"operation": "submit"}
154
+ action_str = "submit"
155
+ error_msg = str(e)[:80]
156
+
157
+ # Execute step
158
  reward = 0.0
159
  try:
160
  step_resp = requests.post(
 
167
  obs = data["observation"]
168
  done = data["done"]
169
  reward = float(data.get("reward", 0.0))
170
+ # FIX: clamp at read time β€” server could theoretically send 0.0 or
171
+ # 1.0 if it's running old code; _clamp_score enforces the contract.
172
+ score = _clamp_score(obs.get("partial_score", 0.001))
173
  except Exception as e:
174
  error_msg = str(e)[:80]
175
  done = True
176
 
177
  log_step(step_num, action_str, reward, done, error_msg)
178
+ time.sleep(0.3)
179
 
180
+ # FIX: if done=True on first check, step_num stays at its range() init
181
+ # value; clamp to 1 so log_end never reports 0 steps for a started episode.
182
  if step_num == 0:
183
  step_num = 1
184
 
185
+ score = _clamp_score(score)
 
186
  success = score >= 0.5
187
  log_end(task_id, score, step_num, success)
188
  return task_id, score, round(time.time() - t0, 2)
189
 
190
 
191
+ # ── Main β€” parallel across all tasks ─────────────────────────────────────────
192
 
193
  def main():
194
+ if not API_KEY:
195
+ print("[ERROR] HF_TOKEN not set", flush=True)
196
+ sys.exit(1)
197
+
198
+ try:
199
+ h = requests.get(f"{ENV_URL}/health", timeout=15)
200
+ print(f"[INFO] Server: {h.json()}", flush=True)
201
+ except Exception as e:
202
+ print(f"[ERROR] Cannot reach server at {ENV_URL}: {e}", flush=True)
203
+ sys.exit(1)
 
 
 
 
 
204
 
205
  tasks = list(TASK_MAX_STEPS.keys())
206
  scores: Dict[str, float] = {}
207
  elapsed: Dict[str, float] = {}
208
 
 
209
  with ThreadPoolExecutor(max_workers=len(tasks)) as pool:
210
  futures = {
211
  pool.submit(run_episode, task_id, 42): task_id
 
215
  task_id = futures[future]
216
  try:
217
  tid, score, secs = future.result()
218
+ scores[tid] = _clamp_score(score) # FIX: clamp final scores too
219
  elapsed[tid] = secs
220
  except Exception as exc:
221
+ scores[task_id] = 0.001
 
 
 
222
  elapsed[task_id] = -1.0
223
+ log_end(task_id, 0.001, 0, False)
224
 
225
+ mean = round(sum(scores.values()) / len(scores), 4) if scores else 0.001
226
+ # FIX: include elapsed_seconds in summary for diagnostics
227
+ print(json.dumps({**scores, "mean": mean, "elapsed_seconds": elapsed}, indent=2), flush=True)
 
 
 
228
 
229
 
230
  if __name__ == "__main__":
231
+ main()