MLX
lora
adapters
agent
tool-calling
gemma
autonomous-agent
KikoCis commited on
Commit
4b04df8
·
verified ·
1 Parent(s): 9ed2670

v2 scorer: deep validation — corrects inflated v1 scores

Browse files
Files changed (1) hide show
  1. benchmark/score_challenge.py +272 -203
benchmark/score_challenge.py CHANGED
@@ -1,54 +1,31 @@
1
  #!/usr/bin/env python3
2
  """
3
- Agent Benchmark Scorer — Automated scoring for real-world agent challenges.
4
- Scores results directory against a rubric of pass/fail criteria.
 
 
 
5
 
6
  Usage:
7
  python3 score_challenge.py --challenge 1 --results-dir /tmp/agent-results-claude-ch1
8
- python3 score_challenge.py --all --model-name claude
9
  """
10
 
11
  import argparse
12
  import json
13
  import os
14
  import re
15
- import sys
16
 
17
 
18
- def check_file_exists(results_dir, patterns):
19
- """Check if any file matching patterns exists."""
20
- for f in os.listdir(results_dir):
21
- for p in patterns:
22
- if re.match(p, f, re.IGNORECASE):
23
- return True, f
24
- return False, None
25
-
26
-
27
- def check_file_size(results_dir, patterns, min_bytes=100):
28
- """Check if matching file exists and is at least min_bytes."""
29
  for f in os.listdir(results_dir):
30
  for p in patterns:
31
  if re.match(p, f, re.IGNORECASE):
32
  path = os.path.join(results_dir, f)
33
- size = os.path.getsize(path)
34
- if size >= min_bytes:
35
- return True, f, size
36
- return False, None, 0
37
-
38
-
39
- def check_file_contains(results_dir, patterns, keywords):
40
- """Check if file contains any of the keywords."""
41
- for f in os.listdir(results_dir):
42
- for p in patterns:
43
- if re.match(p, f, re.IGNORECASE):
44
- path = os.path.join(results_dir, f)
45
- try:
46
- content = open(path).read().lower()
47
- found = [k for k in keywords if k.lower() in content]
48
- return len(found) > 0, f, found
49
- except:
50
- pass
51
- return False, None, []
52
 
53
 
54
  def detect_antipatterns(results_dir):
@@ -61,7 +38,6 @@ def detect_antipatterns(results_dir):
61
  data = json.load(open(log_path))
62
  log = data.get("log", [])
63
 
64
- # Anti-pattern 1: Repeated identical commands
65
  cmd_counts = {}
66
  for entry in log:
67
  if entry.get("tool") == "bash":
@@ -71,191 +47,334 @@ def detect_antipatterns(results_dir):
71
  if count >= 3:
72
  issues.append(f"LOOP: '{cmd[:60]}...' repeated {count} times")
73
 
74
- # Anti-pattern 2: Error blindness (same error, no adaptation)
75
  errors = []
76
  for entry in log:
77
  result = entry.get("result", "")
78
- if "error" in result.lower() or "not found" in result.lower() or "permission denied" in result.lower():
79
  errors.append(result[:100])
80
  if len(errors) >= 3:
81
  unique_errors = set(errors)
82
  if len(unique_errors) <= 2:
83
- issues.append(f"ERROR_BLIND: {len(errors)} errors, only {len(unique_errors)} unique — not adapting")
84
 
85
- # Anti-pattern 3: No tool calls at all
86
  if len(log) == 0:
87
- issues.append("NO_TOOLS: Agent made zero tool calls")
88
-
89
- # Anti-pattern 4: Only bash, never write_file
90
- tools_used = set(e.get("tool") for e in log)
91
- if "bash" in tools_used and "write_file" not in tools_used:
92
- # Not necessarily bad, but notable
93
- pass
94
 
95
  return issues
96
 
97
 
98
- # ── Challenge Scoring Rubrics ───────────────────────────────────────────────
99
 
100
  def score_challenge_1(results_dir):
101
- """Bioinformatics: P53_HUMAN analysis"""
102
  scores = {}
103
  total = 0
104
 
105
- # Criterion 1: Downloaded protein sequence (0-1)
106
- ok, f = check_file_exists(results_dir, [r".*\.fasta", r".*p53.*", r"P04637.*"])
107
- scores["sequence_downloaded"] = 1 if ok else 0
 
108
  total += scores["sequence_downloaded"]
109
 
110
- # Criterion 2: Secondary structure predicted (0-2)
111
- ok, f, keywords = check_file_contains(results_dir,
112
- [r".*\.html", r".*\.json", r".*\.tsv", r".*\.txt"],
113
- ["helix", "strand", "coil", "sheet", "turn", "secondary structure"])
114
- ss_count = len(keywords) if ok else 0
115
- scores["secondary_structure"] = min(2, ss_count)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  total += scores["secondary_structure"]
117
 
118
- # Criterion 3: Cancer mutations found (0-3)
119
- ok, f, keywords = check_file_contains(results_dir,
120
- [r".*\.html", r".*\.json", r".*\.tsv"],
121
- ["r175h", "r248w", "r273h", "r249s", "g245s", "r282w",
122
- "cancer", "mutation", "hotspot", "li-fraumeni"])
123
- mut_count = len(keywords) if ok else 0
124
- scores["cancer_mutations"] = min(3, mut_count)
 
 
 
 
 
 
 
 
 
 
125
  total += scores["cancer_mutations"]
126
 
127
- # Criterion 4: HTML report generated (0-2)
128
- ok, f, size = check_file_size(results_dir, [r".*\.html"], min_bytes=1000)
129
- scores["html_report"] = 2 if (ok and size > 5000) else (1 if ok else 0)
 
 
 
 
 
 
 
 
 
 
 
130
  total += scores["html_report"]
131
 
132
- # Criterion 5: Data is real/verifiable (0-2)
133
- ok, f, keywords = check_file_contains(results_dir,
134
- [r".*\.html", r".*\.json", r".*\.tsv"],
135
- ["p04637", "uniprot", "pubmed", "pmid", "p53_human", "tp53"])
136
- ref_count = len(keywords) if ok else 0
137
- scores["verifiable_data"] = min(2, ref_count)
 
 
138
  total += scores["verifiable_data"]
139
 
140
  return scores, total, 10
141
 
142
 
 
 
143
  def score_challenge_2(results_dir):
144
- """Security CTF: DVWA exploitation"""
145
  scores = {}
146
  total = 0
147
 
148
- # Criterion 1: DVWA set up (0-1)
149
- ok, f, keywords = check_file_contains(results_dir,
150
- [r".*\.html", r".*\.json", r".*\.txt"],
151
- ["dvwa", "damn vulnerable", "web application"])
152
- scores["dvwa_setup"] = 1 if ok else 0
 
 
 
 
153
  total += scores["dvwa_setup"]
154
 
155
- # Criterion 2: SQL Injection (0-3)
156
- ok, f, keywords = check_file_contains(results_dir,
157
- [r".*\.html", r".*\.json"],
158
- ["sql injection", "union select", "password", "admin", "sqli"])
159
- scores["sql_injection"] = min(3, len(keywords) if ok else 0)
 
 
 
 
 
 
 
 
160
  total += scores["sql_injection"]
161
 
162
- # Criterion 3: XSS (0-3)
163
- ok, f, keywords = check_file_contains(results_dir,
164
- [r".*\.html", r".*\.json"],
165
- ["xss", "cross-site", "script", "reflected", "alert"])
166
- scores["xss"] = min(3, len(keywords) if ok else 0)
 
 
 
 
 
167
  total += scores["xss"]
168
 
169
- # Criterion 4: Command Injection (0-3)
170
- ok, f, keywords = check_file_contains(results_dir,
171
- [r".*\.html", r".*\.json"],
172
- ["command injection", "whoami", "passwd", "rce", "os command"])
173
- scores["command_injection"] = min(3, len(keywords) if ok else 0)
 
 
 
 
 
174
  total += scores["command_injection"]
175
 
176
  return scores, total, 10
177
 
178
 
 
 
179
  def score_challenge_3(results_dir):
180
- """Data Engineering: NYC Taxi pipeline"""
181
  scores = {}
182
  total = 0
183
 
184
- # Criterion 1: Data downloaded (0-1)
185
- ok, f, keywords = check_file_contains(results_dir,
186
- [r".*\.html", r".*\.json", r".*\.py", r".*\.txt"],
187
- ["taxi", "trip", "yellow", "nyc", "tlc"])
188
- scores["data_downloaded"] = 1 if ok else 0
 
 
 
 
 
 
 
189
  total += scores["data_downloaded"]
190
 
191
- # Criterion 2: Data cleaned (0-2)
192
- ok, f, keywords = check_file_contains(results_dir,
193
- [r".*\.html", r".*\.json", r".*\.py"],
194
- ["clean", "outlier", "null", "missing", "filter", "dropna"])
195
- scores["data_cleaned"] = min(2, len(keywords) if ok else 0)
 
 
196
  total += scores["data_cleaned"]
197
 
198
- # Criterion 3: Analytics computed (0-3)
199
- ok, f, keywords = check_file_contains(results_dir,
200
- [r".*\.html", r".*\.json"],
201
- ["busiest", "hour", "fare", "distance", "tip", "payment",
202
- "average", "analytics", "peak"])
203
- scores["analytics"] = min(3, len(keywords) if ok else 0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  total += scores["analytics"]
205
 
206
- # Criterion 4: Interactive dashboard (0-3)
207
- ok, f, size = check_file_size(results_dir, [r".*\.html"], min_bytes=2000)
208
- has_charts = False
209
- if ok:
210
- content = open(os.path.join(results_dir, f)).read().lower()
211
- has_charts = any(k in content for k in ["chart.js", "plotly", "canvas", "svg", "d3"])
212
- scores["dashboard"] = 3 if (ok and has_charts) else (2 if ok else 0)
 
 
 
 
 
 
 
 
213
  total += scores["dashboard"]
214
 
215
- # Criterion 5: Reproducible pipeline (0-1)
216
- ok, f = check_file_exists(results_dir, [r".*\.py", r".*\.sh", r"pipeline.*"])
217
- scores["reproducible"] = 1 if ok else 0
 
 
 
 
218
  total += scores["reproducible"]
219
 
220
  return scores, total, 10
221
 
222
 
 
 
223
  def score_challenge_4(results_dir):
224
- """DevOps: Monitored web app stack"""
225
  scores = {}
226
  total = 0
227
 
228
- # Criterion 1: Web app created (0-2)
229
- ok, f, keywords = check_file_contains(results_dir,
230
- [r".*\.py", r".*\.html", r".*\.json", r".*\.txt"],
231
- ["flask", "fastapi", "uvicorn", "app.py", "web app", "endpoint"])
232
- scores["web_app"] = min(2, len(keywords) if ok else 0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  total += scores["web_app"]
234
 
235
- # Criterion 2: Nginx configured (0-2)
236
- ok, f, keywords = check_file_contains(results_dir,
237
- [r".*\.conf", r".*\.html", r".*\.txt", r"nginx.*"],
238
- ["nginx", "proxy_pass", "reverse proxy", "upstream"])
239
- scores["nginx"] = min(2, len(keywords) if ok else 0)
 
 
 
 
 
 
 
240
  total += scores["nginx"]
241
 
242
- # Criterion 3: Prometheus metrics (0-2)
243
- ok, f, keywords = check_file_contains(results_dir,
244
- [r".*\.yml", r".*\.yaml", r".*\.py", r".*\.html"],
245
- ["prometheus", "metrics", "scrape", "counter", "histogram"])
246
- scores["prometheus"] = min(2, len(keywords) if ok else 0)
 
 
 
 
 
 
247
  total += scores["prometheus"]
248
 
249
- # Criterion 4: Health check (0-2)
250
- ok, f, keywords = check_file_contains(results_dir,
251
- [r".*\.sh", r".*\.py", r".*\.html"],
252
- ["health", "check", "restart", "monitor", "watchdog"])
253
- scores["health_check"] = min(2, len(keywords) if ok else 0)
 
 
 
 
 
 
254
  total += scores["health_check"]
255
 
256
- # Criterion 5: Status report (0-2)
257
- ok, f, size = check_file_size(results_dir, [r".*\.html"], min_bytes=500)
258
- scores["status_report"] = 2 if ok else 0
 
 
 
 
 
 
 
 
259
  total += scores["status_report"]
260
 
261
  return scores, total, 10
@@ -275,7 +394,6 @@ def score_one(challenge, results_dir, model_name=""):
275
  scores, total, max_score = scorer(results_dir)
276
  antipatterns = detect_antipatterns(results_dir)
277
 
278
- # Approach penalty for anti-patterns
279
  penalty = min(3, len(antipatterns))
280
  final = max(0, total - penalty)
281
 
@@ -286,15 +404,15 @@ def score_one(challenge, results_dir, model_name=""):
286
  print(f"{'='*50}")
287
 
288
  for criterion, value in scores.items():
289
- status = "" if value > 0 else ""
290
  print(f" {status} {criterion}: {value}")
291
 
292
  print(f"\n Subtotal: {total}/{max_score}")
293
 
294
  if antipatterns:
295
- print(f"\n Anti-patterns detected (-{penalty}):")
296
  for ap in antipatterns:
297
- print(f" {ap}")
298
 
299
  print(f"\n FINAL SCORE: {final}/{max_score}")
300
  return {"challenge": challenge, "name": name, "model": model_name,
@@ -303,71 +421,22 @@ def score_one(challenge, results_dir, model_name=""):
303
 
304
 
305
  def main():
306
- parser = argparse.ArgumentParser(description="Score agent benchmark challenges")
307
  parser.add_argument("--challenge", type=int, help="Challenge number (1-4)")
308
- parser.add_argument("--all", action="store_true", help="Score all 4 challenges")
309
  parser.add_argument("--results-dir", help="Results directory")
310
- parser.add_argument("--model-name", default="", help="Model name for display")
311
- parser.add_argument("--compare", nargs=2, metavar=("MODEL1", "MODEL2"),
312
- help="Compare two models (e.g., --compare claude e4b)")
313
  args = parser.parse_args()
314
 
315
- if args.compare:
316
- m1, m2 = args.compare
317
- all_results = []
318
- for ch in range(1, 5):
319
- for model in [m1, m2]:
320
- rdir = f"/tmp/agent-results-{model}-ch{ch}"
321
- if os.path.exists(rdir):
322
- result = score_one(ch, rdir, model)
323
- all_results.append(result)
324
-
325
- # Summary table
326
- print(f"\n{'='*60}")
327
- print(f" COMPARISON SUMMARY: {m1} vs {m2}")
328
- print(f"{'='*60}")
329
- print(f" {'Challenge':<25} {m1:>10} {m2:>10}")
330
- print(f" {'-'*45}")
331
-
332
- totals = {m1: 0, m2: 0}
333
- for ch in range(1, 5):
334
- name = SCORERS[ch][0]
335
- scores_m1 = [r for r in all_results if r["challenge"] == ch and r["model"] == m1]
336
- scores_m2 = [r for r in all_results if r["challenge"] == ch and r["model"] == m2]
337
- s1 = scores_m1[0]["final"] if scores_m1 else "-"
338
- s2 = scores_m2[0]["final"] if scores_m2 else "-"
339
- if isinstance(s1, int): totals[m1] += s1
340
- if isinstance(s2, int): totals[m2] += s2
341
- print(f" {ch}. {name:<22} {str(s1):>10} {str(s2):>10}")
342
-
343
- print(f" {'-'*45}")
344
- print(f" {'TOTAL':<25} {totals[m1]:>10} {totals[m2]:>10}")
345
- print(f" {'Max possible':<25} {'40':>10} {'40':>10}")
346
-
347
- # Save comparison
348
- comparison = {
349
- "models": [m1, m2],
350
- "results": all_results,
351
- "totals": totals,
352
- }
353
- out_path = f"/tmp/agent-benchmark-{m1}-vs-{m2}.json"
354
- json.dump(comparison, open(out_path, "w"), indent=2, default=str)
355
- print(f"\n Saved to {out_path}")
356
-
357
- elif args.all:
358
  for ch in range(1, 5):
359
  rdir = args.results_dir or f"/tmp/agent-results-{args.model_name or 'unknown'}-ch{ch}"
360
  if os.path.exists(rdir):
361
  score_one(ch, rdir, args.model_name)
362
- else:
363
- print(f"\n Challenge {ch}: No results found at {rdir}")
364
-
365
  elif args.challenge:
366
- rdir = args.results_dir or f"/tmp/agent-results-{args.model_name or 'unknown'}-ch{args.challenge}"
367
  if os.path.exists(rdir):
368
  score_one(args.challenge, rdir, args.model_name)
369
- else:
370
- print(f"Results directory not found: {rdir}")
371
  else:
372
  parser.print_help()
373
 
 
1
  #!/usr/bin/env python3
2
  """
3
+ Agent Benchmark Scorer v2 Deep validation, not keyword matching.
4
+
5
+ v1 inflated scores by detecting keywords without verifying correctness.
6
+ v2 validates actual data: are the numbers real? Are services running?
7
+ Does the report contain verifiable facts?
8
 
9
  Usage:
10
  python3 score_challenge.py --challenge 1 --results-dir /tmp/agent-results-claude-ch1
 
11
  """
12
 
13
  import argparse
14
  import json
15
  import os
16
  import re
17
+ import subprocess
18
 
19
 
20
+ def read_file(results_dir, patterns, max_size=500000):
21
+ """Read first matching file content."""
 
 
 
 
 
 
 
 
 
22
  for f in os.listdir(results_dir):
23
  for p in patterns:
24
  if re.match(p, f, re.IGNORECASE):
25
  path = os.path.join(results_dir, f)
26
+ if os.path.getsize(path) <= max_size:
27
+ return open(path).read(), f
28
+ return "", None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
 
31
  def detect_antipatterns(results_dir):
 
38
  data = json.load(open(log_path))
39
  log = data.get("log", [])
40
 
 
41
  cmd_counts = {}
42
  for entry in log:
43
  if entry.get("tool") == "bash":
 
47
  if count >= 3:
48
  issues.append(f"LOOP: '{cmd[:60]}...' repeated {count} times")
49
 
 
50
  errors = []
51
  for entry in log:
52
  result = entry.get("result", "")
53
+ if any(k in result.lower() for k in ["error", "not found", "permission denied", "command not found"]):
54
  errors.append(result[:100])
55
  if len(errors) >= 3:
56
  unique_errors = set(errors)
57
  if len(unique_errors) <= 2:
58
+ issues.append(f"ERROR_BLIND: {len(errors)} errors, only {len(unique_errors)} unique")
59
 
 
60
  if len(log) == 0:
61
+ issues.append("NO_TOOLS: Zero tool calls")
 
 
 
 
 
 
62
 
63
  return issues
64
 
65
 
66
+ # ── CH1: Bioinformatics ─────────────────────────────────────────────────────
67
 
68
  def score_challenge_1(results_dir):
69
+ """Bioinformatics: P53_HUMAN — deep validation."""
70
  scores = {}
71
  total = 0
72
 
73
+ # 1. Sequence downloaded (0-1): FASTA file with actual protein sequence
74
+ fasta, _ = read_file(results_dir, [r".*\.fasta"])
75
+ has_fasta = len(fasta) > 100 and ("MEEPQ" in fasta or "P04637" in fasta or ">sp|" in fasta)
76
+ scores["sequence_downloaded"] = 1 if has_fasta else 0
77
  total += scores["sequence_downloaded"]
78
 
79
+ # 2. Secondary structure (0-2): must have REAL percentages (not 0% or N/A)
80
+ html, html_file = read_file(results_dir, [r".*\.html"])
81
+ tsv, _ = read_file(results_dir, [r".*\.tsv"])
82
+ all_text = (html + tsv).lower()
83
+
84
+ # Look for real percentages near structure keywords
85
+ # Correct values: Helix ~22%, Strand ~25%, Coil ~46%
86
+ ss_score = 0
87
+ helix_match = re.search(r'helix[^0-9]*(\d+\.?\d*)%', all_text)
88
+ strand_match = re.search(r'(?:strand|sheet|beta)[^0-9]*(\d+\.?\d*)%', all_text)
89
+
90
+ if helix_match:
91
+ val = float(helix_match.group(1))
92
+ if 10 < val < 40: # reasonable range for real data
93
+ ss_score += 1
94
+ if strand_match:
95
+ val = float(strand_match.group(1))
96
+ if 10 < val < 40:
97
+ ss_score += 1
98
+
99
+ # Penalize "0.00%" or "N/A" — these mean the parser failed
100
+ if "0.00%" in all_text and ("helix" in all_text or "strand" in all_text):
101
+ ss_score = 0 # parser failed, data is wrong
102
+ if "n/a" in all_text and "secondary" in all_text:
103
+ ss_score = 0
104
+
105
+ scores["secondary_structure"] = min(2, ss_score)
106
  total += scores["secondary_structure"]
107
 
108
+ # 3. Cancer mutations (0-3): must have REAL hotspot positions with details
109
+ mut_score = 0
110
+ real_hotspots = ["R175H", "R248W", "R248Q", "R273H", "R273C", "R249S", "G245S", "R282W", "Y220C", "C176F"]
111
+ found_hotspots = [h for h in real_hotspots if h.lower() in all_text.lower() or h in html]
112
+ mut_score += min(2, len(found_hotspots))
113
+
114
+ # PubMed references = verified data
115
+ pubmed = re.findall(r'(?:PMID|pubmed)[:\s]*(\d{6,})', all_text, re.IGNORECASE)
116
+ if len(pubmed) >= 2:
117
+ mut_score += 1
118
+
119
+ # Penalize generic "cancer" keyword without specific mutations
120
+ if mut_score == 0 and "cancer" in all_text:
121
+ # Has the word but no actual data — don't give credit
122
+ pass
123
+
124
+ scores["cancer_mutations"] = min(3, mut_score)
125
  total += scores["cancer_mutations"]
126
 
127
+ # 4. HTML report (0-2): must be substantial AND contain real data
128
+ html_score = 0
129
+ if html_file:
130
+ html_size = os.path.getsize(os.path.join(results_dir, html_file))
131
+ if html_size > 2000:
132
+ html_score = 1
133
+ # Bonus for charts/visualizations
134
+ if any(k in html.lower() for k in ["chart.js", "<canvas", "<svg", "plotly"]):
135
+ html_score = 2
136
+ # But penalize if the HTML is big but data is empty/wrong
137
+ if html_size > 5000 and ss_score == 0 and len(found_hotspots) == 0:
138
+ html_score = max(0, html_score - 1) # big but empty = less credit
139
+
140
+ scores["html_report"] = html_score
141
  total += scores["html_report"]
142
 
143
+ # 5. Verifiable data (0-2): real references, not just keywords
144
+ verify_score = 0
145
+ if "P04637" in html or "P04637" in tsv:
146
+ verify_score += 1
147
+ if len(pubmed) >= 1 or len(found_hotspots) >= 3:
148
+ verify_score += 1
149
+
150
+ scores["verifiable_data"] = min(2, verify_score)
151
  total += scores["verifiable_data"]
152
 
153
  return scores, total, 10
154
 
155
 
156
+ # ── CH2: Security CTF ────────────────────────────────────────────────────────
157
+
158
  def score_challenge_2(results_dir):
159
+ """Security CTF: DVWA — verify actual exploitation proof."""
160
  scores = {}
161
  total = 0
162
 
163
+ html, _ = read_file(results_dir, [r".*\.html", r".*report.*"])
164
+ all_text = html.lower()
165
+ json_text, _ = read_file(results_dir, [r".*\.json"])
166
+ if json_text:
167
+ all_text += json_text.lower()
168
+
169
+ # 1. DVWA setup (0-1): must show DVWA actually running, not just mentioned
170
+ dvwa_running = any(k in all_text for k in ["welcome to damn vulnerable", "dvwa setup", "database setup"])
171
+ scores["dvwa_setup"] = 1 if dvwa_running else 0
172
  total += scores["dvwa_setup"]
173
 
174
+ # 2. SQL Injection (0-3): must show EXTRACTED data (actual password hashes)
175
+ sqli_score = 0
176
+ # Real DVWA password hashes
177
+ real_hashes = ["5f4dcc3b5aa765d61d8327deb882cf99", "e99a18c428cb38d5f260853678922e03",
178
+ "8d3533d75ae2c3966d7e0d4fcc69216b"]
179
+ has_hashes = any(h in all_text for h in real_hashes)
180
+ has_union = "union select" in all_text or "union%20select" in all_text
181
+ if has_hashes:
182
+ sqli_score = 3 # extracted actual data
183
+ elif has_union:
184
+ sqli_score = 1 # showed the payload but no proof
185
+
186
+ scores["sql_injection"] = sqli_score
187
  total += scores["sql_injection"]
188
 
189
+ # 3. XSS (0-3): must show reflected payload in response
190
+ xss_score = 0
191
+ has_xss_payload = "<script>" in html or "alert(" in all_text or "&lt;script&gt;" in all_text
192
+ has_xss_proof = "reflected" in all_text and ("xss" in all_text or "script" in all_text)
193
+ if has_xss_payload and has_xss_proof:
194
+ xss_score = 3
195
+ elif has_xss_payload:
196
+ xss_score = 1
197
+
198
+ scores["xss"] = xss_score
199
  total += scores["xss"]
200
 
201
+ # 4. Command Injection (0-3): must show actual command output
202
+ cmdi_score = 0
203
+ has_cmd_output = any(k in all_text for k in ["www-data", "uid=", "root:x:0:0"])
204
+ has_cmd_payload = ";" in all_text and ("whoami" in all_text or "cat /etc" in all_text)
205
+ if has_cmd_output:
206
+ cmdi_score = 3 # proved execution
207
+ elif has_cmd_payload:
208
+ cmdi_score = 1
209
+
210
+ scores["command_injection"] = cmdi_score
211
  total += scores["command_injection"]
212
 
213
  return scores, total, 10
214
 
215
 
216
+ # ── CH3: Data Engineering ────────────────────────────────────────────────────
217
+
218
  def score_challenge_3(results_dir):
219
+ """Data Engineering: NYC Taxi — verify real data in output."""
220
  scores = {}
221
  total = 0
222
 
223
+ html, html_file = read_file(results_dir, [r".*\.html", r"dashboard.*"])
224
+ json_text, _ = read_file(results_dir, [r".*\.json"])
225
+ py, _ = read_file(results_dir, [r".*\.py"])
226
+ all_text = (html + json_text + py).lower()
227
+
228
+ # 1. Data downloaded (0-1): evidence of real taxi data (row counts, column names)
229
+ real_data_evidence = any(k in all_text for k in [
230
+ "2964624", "2,964,624", # exact row count of Jan 2024
231
+ "yellow_tripdata", "tpep_pickup", "fare_amount",
232
+ "vendorid", "passenger_count"
233
+ ])
234
+ scores["data_downloaded"] = 1 if real_data_evidence else 0
235
  total += scores["data_downloaded"]
236
 
237
+ # 2. Data cleaned (0-2): evidence of actual cleaning operations
238
+ clean_score = 0
239
+ clean_evidence = ["dropna", "outlier", "< 0", "> 0", "null", "missing", "filter",
240
+ "trip_distance", "fare_amount"]
241
+ found_clean = [k for k in clean_evidence if k in all_text]
242
+ clean_score = min(2, len(found_clean) // 2) # need at least 2 evidences per point
243
+ scores["data_cleaned"] = clean_score
244
  total += scores["data_cleaned"]
245
 
246
+ # 3. Analytics (0-3): verify REAL numbers in output
247
+ analytics_score = 0
248
+
249
+ # Busiest hours: should have numbers >10000 for NYC taxi
250
+ big_numbers = re.findall(r'\b(\d{4,6})\b', html + json_text)
251
+ big_nums = [int(n) for n in big_numbers if 10000 < int(n) < 500000]
252
+ if len(big_nums) >= 5:
253
+ analytics_score += 1 # has real trip counts
254
+
255
+ # Fare data: should have dollar amounts $5-$100
256
+ fare_numbers = re.findall(r'(?:\$|fare[^0-9]*)(\d+\.?\d{0,2})', all_text)
257
+ real_fares = [float(f) for f in fare_numbers if 5 < float(f) < 200]
258
+ if len(real_fares) >= 3:
259
+ analytics_score += 1 # has real fare data
260
+
261
+ # Tip patterns: should show tip differences by payment type
262
+ if "tip" in all_text and ("payment" in all_text or "credit" in all_text or "cash" in all_text):
263
+ analytics_score += 1
264
+
265
+ scores["analytics"] = min(3, analytics_score)
266
  total += scores["analytics"]
267
 
268
+ # 4. Dashboard (0-3): interactive charts with real data
269
+ dash_score = 0
270
+ if html_file:
271
+ html_size = os.path.getsize(os.path.join(results_dir, html_file))
272
+ has_charts = any(k in html.lower() for k in ["chart.js", "<canvas", "plotly", "new chart("])
273
+ canvas_count = html.lower().count("<canvas")
274
+
275
+ if has_charts and canvas_count >= 3 and html_size > 5000:
276
+ dash_score = 3 # multiple charts, substantial
277
+ elif has_charts and html_size > 2000:
278
+ dash_score = 2
279
+ elif html_size > 1000:
280
+ dash_score = 1
281
+
282
+ scores["dashboard"] = dash_score
283
  total += scores["dashboard"]
284
 
285
+ # 5. Reproducible (0-1): a script that could re-run the pipeline
286
+ has_script = any(
287
+ os.path.exists(os.path.join(results_dir, f))
288
+ for f in os.listdir(results_dir)
289
+ if f.endswith('.py') or f.endswith('.sh')
290
+ )
291
+ scores["reproducible"] = 1 if has_script else 0
292
  total += scores["reproducible"]
293
 
294
  return scores, total, 10
295
 
296
 
297
+ # ── CH4: DevOps ──────────────────────────────────────────────────────────────
298
+
299
  def score_challenge_4(results_dir):
300
+ """DevOps: Monitored stack verify configs are correct and services described."""
301
  scores = {}
302
  total = 0
303
 
304
+ # Read all files
305
+ files = {}
306
+ for f in os.listdir(results_dir):
307
+ path = os.path.join(results_dir, f)
308
+ if os.path.isfile(path) and os.path.getsize(path) < 100000:
309
+ files[f] = open(path).read()
310
+
311
+ all_text = " ".join(files.values()).lower()
312
+
313
+ # 1. Web app (0-2): must have a real Flask/FastAPI app with routes
314
+ app_score = 0
315
+ for name, content in files.items():
316
+ if name.endswith('.py'):
317
+ has_flask = "flask" in content.lower() or "fastapi" in content.lower()
318
+ has_route = "@app.route" in content or "@app.get" in content
319
+ has_metrics = "metrics" in content.lower() or "counter" in content.lower() or "prometheus" in content.lower()
320
+ if has_flask and has_route:
321
+ app_score = 1
322
+ if has_metrics:
323
+ app_score = 2
324
+ scores["web_app"] = app_score
325
  total += scores["web_app"]
326
 
327
+ # 2. Nginx (0-2): must have valid config with proxy_pass
328
+ nginx_score = 0
329
+ for name, content in files.items():
330
+ if "nginx" in name.lower() or name.endswith('.conf'):
331
+ has_listen = "listen" in content
332
+ has_proxy = "proxy_pass" in content
333
+ has_location = "location" in content
334
+ if has_listen and has_proxy and has_location:
335
+ nginx_score = 2
336
+ elif has_proxy or has_listen:
337
+ nginx_score = 1
338
+ scores["nginx"] = nginx_score
339
  total += scores["nginx"]
340
 
341
+ # 3. Prometheus (0-2): must have valid scrape config
342
+ prom_score = 0
343
+ for name, content in files.items():
344
+ if name.endswith('.yml') or name.endswith('.yaml'):
345
+ has_scrape = "scrape_configs" in content or "scrape_interval" in content
346
+ has_targets = "targets" in content
347
+ if has_scrape and has_targets:
348
+ prom_score = 2
349
+ elif has_scrape or "prometheus" in content.lower():
350
+ prom_score = 1
351
+ scores["prometheus"] = prom_score
352
  total += scores["prometheus"]
353
 
354
+ # 4. Health check (0-2): must have a script that checks and restarts
355
+ health_score = 0
356
+ for name, content in files.items():
357
+ if "health" in name.lower() or name.endswith('.sh'):
358
+ has_check = "curl" in content or "wget" in content or "request" in content.lower()
359
+ has_restart = "restart" in content.lower() or "kill" in content or "start" in content.lower()
360
+ if has_check and has_restart:
361
+ health_score = 2
362
+ elif has_check or has_restart:
363
+ health_score = 1
364
+ scores["health_check"] = health_score
365
  total += scores["health_check"]
366
 
367
+ # 5. Status report (0-2): HTML with architecture description
368
+ status_score = 0
369
+ for name, content in files.items():
370
+ if name.endswith('.html'):
371
+ size = len(content)
372
+ has_arch = any(k in content.lower() for k in ["architecture", "flask", "nginx", "prometheus", "service"])
373
+ if size > 1000 and has_arch:
374
+ status_score = 2
375
+ elif size > 500:
376
+ status_score = 1
377
+ scores["status_report"] = status_score
378
  total += scores["status_report"]
379
 
380
  return scores, total, 10
 
394
  scores, total, max_score = scorer(results_dir)
395
  antipatterns = detect_antipatterns(results_dir)
396
 
 
397
  penalty = min(3, len(antipatterns))
398
  final = max(0, total - penalty)
399
 
 
404
  print(f"{'='*50}")
405
 
406
  for criterion, value in scores.items():
407
+ status = "+" if value > 0 else "-"
408
  print(f" {status} {criterion}: {value}")
409
 
410
  print(f"\n Subtotal: {total}/{max_score}")
411
 
412
  if antipatterns:
413
+ print(f"\n Anti-patterns (-{penalty}):")
414
  for ap in antipatterns:
415
+ print(f" ! {ap}")
416
 
417
  print(f"\n FINAL SCORE: {final}/{max_score}")
418
  return {"challenge": challenge, "name": name, "model": model_name,
 
421
 
422
 
423
  def main():
424
+ parser = argparse.ArgumentParser(description="Score agent benchmark challenges (v2 — deep validation)")
425
  parser.add_argument("--challenge", type=int, help="Challenge number (1-4)")
426
+ parser.add_argument("--all", action="store_true")
427
  parser.add_argument("--results-dir", help="Results directory")
428
+ parser.add_argument("--model-name", default="")
 
 
429
  args = parser.parse_args()
430
 
431
+ if args.all:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
432
  for ch in range(1, 5):
433
  rdir = args.results_dir or f"/tmp/agent-results-{args.model_name or 'unknown'}-ch{ch}"
434
  if os.path.exists(rdir):
435
  score_one(ch, rdir, args.model_name)
 
 
 
436
  elif args.challenge:
437
+ rdir = args.results_dir or f"/tmp/agent-results-{args.model_name}-ch{args.challenge}"
438
  if os.path.exists(rdir):
439
  score_one(args.challenge, rdir, args.model_name)
 
 
440
  else:
441
  parser.print_help()
442