ragavrida commited on
Commit
aa30973
Β·
1 Parent(s): 11119a7

Fix inference.py log format to match official spec

Browse files

- Changed from JSON format to key=value pairs
- [START] task=<name> env=<benchmark> model=<model>
- [STEP] step=<n> action=<str> reward=<0.00> done=<bool> error=<msg|null>
- [END] success=<bool> steps=<n> score=<0.000> rewards=<r1,r2,...>
- Uses async pattern with from_docker_image()
- Updated README to match

Files changed (2) hide show
  1. README.md +3 -3
  2. inference.py +247 -380
README.md CHANGED
@@ -432,19 +432,19 @@ The inference script emits structured stdout logs in the **mandatory** `[START]`
432
  ### `[START]` β€” emitted once per task
433
 
434
  ```
435
- [START] {"task_id": "easy", "task_description": "Classify PR severity (none/low/medium/high/critical)"}
436
  ```
437
 
438
  ### `[STEP]` β€” emitted for each action taken
439
 
440
  ```
441
- [STEP] {"step": 1, "action": "{\"action_type\": \"label_severity\", \"severity\": \"high\"}", "observation": "pr_id=PR-001, title=Fix null pointer in UserService.java, step=0", "reward": 0.5, "done": false}
442
  ```
443
 
444
  ### `[END]` β€” emitted once per task at completion
445
 
446
  ```
447
- [END] {"task_id": "easy", "total_reward": 3.5, "steps": 5, "success": true}
448
  ```
449
 
450
  ---
 
432
  ### `[START]` β€” emitted once per task
433
 
434
  ```
435
+ [START] task=severity-labeling env=code-review-env model=openai/gpt-4o-mini
436
  ```
437
 
438
  ### `[STEP]` β€” emitted for each action taken
439
 
440
  ```
441
+ [STEP] step=1 action=label_severity:high reward=0.50 done=false error=null
442
  ```
443
 
444
  ### `[END]` β€” emitted once per task at completion
445
 
446
  ```
447
+ [END] success=true steps=5 score=0.767 rewards=0.50,1.00,0.80,0.60,0.93
448
  ```
449
 
450
  ---
inference.py CHANGED
@@ -1,60 +1,85 @@
1
- #!/usr/bin/env python3
2
  """
3
- CodeReviewEnv β€” Mandatory Inference Script
4
- ============================================
5
- MANDATORY ENV VARS:
6
- API_BASE_URL The API endpoint for the LLM.
7
- MODEL_NAME The model identifier to use for inference.
8
- HF_TOKEN Your Hugging Face / API key.
9
-
10
- Uses OpenAI Client for all LLM calls.
11
- Emits structured stdout logs: [START], [STEP], [END] β€” per task.
12
-
13
- Log format (strict, matches OpenEnv spec exactly):
14
- [START] {"task_id": "...", "task_description": "..."}
15
- [STEP] {"step": N, "action": "...", "observation": "...", "reward": 0.0, "done": false}
16
- [END] {"task_id": "...", "total_reward": 0.0, "steps": N, "success": false}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  """
18
 
 
 
19
  import os
20
  import re
21
- import sys
22
- import json
23
- import time
24
- import statistics
25
- from typing import Any, Dict, List, Optional, Tuple
26
 
27
  from openai import OpenAI
28
 
29
- # ─── Configuration ────────────────────────────────────────────────────────────
 
30
 
31
- API_BASE_URL = os.getenv("API_BASE_URL", "https://openrouter.ai/api/v1")
32
- MODEL_NAME = os.getenv("MODEL_NAME", "openai/gpt-4o-mini")
33
- HF_TOKEN = os.getenv("HF_TOKEN") or os.getenv("OPENAI_API_KEY") or os.getenv("API_KEY")
34
 
35
- SEED = 42
 
 
 
 
36
  TEMPERATURE = 0.0
37
  MAX_TOKENS = 300
 
38
 
39
 
40
- # ─── Structured Logging (spec-compliant) ────────────────────────────────────
41
 
42
- def log_start(task_id: str, task_description: str):
43
- """Emit [START] structured log β€” one per task."""
44
- entry = {"task_id": task_id, "task_description": task_description}
45
- print(f"[START] {json.dumps(entry)}", flush=True)
46
 
47
 
48
- def log_step(step: int, action: str, observation: str, reward: float, done: bool):
49
- """Emit [STEP] structured log β€” one per environment step."""
50
- entry = {"step": step, "action": action, "observation": observation, "reward": reward, "done": done}
51
- print(f"[STEP] {json.dumps(entry)}", flush=True)
 
 
 
52
 
53
 
54
- def log_end(task_id: str, total_reward: float, steps: int, success: bool):
55
- """Emit [END] structured log β€” one per task."""
56
- entry = {"task_id": task_id, "total_reward": total_reward, "steps": steps, "success": success}
57
- print(f"[END] {json.dumps(entry)}", flush=True)
 
 
58
 
59
 
60
  # ─── LLM Interface ──────────────────────────────────────────────────────────
@@ -66,14 +91,15 @@ def call_llm(client: OpenAI, system_prompt: str, user_prompt: str) -> str:
66
  model=MODEL_NAME,
67
  messages=[
68
  {"role": "system", "content": system_prompt},
69
- {"role": "user", "content": user_prompt},
70
  ],
71
  temperature=TEMPERATURE,
72
  max_tokens=MAX_TOKENS,
73
  stream=False,
74
  )
75
- return completion.choices[0].message.content or ""
76
  except Exception as exc:
 
77
  return ""
78
 
79
 
@@ -89,396 +115,237 @@ def parse_json_response(response: str) -> Optional[Dict]:
89
  return json.loads(response)
90
  except json.JSONDecodeError:
91
  pass
92
- for pattern in [r'\{[^{}]*\}', r'\{.*\}']:
93
- m = re.search(pattern, response, re.DOTALL)
94
- if m:
95
- try:
96
- return json.loads(m.group(0))
97
- except json.JSONDecodeError:
98
- pass
99
  return None
100
 
101
 
102
  # ─── System Prompts ──────────────────────────────────────────────────────────
103
 
104
- EASY_SYSTEM_PROMPT = """You are a senior software engineer performing code review.
105
- You will receive a pull request with a code diff. Your job is to assess the severity of any bugs present.
106
-
107
- Severity scale:
108
- - "critical": Security vulnerabilities (SQL injection, auth bypass, hardcoded secrets, etc.)
109
- - "high": Crashes or data corruption (null pointer dereference, race conditions, etc.)
110
- - "medium": Logic errors or missing error handling (off-by-one, uncaught exceptions, etc.)
111
- - "low": Performance issues (N+1 queries, unnecessary loops, etc.)
112
- - "none": Style-only changes, no bugs
113
 
114
- Respond ONLY with valid JSON. No explanation, no markdown, no code blocks.
115
- Format: {"action_type": "label_severity", "severity": "<critical|high|medium|low|none>"}"""
 
 
 
 
116
 
117
- MEDIUM_SYSTEM_PROMPT = """You are a senior software engineer managing a code review queue.
118
- You will receive a list of pull requests. Order them by review priority (most urgent first).
 
119
 
120
- Priority rules:
121
- 1. Security-related PRs (SQL injection, auth issues) are always highest priority
122
- 2. Higher severity bugs should be reviewed before lower severity ones
123
- 3. PRs from junior developers need more urgent review than senior ones
124
- 4. PRs without tests should be reviewed earlier
125
 
126
- Respond ONLY with valid JSON. No explanation, no markdown, no code blocks.
127
- Format: {"action_type": "prioritize", "priority_order": ["PR-XXX", "PR-YYY", ...]}"""
 
 
 
128
 
129
- HARD_SYSTEM_PROMPT = """You are a senior software engineer performing detailed code review.
130
- You will see a pull request with a code diff. You must:
131
- 1. Add specific, actionable review comments targeting buggy lines
132
- 2. Then approve or request changes
133
 
134
- For comments, respond with JSON:
135
- {"action_type": "add_comment", "comment": "<specific actionable feedback>", "target_file": "<filename>", "target_line": <line_number>}
 
 
136
 
137
- When done reviewing, respond with:
138
- {"action_type": "request_changes"} if there are bugs, or {"action_type": "approve"} if clean.
139
 
140
- Use domain-specific keywords in comments (e.g. "null check", "parameterized query", "mutex lock").
141
- Be specific about the bug and suggest a concrete fix.
142
 
143
- Respond ONLY with valid JSON. No explanation, no markdown, no code blocks."""
 
144
 
145
 
146
  # ─── Observation Formatting ──────────────────────────────────────────────────
147
 
148
- def format_observation_easy(obs) -> str:
149
  files_text = ""
150
  for f in obs.files:
151
- files_text += f"\n--- {f.filename} ({f.language}, {f.lines_changed} lines changed"
152
- files_text += f", {'has' if f.has_tests else 'no'} tests) ---\n"
153
- files_text += f.diff + "\n"
154
  return (
155
- f"Pull Request: {obs.pr_id}\nTitle: {obs.title}\n"
156
- f"Description: {obs.description}\nAuthor experience: {obs.author_experience}\n"
157
- f"{files_text}\n"
158
- f"Step {obs.step_number + 1} of {obs.episode_budget + obs.step_number}. "
159
- f"What is the severity of any bugs in this PR?"
160
  )
161
 
162
 
163
- def format_observation_medium(obs, queue_templates: List[Dict]) -> str:
164
- queue_text = ""
165
- for t in queue_templates:
166
- has_tests = "has tests" if t.get("has_tests") else "no tests"
167
- bug = t.get("bug_category", "unknown")
168
- queue_text += f"\n- {t['pr_id']}: \"{t['title']}\" (author: {t['author_experience']}, "
169
- queue_text += f"category: {bug}, {has_tests}, {t.get('lines_changed', '?')} lines changed)"
170
- diff_preview = t.get("diff", "")[:200]
171
- if diff_preview:
172
- queue_text += f"\n Diff preview: {diff_preview.strip()[:150]}..."
173
- return (
174
- f"Review Queue β€” Step {obs.step_number + 1} of {obs.episode_budget + obs.step_number}\n\n"
175
- f"You have {len(queue_templates)} PRs to prioritize:{queue_text}\n\n"
176
- f"Order these PRs by review priority (most urgent first). Return ALL PR IDs."
177
- )
178
 
179
 
180
- def format_observation_hard(obs) -> str:
181
  files_text = ""
182
  for f in obs.files:
183
- files_text += f"\n--- {f.filename} ({f.language}, {f.lines_changed} lines changed) ---\n"
184
- files_text += f.diff + "\n"
185
- comments_text = ""
186
  if obs.existing_comments:
187
- comments_text = "\nYour previous comments on this PR:\n"
188
- for c in obs.existing_comments:
189
- comments_text += f" - {c}\n"
190
- return (
191
- f"Pull Request: {obs.pr_id}\nTitle: {obs.title}\n"
192
- f"Description: {obs.description}\nAuthor experience: {obs.author_experience}\n"
193
- f"Remaining PRs in queue: {', '.join(obs.review_queue) if obs.review_queue else 'none'}\n"
194
- f"{files_text}{comments_text}\n"
195
- f"Review this code. If you see bugs, add a specific comment targeting the buggy line.\n"
196
- f"If you've already commented on the main issues, use \"request_changes\" (if bugs) or \"approve\" (if clean)."
197
- )
198
 
199
 
200
- def obs_summary(obs) -> str:
201
- """Create a short observation summary for the log."""
202
- return f"pr_id={obs.pr_id}, title={obs.title[:60]}, step={obs.step_number}"
 
 
 
 
 
 
 
 
 
 
203
 
204
 
205
  # ─── Task Runners ────────────────────────────────────────────────────────────
206
 
207
- def run_easy(client: OpenAI, seed: int) -> Tuple[float, List[Dict]]:
208
- """Run easy task episode. Returns (mean_reward, step_logs)."""
209
- from env.base import CodeReviewEnv
210
- from env.models import Action
211
-
212
- env = CodeReviewEnv(task="easy", seed=seed)
213
- obs = env.reset()
214
-
215
- log_start(
216
- task_id="easy",
217
- task_description="Classify PR severity (none/low/medium/high/critical)",
218
- )
219
-
220
- step_rewards = []
221
- total_steps = 0
222
-
223
- for step in range(5):
224
- prompt = format_observation_easy(obs)
225
- response = call_llm(client, EASY_SYSTEM_PROMPT, prompt)
226
- parsed = parse_json_response(response)
227
-
228
- if parsed and parsed.get("severity"):
229
- action = Action(action_type="label_severity", severity=parsed["severity"])
230
- else:
231
- action = Action(action_type="label_severity", severity="medium")
232
-
233
- obs, reward, done, info = env.step(action)
234
- total_steps = step + 1
235
-
236
- action_str = json.dumps({"action_type": action.action_type, "severity": action.severity})
237
- obs_str = obs_summary(obs)
238
-
239
- log_step(
240
- step=step + 1,
241
- action=action_str,
242
- observation=obs_str,
243
- reward=round(reward.value, 4),
244
- done=done,
245
- )
246
- step_rewards.append(reward.value)
247
-
248
- if done:
249
- break
250
-
251
- mean = statistics.mean(step_rewards) if step_rewards else 0.0
252
- total = sum(step_rewards)
253
- success = mean >= 0.6 # threshold: better than random
254
-
255
- log_end(
256
- task_id="easy",
257
- total_reward=round(total, 4),
258
- steps=total_steps,
259
- success=success,
260
- )
261
-
262
- return mean, step_rewards
263
 
 
 
 
264
 
265
- def run_medium(client: OpenAI, seed: int) -> Tuple[float, List[Dict]]:
266
- """Run medium task episode. Returns (mean_reward, step_logs)."""
267
- from env.base import CodeReviewEnv
268
- from env.models import Action
269
- from tasks.task_medium import MediumTask
270
-
271
- env = CodeReviewEnv(task="medium", seed=seed)
272
- obs = env.reset()
273
-
274
- log_start(
275
- task_id="medium",
276
- task_description="Prioritize review queue by urgency",
277
- )
278
-
279
- step_rewards = []
280
- total_steps = 0
281
-
282
- for step in range(3):
283
- queue_templates = env.task.get_queue_templates(step)
284
- prompt = format_observation_medium(obs, queue_templates)
285
- response = call_llm(client, MEDIUM_SYSTEM_PROMPT, prompt)
286
- parsed = parse_json_response(response)
287
-
288
- queue_ids = [t["pr_id"] for t in queue_templates]
289
- if parsed and parsed.get("priority_order"):
290
- order = parsed["priority_order"]
291
- for qid in queue_ids:
292
- if qid not in order:
293
- order.append(qid)
294
- order = [qid for qid in order if qid in queue_ids] or queue_ids
295
- action = Action(action_type="prioritize", priority_order=order)
296
- else:
297
- action = Action(action_type="prioritize", priority_order=queue_ids)
298
-
299
- obs, reward, done, info = env.step(action)
300
- total_steps = step + 1
301
-
302
- action_str = json.dumps({"action_type": "prioritize", "priority_order": action.priority_order})
303
- obs_str = obs_summary(obs)
304
-
305
- log_step(
306
- step=step + 1,
307
- action=action_str,
308
- observation=obs_str,
309
- reward=round(reward.value, 4),
310
- done=done,
311
- )
312
- step_rewards.append(reward.value)
313
 
314
- if done:
315
- break
 
 
316
 
317
- mean = statistics.mean(step_rewards) if step_rewards else 0.0
318
- total = sum(step_rewards)
319
- success = mean >= 0.5
 
 
320
 
321
- log_end(
322
- task_id="medium",
323
- total_reward=round(total, 4),
324
- steps=total_steps,
325
- success=success,
326
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
 
328
- return mean, step_rewards
 
329
 
 
 
330
 
331
- def run_hard(client: OpenAI, seed: int) -> Tuple[float, List[Dict]]:
332
- """Run hard task episode. Returns (mean_reward, step_logs)."""
333
- from env.base import CodeReviewEnv
334
- from env.models import Action
335
 
336
- env = CodeReviewEnv(task="hard", seed=seed)
337
- obs = env.reset()
338
 
339
- log_start(
340
- task_id="hard",
341
- task_description="Generate actionable review feedback for 3 PRs",
342
- )
343
 
344
- step_rewards = []
345
- total_steps = 0
346
- max_steps = 18 # 3 PRs Γ— 6 actions max
347
-
348
- for step in range(max_steps):
349
- prompt = format_observation_hard(obs)
350
- response = call_llm(client, HARD_SYSTEM_PROMPT, prompt)
351
- parsed = parse_json_response(response)
352
-
353
- if parsed:
354
- action_type = parsed.get("action_type", "")
355
- if action_type == "add_comment":
356
- target_line = parsed.get("target_line", 1)
357
- if not isinstance(target_line, int):
358
- try:
359
- target_line = int(target_line)
360
- except (ValueError, TypeError):
361
- target_line = 1
362
- action = Action(
363
- action_type="add_comment",
364
- comment=parsed.get("comment", "Consider fixing this issue."),
365
- target_file=parsed.get("target_file", "unknown.py"),
366
- target_line=target_line,
367
- )
368
- elif action_type in ("approve", "request_changes"):
369
- action = Action(action_type=action_type)
370
- else:
371
- action = Action(action_type="request_changes")
372
- else:
373
- action = Action(action_type="request_changes")
374
-
375
- obs, reward, done, info = env.step(action)
376
- total_steps = step + 1
377
-
378
- action_dict = {"action_type": action.action_type}
379
- if action.action_type == "add_comment":
380
- action_dict["comment"] = (action.comment or "")[:100]
381
- action_dict["target_file"] = action.target_file
382
- action_dict["target_line"] = action.target_line
383
- action_str = json.dumps(action_dict)
384
- obs_str = obs_summary(obs)
385
-
386
- log_step(
387
- step=step + 1,
388
- action=action_str,
389
- observation=obs_str,
390
- reward=round(reward.value, 4),
391
- done=done,
392
- )
393
- step_rewards.append(reward.value)
394
-
395
- if done:
396
- break
397
-
398
- # PR-level scoring: filter out comment acks (0.05)
399
- pr_rewards = [r for r in step_rewards if abs(r - 0.05) > 0.01]
400
- mean = statistics.mean(pr_rewards) if pr_rewards else 0.0
401
- total = sum(step_rewards)
402
- success = mean >= 0.3
403
-
404
- log_end(
405
- task_id="hard",
406
- total_reward=round(total, 4),
407
- steps=total_steps,
408
- success=success,
409
- )
410
 
411
- return mean, step_rewards
 
 
 
 
 
 
412
 
 
 
 
 
 
413
 
414
- # ─── Main ────────────────────────────────────────────────────────────────────
 
415
 
416
- def main():
417
- if not HF_TOKEN:
418
- print("ERROR: No API key. Set HF_TOKEN, OPENAI_API_KEY, or API_KEY.", flush=True)
419
- sys.exit(1)
420
-
421
- client = OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN)
422
-
423
- start_time = time.time()
424
- all_results = {}
425
-
426
- # ── Easy Task ─────────────────────────────────────────────────────
427
- easy_scores = []
428
- for ep in range(3):
429
- ep_seed = SEED + ep
430
- score, logs = run_easy(client, ep_seed)
431
- easy_scores.append(score)
432
- easy_mean = statistics.mean(easy_scores)
433
- easy_std = statistics.stdev(easy_scores) if len(easy_scores) > 1 else 0.0
434
- all_results["easy"] = {"mean": round(easy_mean, 4), "std": round(easy_std, 4), "scores": [round(s, 4) for s in easy_scores]}
435
-
436
- # ── Medium Task ───────────────────────────────────────────────────
437
- medium_scores = []
438
- for ep in range(3):
439
- ep_seed = SEED + ep
440
- score, logs = run_medium(client, ep_seed)
441
- medium_scores.append(score)
442
- medium_mean = statistics.mean(medium_scores)
443
- medium_std = statistics.stdev(medium_scores) if len(medium_scores) > 1 else 0.0
444
- all_results["medium"] = {"mean": round(medium_mean, 4), "std": round(medium_std, 4), "scores": [round(s, 4) for s in medium_scores]}
445
-
446
- # ── Hard Task ─────────────────────────────────────────────────────
447
- hard_scores = []
448
- for ep in range(3):
449
- ep_seed = SEED + ep
450
- score, logs = run_hard(client, ep_seed)
451
- hard_scores.append(score)
452
- hard_mean = statistics.mean(hard_scores)
453
- hard_std = statistics.stdev(hard_scores) if len(hard_scores) > 1 else 0.0
454
- all_results["hard"] = {"mean": round(hard_mean, 4), "std": round(hard_std, 4), "scores": [round(s, 4) for s in hard_scores]}
455
-
456
- # ── Save results ──────────────────────────────────────────────────
457
- elapsed = time.time() - start_time
458
- composite = round((easy_mean + medium_mean + hard_mean) / 3, 4)
459
-
460
- results_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "baseline", "results.json")
461
- os.makedirs(os.path.dirname(results_path), exist_ok=True)
462
- out = {
463
- "model": MODEL_NAME,
464
- "composite": composite,
465
- "seed": SEED,
466
- **all_results,
467
- "elapsed_seconds": round(elapsed, 1),
468
- }
469
- with open(results_path, "w") as f:
470
- json.dump(out, f, indent=2)
471
-
472
- # Print summary
473
- print(f"\n{'=' * 60}", flush=True)
474
- print(f"INFERENCE COMPLETE β€” {MODEL_NAME}", flush=True)
475
- print(f" Composite: {composite:.4f}", flush=True)
476
- print(f" Easy: {easy_mean:.4f} Β± {easy_std:.4f}", flush=True)
477
- print(f" Medium: {medium_mean:.4f} Β± {medium_std:.4f}", flush=True)
478
- print(f" Hard: {hard_mean:.4f} Β± {hard_std:.4f}", flush=True)
479
- print(f" Elapsed: {elapsed:.1f}s", flush=True)
480
- print(f"{'=' * 60}", flush=True)
481
 
482
 
483
  if __name__ == "__main__":
484
- main()
 
 
1
  """
2
+ CodeReviewEnv β€” Inference Script
3
+ ===================================
4
+ MANDATORY
5
+ - Before submitting, ensure the following variables are defined in your environment configuration:
6
+ API_BASE_URL The API endpoint for the LLM.
7
+ MODEL_NAME The model identifier to use for inference.
8
+ HF_TOKEN Your Hugging Face / API key.
9
+ IMAGE_NAME The name of the local image to use for the environment if using from_docker_image()
10
+
11
+ - The inference script must be named `inference.py` and placed in the root directory of the project
12
+ - Participants must use OpenAI Client for all LLM calls using above variables
13
+
14
+ STDOUT FORMAT
15
+ - The script must emit exactly three line types to stdout, in this order:
16
+
17
+ [START] task=<task_name> env=<benchmark> model=<model_name>
18
+ [STEP] step=<n> action=<action_str> reward=<0.00> done=<true|false> error=<msg|null>
19
+ [END] success=<true|false> steps=<n> score=<0.000> rewards=<r1,r2,...,rn>
20
+
21
+ Rules:
22
+ - One [START] line at episode begin.
23
+ - One [STEP] line per step, immediately after env.step() returns.
24
+ - One [END] line after env.close(), always emitted (even on exception).
25
+ - reward and rewards are formatted to 2 decimal places.
26
+ - done and success are lowercase booleans: true or false.
27
+ - error is the raw last_action_error string, or null if none.
28
+ - All fields on a single line with no newlines within a line.
29
+
30
+ Example:
31
+ [START] task=severity-labeling env=code-review-env model=openai/gpt-4o-mini
32
+ [STEP] step=1 action=label_severity:high reward=0.50 done=false error=null
33
+ [STEP] step=2 action=label_severity:critical reward=1.00 done=false error=null
34
+ [STEP] step=3 action=label_severity:medium reward=0.80 done=true error=null
35
+ [END] success=true steps=3 score=0.767 rewards=0.50,1.00,0.80
36
  """
37
 
38
+ import asyncio
39
+ import json
40
  import os
41
  import re
42
+ import textwrap
43
+ from typing import Any, Dict, List, Optional
 
 
 
44
 
45
  from openai import OpenAI
46
 
47
+ from models import CodeReviewAction, CodeReviewObservation, CodeReviewState
48
+ from client import CodeReviewEnv
49
 
50
+ # ─── Configuration ────────────────────────────────────────────────────────────
 
 
51
 
52
+ IMAGE_NAME = os.getenv("IMAGE_NAME")
53
+ API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY")
54
+ API_BASE_URL = os.getenv("API_BASE_URL") or "https://openrouter.ai/api/v1"
55
+ MODEL_NAME = os.getenv("MODEL_NAME") or "openai/gpt-4o-mini"
56
+ BENCHMARK = "code-review-env"
57
  TEMPERATURE = 0.0
58
  MAX_TOKENS = 300
59
+ SUCCESS_SCORE_THRESHOLD = 0.3
60
 
61
 
62
+ # ─── Structured Logging (exact spec format) ──────────────────────────────────
63
 
64
+ def log_start(task: str, env: str, model: str) -> None:
65
+ print(f"[START] task={task} env={env} model={model}", flush=True)
 
 
66
 
67
 
68
+ def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:
69
+ error_val = error if error else "null"
70
+ done_val = str(done).lower()
71
+ print(
72
+ f"[STEP] step={step} action={action} reward={reward:.2f} done={done_val} error={error_val}",
73
+ flush=True,
74
+ )
75
 
76
 
77
+ def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:
78
+ rewards_str = ",".join(f"{r:.2f}" for r in rewards)
79
+ print(
80
+ f"[END] success={str(success).lower()} steps={steps} score={score:.3f} rewards={rewards_str}",
81
+ flush=True,
82
+ )
83
 
84
 
85
  # ─── LLM Interface ──────────────────────────────────────────────────────────
 
91
  model=MODEL_NAME,
92
  messages=[
93
  {"role": "system", "content": system_prompt},
94
+ {"role": "user", "content": user_prompt},
95
  ],
96
  temperature=TEMPERATURE,
97
  max_tokens=MAX_TOKENS,
98
  stream=False,
99
  )
100
+ return (completion.choices[0].message.content or "").strip()
101
  except Exception as exc:
102
+ print(f"[DEBUG] Model request failed: {exc}", flush=True)
103
  return ""
104
 
105
 
 
115
  return json.loads(response)
116
  except json.JSONDecodeError:
117
  pass
118
+ m = re.search(r'\{.*\}', response, re.DOTALL)
119
+ if m:
120
+ try:
121
+ return json.loads(m.group(0))
122
+ except json.JSONDecodeError:
123
+ pass
 
124
  return None
125
 
126
 
127
  # ─── System Prompts ──────────────────────────────────────────────────────────
128
 
129
+ EASY_SYSTEM_PROMPT = textwrap.dedent("""
130
+ You are a senior software engineer performing code review.
131
+ You will receive a pull request with a code diff. Assess the severity of any bugs present.
 
 
 
 
 
 
132
 
133
+ Severity scale:
134
+ - "critical": Security vulnerabilities (SQL injection, auth bypass, hardcoded secrets)
135
+ - "high": Crashes or data corruption (null pointer dereference, race conditions)
136
+ - "medium": Logic errors or missing error handling (off-by-one, uncaught exceptions)
137
+ - "low": Performance issues (N+1 queries, unnecessary loops)
138
+ - "none": Style-only changes, no bugs
139
 
140
+ Respond ONLY with valid JSON:
141
+ {"action_type": "label_severity", "severity": "<critical|high|medium|low|none>"}
142
+ """).strip()
143
 
144
+ MEDIUM_SYSTEM_PROMPT = textwrap.dedent("""
145
+ You are a senior software engineer managing a code review queue.
146
+ Order the PRs by review priority (most urgent first).
 
 
147
 
148
+ Priority rules:
149
+ 1. Security-related PRs are always highest priority
150
+ 2. Higher severity bugs before lower severity
151
+ 3. Junior developers need more urgent review
152
+ 4. PRs without tests should be reviewed earlier
153
 
154
+ Respond ONLY with valid JSON:
155
+ {"action_type": "prioritize", "priority_order": ["PR-XXX", "PR-YYY", ...]}
156
+ """).strip()
 
157
 
158
+ HARD_SYSTEM_PROMPT = textwrap.dedent("""
159
+ You are a senior software engineer performing detailed code review.
160
+ You must: 1) Add specific, actionable review comments targeting buggy lines
161
+ 2) Then approve or request changes
162
 
163
+ For comments:
164
+ {"action_type": "add_comment", "comment": "<specific feedback>", "target_file": "<filename>", "target_line": <line>}
165
 
166
+ When done reviewing:
167
+ {"action_type": "request_changes"} if bugs found, or {"action_type": "approve"} if clean.
168
 
169
+ Respond ONLY with valid JSON.
170
+ """).strip()
171
 
172
 
173
  # ─── Observation Formatting ──────────────────────────────────────────────────
174
 
175
+ def format_obs_easy(obs: CodeReviewObservation) -> str:
176
  files_text = ""
177
  for f in obs.files:
178
+ files_text += f"\n--- {f['filename']} ({f['language']}, {f['lines_changed']} lines) ---\n"
179
+ files_text += f["diff"] + "\n"
 
180
  return (
181
+ f"PR: {obs.pr_id} | {obs.title}\n{obs.description}\n"
182
+ f"Author: {obs.author_experience}\n{files_text}\n"
183
+ f"What is the severity?"
 
 
184
  )
185
 
186
 
187
+ def format_obs_medium(obs: CodeReviewObservation) -> str:
188
+ queue_text = f"PRs to prioritize: {', '.join(obs.review_queue)}\n"
189
+ files_text = ""
190
+ for f in obs.files:
191
+ files_text += f"\n--- {f['filename']} ({f['language']}) ---\n"
192
+ files_text += (f["diff"][:300] + "...\n") if len(f["diff"]) > 300 else f["diff"] + "\n"
193
+ return f"Review Queue β€” Step {obs.step_number + 1}\n{queue_text}{files_text}\nOrder by priority."
 
 
 
 
 
 
 
 
194
 
195
 
196
+ def format_obs_hard(obs: CodeReviewObservation) -> str:
197
  files_text = ""
198
  for f in obs.files:
199
+ files_text += f"\n--- {f['filename']} ({f['language']}, {f['lines_changed']} lines) ---\n"
200
+ files_text += f["diff"] + "\n"
201
+ comments = ""
202
  if obs.existing_comments:
203
+ comments = "\nYour previous comments:\n" + "\n".join(f" - {c}" for c in obs.existing_comments) + "\n"
204
+ return f"PR: {obs.pr_id} | {obs.title}\n{obs.description}\n{files_text}{comments}\nReview this code."
 
 
 
 
 
 
 
 
 
205
 
206
 
207
+ def action_to_str(action_dict: Dict) -> str:
208
+ """Convert action dict to a compact string for logging."""
209
+ at = action_dict.get("action_type", "unknown")
210
+ if at == "label_severity":
211
+ return f"label_severity:{action_dict.get('severity', '?')}"
212
+ elif at == "prioritize":
213
+ order = action_dict.get("priority_order", [])
214
+ return f"prioritize:[{','.join(order)}]"
215
+ elif at == "add_comment":
216
+ comment = (action_dict.get("comment", ""))[:50]
217
+ return f"add_comment:{comment}"
218
+ else:
219
+ return at
220
 
221
 
222
  # ─── Task Runners ────────────────────────────────────────────────────────────
223
 
224
+ TASK_CONFIGS = {
225
+ "easy": {
226
+ "task_name": "severity-labeling",
227
+ "system_prompt": EASY_SYSTEM_PROMPT,
228
+ "max_steps": 5,
229
+ "format_obs": format_obs_easy,
230
+ "default_action": lambda obs: {"action_type": "label_severity", "severity": "medium"},
231
+ },
232
+ "medium": {
233
+ "task_name": "queue-prioritization",
234
+ "system_prompt": MEDIUM_SYSTEM_PROMPT,
235
+ "max_steps": 3,
236
+ "format_obs": format_obs_medium,
237
+ "default_action": lambda obs: {"action_type": "prioritize", "priority_order": list(obs.review_queue)},
238
+ },
239
+ "hard": {
240
+ "task_name": "feedback-generation",
241
+ "system_prompt": HARD_SYSTEM_PROMPT,
242
+ "max_steps": 18,
243
+ "format_obs": format_obs_hard,
244
+ "default_action": lambda obs: {"action_type": "request_changes"},
245
+ },
246
+ }
247
+
248
+
249
+ async def run_task(env: CodeReviewEnv, llm_client: OpenAI, task: str) -> float:
250
+ """Run a single task episode. Returns normalized score."""
251
+ config = TASK_CONFIGS[task]
252
+ rewards: List[float] = []
253
+ steps_taken = 0
254
+ success = False
255
+ score = 0.0
256
+
257
+ log_start(task=config["task_name"], env=BENCHMARK, model=MODEL_NAME)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
 
259
+ try:
260
+ result = await env.reset(seed=42)
261
+ obs = result.observation
262
 
263
+ for step in range(1, config["max_steps"] + 1):
264
+ if result.done:
265
+ break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
 
267
+ # Get LLM response
268
+ user_prompt = config["format_obs"](obs)
269
+ response = call_llm(llm_client, config["system_prompt"], user_prompt)
270
+ parsed = parse_json_response(response)
271
 
272
+ # Build action
273
+ if parsed and parsed.get("action_type"):
274
+ action_dict = parsed
275
+ else:
276
+ action_dict = config["default_action"](obs)
277
 
278
+ # Ensure valid action fields
279
+ try:
280
+ action = CodeReviewAction(**action_dict)
281
+ except Exception:
282
+ action_dict = config["default_action"](obs)
283
+ action = CodeReviewAction(**action_dict)
284
+
285
+ # Step
286
+ result = await env.step(action)
287
+ obs = result.observation
288
+ reward = result.reward or 0.0
289
+ done = result.done
290
+ error = None
291
+
292
+ rewards.append(reward)
293
+ steps_taken = step
294
+
295
+ log_step(
296
+ step=step,
297
+ action=action_to_str(action_dict),
298
+ reward=reward,
299
+ done=done,
300
+ error=error,
301
+ )
302
+
303
+ if done:
304
+ break
305
+
306
+ # Compute score: mean reward normalized to [0, 1]
307
+ if rewards:
308
+ score = sum(rewards) / len(rewards)
309
+ score = min(max(score, 0.0), 1.0)
310
+ success = score >= SUCCESS_SCORE_THRESHOLD
311
 
312
+ except Exception as exc:
313
+ print(f"[DEBUG] Task {task} error: {exc}", flush=True)
314
 
315
+ finally:
316
+ log_end(success=success, steps=steps_taken, score=score, rewards=rewards)
317
 
318
+ return score
 
 
 
319
 
 
 
320
 
321
+ # ─── Main ────────────────────────────────────────────────────────────────────
 
 
 
322
 
323
+ async def main() -> None:
324
+ llm_client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
 
326
+ # Connect to environment via Docker image or HF Space
327
+ if IMAGE_NAME:
328
+ env = await CodeReviewEnv.from_docker_image(IMAGE_NAME)
329
+ else:
330
+ # Fallback: connect to running server
331
+ space_url = os.getenv("SPACE_URL", "https://ragavrida-code-review-env.hf.space")
332
+ env = CodeReviewEnv(base_url=space_url)
333
 
334
+ try:
335
+ scores = {}
336
+ for task in ["easy", "medium", "hard"]:
337
+ score = await run_task(env, llm_client, task)
338
+ scores[task] = score
339
 
340
+ composite = sum(scores.values()) / len(scores)
341
+ print(f"\n[SUMMARY] composite={composite:.3f} easy={scores['easy']:.3f} medium={scores['medium']:.3f} hard={scores['hard']:.3f}", flush=True)
342
 
343
+ finally:
344
+ try:
345
+ await env.close()
346
+ except Exception as e:
347
+ print(f"[DEBUG] env.close() error: {e}", flush=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
 
349
 
350
  if __name__ == "__main__":
351
+ asyncio.run(main())