aarxshi commited on
Commit
e68e1a6
·
1 Parent(s): cc4bef4

Fixing llm error

Browse files
Files changed (2) hide show
  1. Dockerfile +1 -1
  2. inference.py +145 -341
Dockerfile CHANGED
@@ -13,7 +13,7 @@ COPY requirements.txt .
13
 
14
  # install Python dependencies
15
  RUN pip install --no-cache-dir -r requirements.txt
16
-
17
  # copy all project files
18
  COPY . .
19
 
 
13
 
14
  # install Python dependencies
15
  RUN pip install --no-cache-dir -r requirements.txt
16
+ RUN pip install openenv-core
17
  # copy all project files
18
  COPY . .
19
 
inference.py CHANGED
@@ -1,28 +1,4 @@
1
  # -*- coding: utf-8 -*-
2
- """inference.ipynb
3
-
4
- Automatically generated by Colab.
5
-
6
- Original file is located at
7
- https://colab.research.google.com/drive/11pvVSX9XeHQWW2ouk6VfcA5SdNLaVyPk
8
- """
9
-
10
- # ============================================================
11
- # inference.py — LLM Agent Loop for DistTrainEnv
12
- #
13
- # MANDATORY REQUIREMENTS MET:
14
- # - Reads API_BASE_URL, MODEL_NAME, HF_TOKEN from env vars
15
- # - Uses OpenAI client for all LLM calls
16
- # - Emits strict [START] [STEP] [END] stdout format
17
- # - Runs all 3 tasks and produces scores in [0, 1]
18
- # - Named inference.py in root directory
19
- # - Runs within 20 min on 2 vCPU / 8GB RAM
20
- #
21
- # Usage:
22
- # HF_TOKEN=xxx python inference.py
23
- # HF_TOKEN=xxx python inference.py --task easy
24
- # python inference.py --dry-run
25
- # ============================================================
26
 
27
  import os
28
  import sys
@@ -38,431 +14,259 @@ from environment.env import DistTrainEnv
38
  from environment.models import Action
39
 
40
  # ----------------------------------------------------------
41
- # Environment variables — mandatory per hackathon spec
42
- # ----------------------------------------------------------
43
- API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY")
44
- API_BASE_URL = os.getenv("API_BASE_URL")
45
- MODEL_NAME = os.getenv("MODEL_NAME", "llama-3.3-70b-versatile")
46
- ENV_NAME = "dist-train-env"
47
 
48
- # ----------------------------------------------------------
49
- # Episode config
50
- # Kept within 20 min inference timeout:
51
- # 3 tasks x 15 steps x ~15s per LLM call = ~11 min worst case
52
  # ----------------------------------------------------------
53
  MAX_STEPS_PER_TASK = 15
54
  TASKS = ["easy", "medium", "hard"]
55
  SUCCESS_THRESHOLD = 0.5
56
 
57
  # ----------------------------------------------------------
58
- # System prompt
59
- # Tells the LLM agent what the environment is,
60
- # what actions it can take, and how to respond.
61
- # Compact to save tokens and stay within 20 min timeout.
62
- # ----------------------------------------------------------
63
- SYSTEM_PROMPT = """You are an AI operations agent managing a distributed ML training cluster.
64
- The cluster uses ring all-reduce to synchronize gradients across 8 worker nodes (node_0 to node_7).
65
-
66
- Each step you receive a JSON observation and must return a JSON action.
67
- Your goal: detect faults, recover the cluster, and keep training healthy.
68
-
69
- AVAILABLE ACTIONS:
70
- - restart_node: bring a crashed node back online (requires target_node)
71
- - remove_from_ring: remove a slow/faulty node from the all-reduce ring (requires target_node)
72
- - reduce_batch: reduce batch size on a node showing memory pressure (requires target_node)
73
- - checkpoint: save training checkpoint (use sparingly - halves throughput this step)
74
- - inspect: get detailed diagnostics on a node (requires target_node)
75
- - noop: do nothing this step
76
 
77
- RESPONSE FORMAT (JSON only, no explanation):
78
  {"action_type": "<action>", "target_node": "<node_id or null>"}
79
 
80
- KEY SIGNALS TO WATCH:
81
- - node status: healthy / slow / oom / crashed
82
- - node memory: climbing above 0.75 = early OOM warning
83
- - gradient_staleness: above 0.3 = training degrading
84
- - loss vs expected_loss: diverging = serious problem
85
- - alerts: read these carefully - they name problematic nodes
86
-
87
- STRATEGY:
88
- - Crashed node -> restart_node immediately
89
- - Slow node with high latency -> remove_from_ring
90
- - Node with climbing memory -> reduce_batch EARLY (before it crashes)
91
- - If you see a symptom (slow node) AND another node has high memory, fix the memory node first
92
- - Do NOT restart healthy nodes"""
93
 
 
 
 
 
 
 
 
 
94
 
95
  # ----------------------------------------------------------
96
- # Logging functions
97
- # STRICT FORMAT — any deviation breaks evaluation scoring
98
  # ----------------------------------------------------------
99
 
100
- def log_start(task: str, env: str, model: str) -> None:
101
- """Emit [START] line. Called once per episode."""
102
  print(f"[START] task={task} env={env} model={model}", flush=True)
103
 
104
-
105
- def log_step(step: int, action: str, reward: float,
106
- done: bool, error: Optional[str]) -> None:
107
- """
108
- Emit [STEP] line. Called after every env.step().
109
- - reward formatted to 2 decimal places
110
- - done is lowercase boolean: true or false
111
- - error is raw error string or null
112
- """
113
  error_val = error if error else "null"
114
- print(
115
- f"[STEP] step={step} action={action} "
116
- f"reward={reward:.2f} done={str(done).lower()} "
117
- f"error={error_val}",
118
- flush=True
119
- )
120
-
121
-
122
- def log_end(success: bool, steps: int,
123
- score: float, rewards: List[float]) -> None:
124
- """
125
- Emit [END] line. Always called, even on exception.
126
- - success is lowercase boolean
127
- - score formatted to 2 decimal places
128
- - rewards is comma-separated list of 2dp floats
129
- Field order: success, steps, score, rewards
130
- """
131
- rewards_str = ",".join(f"{r:.2f}" for r in rewards)
132
- print(
133
- f"[END] success={str(success).lower()} "
134
- f"steps={steps} "
135
- f"score={score:.2f} "
136
- f"rewards={rewards_str}",
137
- flush=True
138
- )
139
-
140
-
141
- # ----------------------------------------------------------
142
- # Observation formatting
143
- # Converts Observation model to compact LLM-readable prompt
144
- # ----------------------------------------------------------
145
-
146
- def observation_to_prompt(obs_dict: dict, step: int, task_id: str) -> str:
147
- """
148
- Format observation as a compact JSON prompt for the LLM.
149
- Healthy nodes with normal memory are summarized briefly
150
- to keep prompt size small and LLM calls fast.
151
- """
152
- nodes_summary = []
153
- for node in obs_dict.get("nodes", []):
154
- if node["status"] != "healthy" or node["memory"] > 0.65:
155
- # Include full detail for suspicious nodes
156
- nodes_summary.append(node)
157
- else:
158
- # Summarize healthy nodes briefly
159
- nodes_summary.append({
160
- "id": node["id"],
161
- "status": "healthy",
162
- "memory": round(node["memory"], 2),
163
- "throughput": round(node["throughput"], 2),
164
- })
165
-
166
- job = obs_dict.get("job", {})
167
- compact_obs = {
168
- "step": step,
169
- "task": task_id,
170
- "nodes": nodes_summary,
171
- "job": {
172
- "loss": round(job.get("loss", 0), 4),
173
- "expected_loss": round(job.get("expected_loss", 0), 4),
174
- "cluster_throughput": round(job.get("cluster_throughput", 0), 3),
175
- "gradient_staleness": round(job.get("gradient_staleness", 0), 3),
176
- "loss_diverging": job.get("loss_diverging", False),
177
- },
178
- "ring_order": obs_dict.get("ring_order", []),
179
- "alerts": obs_dict.get("alerts", []),
180
- }
181
- return f"Observation (step {step}):\n{json.dumps(compact_obs, indent=2)}"
182
 
 
 
 
183
 
184
- # ----------------------------------------------------------
185
- # Action parsing
186
  # ----------------------------------------------------------
187
 
188
  def parse_action(response_text: str) -> Action:
189
- """
190
- Parse LLM JSON response into an Action.
191
- Strips markdown code fences if present.
192
- Falls back to noop on any parse error —
193
- ensures inference never crashes on bad LLM output.
194
- """
195
  try:
196
  text = response_text.strip()
197
- # Strip markdown code fences if LLM wraps response
198
  if text.startswith("```"):
199
  lines = text.split("\n")
200
- text = "\n".join(
201
- lines[1:-1] if lines[-1].strip() == "```" else lines[1:]
202
- )
203
- data = json.loads(text.strip())
204
  return Action(
205
  action_type=data.get("action_type", "noop"),
206
  target_node=data.get("target_node"),
207
- parameters=data.get("parameters"),
208
  )
209
  except Exception:
210
  return Action(action_type="noop")
211
 
212
-
213
  def action_to_str(action: Action) -> str:
214
- """Compact action string for [STEP] log line."""
215
  if action.target_node:
216
  return f"{action.action_type}('{action.target_node}')"
217
  return action.action_type
218
 
219
-
220
- # ----------------------------------------------------------
221
- # Rule-based fallback agent
222
- # Used in --dry-run mode (no LLM needed)
223
- # Also serves as a sanity check that the env works
224
  # ----------------------------------------------------------
225
 
226
- def _rule_based_action(obs_dict: dict) -> Action:
227
- """
228
- Simple deterministic agent for dry runs.
229
- Priority: crashed > OOM memory > slow straggler > noop
230
- """
231
  nodes = obs_dict.get("nodes", [])
 
232
 
233
- # Priority 1: restart any crashed node immediately
234
  for node in nodes:
235
  if node["status"] == "crashed":
236
- return Action(
237
- action_type="restart_node",
238
- target_node=node["id"]
239
- )
240
 
241
- # Priority 2: reduce batch on node with climbing memory
242
- # (early intervention before OOM crash)
243
  for node in nodes:
244
- if node["memory"] > 0.75 and node["status"] in ["slow", "oom"]:
245
- return Action(
246
- action_type="reduce_batch",
247
- target_node=node["id"]
248
- )
249
 
250
- # Priority 3: remove straggler from ring
251
  for node in nodes:
252
- if node["status"] == "slow" and node.get("in_ring", True):
253
- return Action(
254
- action_type="remove_from_ring",
255
- target_node=node["id"]
256
- )
257
 
258
- return Action(action_type="noop")
 
 
 
259
 
 
260
 
261
- # ----------------------------------------------------------
262
- # Grader — computes final episode score
263
- # ----------------------------------------------------------
 
 
264
 
265
- def compute_score(task_id: str, episode_history: list,
266
- rewards: List[float]) -> float:
267
- """
268
- Compute final score for an episode in [0, 1].
269
 
270
- Tries to import graders if available.
271
- Falls back to reward-based scoring if graders not yet written.
272
- This ensures inference.py never crashes even before
273
- Person C writes the grader files.
274
- """
275
- try:
276
- from graders.run_graders import run_grader
277
- return run_grader(task_id, episode_history)
278
- except ImportError:
279
- # Grader files not written yet — use reward average as proxy
280
- if not rewards:
281
- return 0.0
282
- avg = sum(rewards) / len(rewards)
283
- # Normalize: rewards typically range from 0.5 to 1.2
284
- # clamp to [0, 1]
285
- return min(1.0, max(0.0, (avg - 0.4) / 0.8))
286
- except Exception:
287
  return 0.0
 
 
288
 
289
-
290
- # ----------------------------------------------------------
291
- # Single task episode runner
292
  # ----------------------------------------------------------
293
 
294
- def run_task(client: Optional[OpenAI], task_id: str,
295
- dry_run: bool = False) -> dict:
296
- """
297
- Run one full episode for a given task.
298
- Emits [START], [STEP]x, [END] logs.
299
-
300
- Args:
301
- client: OpenAI client (None if dry_run)
302
- task_id: "easy" | "medium" | "hard"
303
- dry_run: use rule-based agent instead of LLM
304
-
305
- Returns:
306
- dict with episode history and final score
307
- """
308
  env = DistTrainEnv(task_id=task_id)
309
- obs = env.reset(task_id=task_id)
310
- obs_dict = obs.model_dump()
311
-
312
- rewards: List[float] = []
313
- episode_history = []
314
-
315
- # Conversation history for multi-turn LLM context
316
- messages = [{"role": "system", "content": SYSTEM_PROMPT}]
317
-
318
- log_start(task=task_id, env=ENV_NAME, model=MODEL_NAME)
319
 
 
 
320
  score = 0.0
321
  success = False
322
 
 
 
323
  try:
324
- for step_num in range(1, MAX_STEPS_PER_TASK + 1):
 
325
 
326
- # Format observation as LLM prompt
327
- user_content = observation_to_prompt(
328
- obs_dict, step_num, task_id)
329
- messages.append({"role": "user", "content": user_content})
330
 
 
331
  error_str = None
332
-
333
- # Get action from LLM or rule-based agent
 
 
 
334
  if dry_run:
335
  action = _rule_based_action(obs_dict)
336
  else:
337
- try:
338
- response = client.chat.completions.create(
339
- model=MODEL_NAME,
340
- messages=messages,
341
- max_tokens=100, # keep responses short
342
- temperature=0.0, # deterministic for reproducibility
343
- )
344
- response_text = response.choices[0].message.content
345
- # Add assistant response to conversation history
346
- messages.append({
347
- "role": "assistant",
348
- "content": response_text
349
- })
350
- action = parse_action(response_text)
351
- except Exception as e:
352
- error_str = str(e)[:80]
353
- action = Action(action_type="noop")
354
-
355
- # Step the environment
 
 
 
 
 
 
 
 
 
356
  result = env.step(action)
357
- new_obs_dict = result.observation.model_dump()
 
358
  reward = result.reward.value
359
  done = result.done
360
 
361
  rewards.append(reward)
 
362
 
363
- # Emit [STEP] log — mandatory format
364
- log_step(
365
- step=step_num,
366
- action=action_to_str(action),
367
- reward=reward,
368
- done=done,
369
- error=error_str
370
- )
371
-
372
- # Store full step data for grader
373
- episode_history.append({
374
- "step": step_num,
375
- "observation": obs_dict,
376
- "action": action.model_dump(),
377
- "reward": result.reward.model_dump(),
378
- "done": done,
379
- })
380
-
381
- obs_dict = new_obs_dict
382
 
383
  if done:
384
  break
385
 
386
- finally:
387
- # [END] must always be emitted — even if exception occurs
388
- score = compute_score(task_id, episode_history, rewards)
389
  success = score >= SUCCESS_THRESHOLD
390
- log_end(
391
- success=success,
392
- steps=len(episode_history),
393
- score=score,
394
- rewards=rewards
395
- )
396
 
397
- return {"history": episode_history, "score": score}
 
398
 
 
 
 
 
 
 
 
 
 
399
 
400
- # ----------------------------------------------------------
401
- # Main — runs all tasks and prints final summary
402
  # ----------------------------------------------------------
403
 
404
  def main():
405
- parser = argparse.ArgumentParser(
406
- description="DistTrainEnv inference agent"
407
- )
408
- parser.add_argument(
409
- "--task",
410
- choices=["easy", "medium", "hard", "all"],
411
- default="all",
412
- )
413
  parser.add_argument("--dry-run", action="store_true")
414
  args = parser.parse_args()
415
 
416
  dry_run = args.dry_run
417
  client = None
418
 
419
- # ✅ SAFE OpenAI initialization (this was your crash)
420
  if not dry_run:
421
- if not API_KEY:
422
- print("WARNING: HF_TOKEN not set. Falling back to dry-run.", flush=True)
423
- dry_run = True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
424
  else:
425
- try:
426
- client = OpenAI(
427
- api_key=API_KEY,
428
- base_url=API_BASE_URL,
429
- timeout=60.0,
430
- )
431
- except Exception as e:
432
- print(f"WARNING: OpenAI init failed: {e}. Falling back to dry-run.", flush=True)
433
- dry_run = True
434
- client = None
435
 
436
  tasks_to_run = TASKS if args.task == "all" else [args.task]
437
- all_scores = {}
438
 
439
  for task_id in tasks_to_run:
440
  try:
441
- result = run_task(client, task_id, dry_run=dry_run)
442
- all_scores[task_id] = result["score"]
443
  except Exception as e:
444
- # ✅ NEVER crash the script
445
  print(f"ERROR in task {task_id}: {e}", flush=True)
446
-
447
- # MUST emit END or evaluator fails
448
  log_end(success=False, steps=0, score=0.0, rewards=[])
 
449
 
450
- all_scores[task_id] = 0.0
451
-
452
- # Final summary (safe)
453
- print(f"\n{'='*55}", flush=True)
454
  print("FINAL SCORES", flush=True)
455
- print(f"{'='*55}", flush=True)
456
- for task_id, score in all_scores.items():
457
- print(f" {task_id:8s}: {score:.4f}", flush=True)
 
458
 
459
  weights = {"easy": 0.2, "medium": 0.3, "hard": 0.5}
460
- weighted = sum(
461
- all_scores.get(t, 0.0) * weights[t]
462
- for t in tasks_to_run
463
- )
464
- print(f" {'WEIGHTED':8s}: {weighted:.4f}", flush=True)
465
 
 
 
 
466
 
467
  if __name__ == "__main__":
468
  main()
 
1
  # -*- coding: utf-8 -*-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  import os
4
  import sys
 
14
  from environment.models import Action
15
 
16
  # ----------------------------------------------------------
17
+ API_KEY = os.environ.get("API_KEY")
18
+ API_BASE_URL = os.environ.get("API_BASE_URL")
19
+ MODEL_NAME = os.environ.get("MODEL_NAME", "llama-3.3-70b-versatile")
20
+ ENV_NAME = "dist-train-env"
 
 
21
 
 
 
 
 
22
  # ----------------------------------------------------------
23
  MAX_STEPS_PER_TASK = 15
24
  TASKS = ["easy", "medium", "hard"]
25
  SUCCESS_THRESHOLD = 0.5
26
 
27
  # ----------------------------------------------------------
28
+ SYSTEM_PROMPT = """You are an AI ops agent managing a distributed ML cluster.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
+ Return ONLY JSON:
31
  {"action_type": "<action>", "target_node": "<node_id or null>"}
32
 
33
+ Valid actions:
34
+ restart_node, remove_from_ring, reduce_batch, checkpoint, inspect, noop
 
 
 
 
 
 
 
 
 
 
 
35
 
36
+ STRICT RULES - act immediately, never noop when a fault exists:
37
+ - If any node status == "crashed" → restart_node on that node
38
+ - If any node status == "oom" → reduce_batch on that node
39
+ - If any node status == "slow" or "straggler" → remove_from_ring on that node
40
+ - If any node memory > 0.75 → reduce_batch on that node
41
+ - Only use noop if ALL nodes are healthy and memory < 0.75
42
+ - Always pick the most critical node (crashed > oom > slow > high memory)
43
+ """
44
 
45
  # ----------------------------------------------------------
46
+ # LOGGING
 
47
  # ----------------------------------------------------------
48
 
49
+ def log_start(task: str, env: str, model: str):
 
50
  print(f"[START] task={task} env={env} model={model}", flush=True)
51
 
52
+ def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]):
 
 
 
 
 
 
 
 
53
  error_val = error if error else "null"
54
+ print(f"[STEP] step={step} action={action} reward={reward:.2f} done={str(done).lower()} error={error_val}", flush=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
+ def log_end(success: bool, steps: int, score: float, rewards: List[float]):
57
+ rewards_str = ",".join(f"{r:.2f}" for r in rewards)
58
+ print(f"[END] success={str(success).lower()} steps={steps} score={score:.2f} rewards={rewards_str}", flush=True)
59
 
 
 
60
  # ----------------------------------------------------------
61
 
62
  def parse_action(response_text: str) -> Action:
 
 
 
 
 
 
63
  try:
64
  text = response_text.strip()
 
65
  if text.startswith("```"):
66
  lines = text.split("\n")
67
+ text = "\n".join(lines[1:-1])
68
+ data = json.loads(text)
 
 
69
  return Action(
70
  action_type=data.get("action_type", "noop"),
71
  target_node=data.get("target_node"),
 
72
  )
73
  except Exception:
74
  return Action(action_type="noop")
75
 
 
76
  def action_to_str(action: Action) -> str:
 
77
  if action.target_node:
78
  return f"{action.action_type}('{action.target_node}')"
79
  return action.action_type
80
 
 
 
 
 
 
81
  # ----------------------------------------------------------
82
 
83
+ def _get_urgent_action(obs_dict: dict) -> Optional[Action]:
 
 
 
 
84
  nodes = obs_dict.get("nodes", [])
85
+ alerts = obs_dict.get("alerts", [])
86
 
87
+ # Priority 1: crashed
88
  for node in nodes:
89
  if node["status"] == "crashed":
90
+ return Action(action_type="restart_node", target_node=node["id"])
 
 
 
91
 
92
+ # Priority 2: oom status OR high memory (>0.4) and in ring
 
93
  for node in nodes:
94
+ if node["status"] == "oom":
95
+ return Action(action_type="reduce_batch", target_node=node["id"])
96
+ for node in nodes:
97
+ if node.get("memory", 0) > 0.4 and node.get("in_ring", False):
98
+ return Action(action_type="reduce_batch", target_node=node["id"])
99
 
100
+ # Priority 3: slow/straggler AND still in ring (don't repeat if already removed)
101
  for node in nodes:
102
+ if node["status"] in ("slow", "straggler") and node.get("in_ring", True):
103
+ return Action(action_type="remove_from_ring", target_node=node["id"])
 
 
 
104
 
105
+ # Priority 4: high memory (>0.35) as early warning
106
+ for node in nodes:
107
+ if node.get("memory", 0) > 0.35 and node.get("in_ring", False):
108
+ return Action(action_type="reduce_batch", target_node=node["id"])
109
 
110
+ return None
111
 
112
+ def _rule_based_action(obs_dict: dict) -> Action:
113
+ urgent = _get_urgent_action(obs_dict)
114
+ if urgent:
115
+ return urgent
116
+ return Action(action_type="noop")
117
 
118
+ # ----------------------------------------------------------
 
 
 
119
 
120
+ def compute_score(rewards: List[float]) -> float:
121
+ if not rewards:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  return 0.0
123
+ avg = sum(rewards) / len(rewards)
124
+ return min(1.0, max(0.0, (avg - 0.4) / 0.8))
125
 
 
 
 
126
  # ----------------------------------------------------------
127
 
128
+ def run_task(client: Optional[OpenAI], task_id: str, dry_run: bool):
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  env = DistTrainEnv(task_id=task_id)
 
 
 
 
 
 
 
 
 
 
130
 
131
+ rewards = []
132
+ steps_taken = 0
133
  score = 0.0
134
  success = False
135
 
136
+ log_start(task_id, ENV_NAME, MODEL_NAME)
137
+
138
  try:
139
+ obs = env.reset(task_id=task_id)
140
+ obs_dict = obs.model_dump()
141
 
142
+ messages = [{"role": "system", "content": SYSTEM_PROMPT}]
 
 
 
143
 
144
+ for step in range(1, MAX_STEPS_PER_TASK + 1):
145
  error_str = None
146
+
147
+
148
+
149
+
150
+ # -------- ACTION --------
151
  if dry_run:
152
  action = _rule_based_action(obs_dict)
153
  else:
154
+ # Always try rule-based first for obvious faults
155
+ urgent = _get_urgent_action(obs_dict)
156
+ if urgent:
157
+ action = urgent
158
+ else:
159
+ try:
160
+ messages.append({
161
+ "role": "user",
162
+ "content": json.dumps(obs_dict)
163
+ })
164
+
165
+ response = client.chat.completions.create(
166
+ model=MODEL_NAME,
167
+ messages=messages,
168
+ max_tokens=100,
169
+ temperature=0.0,
170
+ )
171
+
172
+ text = response.choices[0].message.content or ""
173
+ messages.append({"role": "assistant", "content": text})
174
+
175
+ action = parse_action(text)
176
+
177
+ except Exception as e:
178
+ error_str = str(e)[:80]
179
+ action = Action(action_type="noop")
180
+
181
+ # -------- ENV STEP --------
182
  result = env.step(action)
183
+
184
+ obs_dict = result.observation.model_dump()
185
  reward = result.reward.value
186
  done = result.done
187
 
188
  rewards.append(reward)
189
+ steps_taken = step
190
 
191
+ log_step(step, action_to_str(action), reward, done, error_str)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
 
193
  if done:
194
  break
195
 
196
+ score = compute_score(rewards)
 
 
197
  success = score >= SUCCESS_THRESHOLD
 
 
 
 
 
 
198
 
199
+ except Exception as e:
200
+ print(f"ERROR: {str(e)[:100]}", flush=True)
201
 
202
+ finally:
203
+ try:
204
+ env.close()
205
+ except Exception:
206
+ pass
207
+
208
+ log_end(success, steps_taken, score, rewards)
209
+
210
+ return {"score": score}
211
 
 
 
212
  # ----------------------------------------------------------
213
 
214
  def main():
215
+ parser = argparse.ArgumentParser()
216
+ parser.add_argument("--task", choices=["easy", "medium", "hard", "all"], default="all")
 
 
 
 
 
 
217
  parser.add_argument("--dry-run", action="store_true")
218
  args = parser.parse_args()
219
 
220
  dry_run = args.dry_run
221
  client = None
222
 
 
223
  if not dry_run:
224
+ api_key = os.environ.get("API_KEY")
225
+ api_base = os.environ.get("API_BASE_URL")
226
+
227
+ if api_base and api_key:
228
+ # ✅ Eval environment — use injected proxy (required for submission)
229
+ client = OpenAI(
230
+ api_key=api_key,
231
+ base_url=api_base,
232
+ timeout=60.0,
233
+ )
234
+ elif api_key:
235
+ # Local testing with Groq
236
+ client = OpenAI(
237
+ api_key=api_key,
238
+ base_url="https://api.groq.com/openai/v1",
239
+ timeout=60.0,
240
+ )
241
  else:
242
+ print("WARNING: No API credentials. Falling back to dry-run.", flush=True)
243
+ dry_run = True
 
 
 
 
 
 
 
 
244
 
245
  tasks_to_run = TASKS if args.task == "all" else [args.task]
246
+ scores = {}
247
 
248
  for task_id in tasks_to_run:
249
  try:
250
+ result = run_task(client, task_id, dry_run)
251
+ scores[task_id] = result["score"]
252
  except Exception as e:
 
253
  print(f"ERROR in task {task_id}: {e}", flush=True)
 
 
254
  log_end(success=False, steps=0, score=0.0, rewards=[])
255
+ scores[task_id] = 0.0
256
 
257
+ print("\n=======================================================", flush=True)
 
 
 
258
  print("FINAL SCORES", flush=True)
259
+ print("=======================================================", flush=True)
260
+
261
+ for t, s in scores.items():
262
+ print(f"{t:8s}: {s:.4f}", flush=True)
263
 
264
  weights = {"easy": 0.2, "medium": 0.3, "hard": 0.5}
265
+ total = sum(scores.get(t, 0.0) * weights[t] for t in tasks_to_run)
 
 
 
 
266
 
267
+ print(f"{'WEIGHTED':8s}: {total:.4f}", flush=True)
268
+
269
+ # ----------------------------------------------------------
270
 
271
  if __name__ == "__main__":
272
  main()