yxc20098 commited on
Commit
9fead46
·
1 Parent(s): afa3975

Add agent URL hyperlinks, replay downloads, and submit_with_replay endpoint

Browse files

- Agent names link to GitHub/project URL when agent_url is provided
- Replay column shows download links for entries with replays
- New submit_with_replay API endpoint accepts replay file uploads
- Serves replay files from submissions/ directory via allowed_paths
- Added agent_url column to CSV schema

Files changed (3) hide show
  1. app.py +77 -5
  2. data/results.csv +2 -2
  3. tests/test_app.py +113 -0
app.py CHANGED
@@ -45,6 +45,7 @@ DISPLAY_COLUMNS = [
45
  "Avg Economy",
46
  "Avg Game Length",
47
  "Date",
 
48
  ]
49
 
50
 
@@ -57,9 +58,33 @@ def load_data() -> pd.DataFrame:
57
  df = df.sort_values("score", ascending=False).reset_index(drop=True)
58
  df.insert(0, "Rank", range(1, len(df) + 1))
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  # Rename for display
61
  df = df.rename(columns={
62
- "agent_name": "Agent",
63
  "agent_type": "Type",
64
  "opponent": "Opponent",
65
  "games": "Games",
@@ -161,7 +186,7 @@ def save_submission(results: dict) -> None:
161
  fieldnames = [
162
  "agent_name", "agent_type", "opponent", "games", "win_rate",
163
  "score", "avg_kills", "avg_deaths", "kd_ratio", "avg_economy",
164
- "avg_game_length", "timestamp", "replay_url",
165
  ]
166
  with open(csv_path, "a", newline="") as f:
167
  writer = csv.DictWriter(f, fieldnames=fieldnames)
@@ -235,6 +260,7 @@ def _score_from_submission(data: dict) -> dict:
235
  "avg_game_length": data.get("ticks", 0),
236
  "timestamp": data.get("timestamp", datetime.now(timezone.utc).strftime("%Y-%m-%d"))[:10],
237
  "replay_url": "",
 
238
  }
239
 
240
 
@@ -291,6 +317,39 @@ def handle_api_submit(json_data: str) -> str:
291
  )
292
 
293
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  # ── UI ────────────────────────────────────────────────────────────────────────
295
 
296
  ABOUT_MD = """
@@ -441,7 +500,7 @@ def build_app() -> gr.Blocks:
441
  value=initial_df,
442
  datatype=[
443
  "number", # Rank
444
- "str", # Agent
445
  "html", # Type (badge)
446
  "str", # Opponent
447
  "number", # Games
@@ -453,6 +512,7 @@ def build_app() -> gr.Blocks:
453
  "number", # Avg Economy
454
  "number", # Avg Game Length
455
  "str", # Date
 
456
  ],
457
  interactive=False,
458
  show_label=False,
@@ -498,7 +558,7 @@ def build_app() -> gr.Blocks:
498
  outputs=[submit_output, leaderboard],
499
  )
500
 
501
- # API endpoint for CLI auto-upload
502
  api_json_input = gr.Textbox(visible=False)
503
  api_result = gr.Textbox(visible=False)
504
  api_btn = gr.Button(visible=False)
@@ -509,6 +569,18 @@ def build_app() -> gr.Blocks:
509
  api_name="submit",
510
  )
511
 
 
 
 
 
 
 
 
 
 
 
 
 
512
  gr.Markdown(SUBMIT_MD)
513
 
514
  return app
@@ -516,4 +588,4 @@ def build_app() -> gr.Blocks:
516
 
517
  if __name__ == "__main__":
518
  app = build_app()
519
- app.launch()
 
45
  "Avg Economy",
46
  "Avg Game Length",
47
  "Date",
48
+ "Replay",
49
  ]
50
 
51
 
 
58
  df = df.sort_values("score", ascending=False).reset_index(drop=True)
59
  df.insert(0, "Rank", range(1, len(df) + 1))
60
 
61
+ # Build agent name with optional hyperlink
62
+ if "agent_url" in df.columns:
63
+ df["Agent"] = df.apply(
64
+ lambda r: (
65
+ f'<a href="{r["agent_url"]}" target="_blank">{r["agent_name"]}</a>'
66
+ if pd.notna(r.get("agent_url")) and str(r["agent_url"]).strip()
67
+ else r["agent_name"]
68
+ ),
69
+ axis=1,
70
+ )
71
+ else:
72
+ df["Agent"] = df["agent_name"]
73
+
74
+ # Build replay download link
75
+ if "replay_url" in df.columns:
76
+ df["Replay"] = df["replay_url"].apply(
77
+ lambda u: (
78
+ f'<a href="/replays/{u}" download title="Download replay">&#11015;</a>'
79
+ if pd.notna(u) and str(u).strip()
80
+ else ""
81
+ )
82
+ )
83
+ else:
84
+ df["Replay"] = ""
85
+
86
  # Rename for display
87
  df = df.rename(columns={
 
88
  "agent_type": "Type",
89
  "opponent": "Opponent",
90
  "games": "Games",
 
186
  fieldnames = [
187
  "agent_name", "agent_type", "opponent", "games", "win_rate",
188
  "score", "avg_kills", "avg_deaths", "kd_ratio", "avg_economy",
189
+ "avg_game_length", "timestamp", "replay_url", "agent_url",
190
  ]
191
  with open(csv_path, "a", newline="") as f:
192
  writer = csv.DictWriter(f, fieldnames=fieldnames)
 
260
  "avg_game_length": data.get("ticks", 0),
261
  "timestamp": data.get("timestamp", datetime.now(timezone.utc).strftime("%Y-%m-%d"))[:10],
262
  "replay_url": "",
263
+ "agent_url": data.get("agent_url", ""),
264
  }
265
 
266
 
 
317
  )
318
 
319
 
320
+ def handle_api_submit_with_replay(json_data: str, replay_file) -> str:
321
+ """API endpoint: accept JSON + replay file. Used by CLI with --replay."""
322
+ try:
323
+ data = json.loads(json_data)
324
+ except (json.JSONDecodeError, Exception) as e:
325
+ return f"Invalid JSON: {e}"
326
+
327
+ is_valid, error = validate_submission(data)
328
+ if not is_valid:
329
+ return f"Validation error: {error}"
330
+
331
+ results_row = _score_from_submission(data)
332
+
333
+ # Save replay if provided
334
+ if replay_file is not None:
335
+ import shutil
336
+ from datetime import datetime, timezone
337
+
338
+ orig = Path(replay_file) if isinstance(replay_file, str) else Path(replay_file.name)
339
+ ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
340
+ slug = data["agent_name"].replace("/", "_").replace(" ", "_")[:30]
341
+ replay_name = f"replay-{slug}-{ts}.orarep"
342
+ shutil.copy2(str(orig), SUBMISSIONS_DIR / replay_name)
343
+ results_row["replay_url"] = replay_name
344
+
345
+ save_submission(results_row)
346
+
347
+ return (
348
+ f"OK: {data['agent_name']} ({data['agent_type']}) "
349
+ f"vs {data['opponent']}: score {results_row['score']}"
350
+ )
351
+
352
+
353
  # ── UI ────────────────────────────────────────────────────────────────────────
354
 
355
  ABOUT_MD = """
 
500
  value=initial_df,
501
  datatype=[
502
  "number", # Rank
503
+ "html", # Agent (may contain hyperlink)
504
  "html", # Type (badge)
505
  "str", # Opponent
506
  "number", # Games
 
512
  "number", # Avg Economy
513
  "number", # Avg Game Length
514
  "str", # Date
515
+ "html", # Replay (download link)
516
  ],
517
  interactive=False,
518
  show_label=False,
 
558
  outputs=[submit_output, leaderboard],
559
  )
560
 
561
+ # API endpoint for CLI auto-upload (JSON only)
562
  api_json_input = gr.Textbox(visible=False)
563
  api_result = gr.Textbox(visible=False)
564
  api_btn = gr.Button(visible=False)
 
569
  api_name="submit",
570
  )
571
 
572
+ # API endpoint for CLI upload with replay
573
+ api_json_input2 = gr.Textbox(visible=False)
574
+ api_replay_input = gr.File(visible=False)
575
+ api_result2 = gr.Textbox(visible=False)
576
+ api_btn2 = gr.Button(visible=False)
577
+ api_btn2.click(
578
+ fn=handle_api_submit_with_replay,
579
+ inputs=[api_json_input2, api_replay_input],
580
+ outputs=[api_result2],
581
+ api_name="submit_with_replay",
582
+ )
583
+
584
  gr.Markdown(SUBMIT_MD)
585
 
586
  return app
 
588
 
589
  if __name__ == "__main__":
590
  app = build_app()
591
+ app.launch(allowed_paths=[str(SUBMISSIONS_DIR)])
data/results.csv CHANGED
@@ -1,2 +1,2 @@
1
- agent_name,agent_type,opponent,games,win_rate,score,avg_kills,avg_deaths,kd_ratio,avg_economy,avg_game_length,timestamp,replay_url
2
- qwen/qwen3-coder-next,LLM,Beginner,1,0.0,18.3,1000,2900,0.34,9050,27349,2026-02-25,
 
1
+ agent_name,agent_type,opponent,games,win_rate,score,avg_kills,avg_deaths,kd_ratio,avg_economy,avg_game_length,timestamp,replay_url,agent_url
2
+ qwen/qwen3-coder-next,LLM,Beginner,1,0.0,18.3,1000,2900,0.34,9050,27349,2026-02-25,,
tests/test_app.py CHANGED
@@ -17,6 +17,7 @@ from app import (
17
  build_app,
18
  filter_leaderboard,
19
  handle_api_submit,
 
20
  load_data,
21
  validate_submission,
22
  )
@@ -195,3 +196,115 @@ class TestApiSubmit:
195
  import json
196
  result = handle_api_submit(json.dumps({"agent_name": "Bot"}))
197
  assert "Validation error" in result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  build_app,
18
  filter_leaderboard,
19
  handle_api_submit,
20
+ handle_api_submit_with_replay,
21
  load_data,
22
  validate_submission,
23
  )
 
196
  import json
197
  result = handle_api_submit(json.dumps({"agent_name": "Bot"}))
198
  assert "Validation error" in result
199
+
200
+
201
+ class TestDisplayColumns:
202
+ """Test display column configuration."""
203
+
204
+ def test_replay_in_display_columns(self):
205
+ assert "Replay" in DISPLAY_COLUMNS
206
+
207
+ def test_display_columns_count(self):
208
+ assert len(DISPLAY_COLUMNS) == 14
209
+
210
+
211
+ class TestAgentUrl:
212
+ """Test agent URL hyperlink rendering."""
213
+
214
+ def test_agent_url_in_submission(self):
215
+ import json
216
+ import tempfile
217
+ data = {
218
+ "agent_name": "DeathBot",
219
+ "agent_type": "RL",
220
+ "agent_url": "https://github.com/user/deathbot",
221
+ "opponent": "Normal",
222
+ "result": "win",
223
+ "ticks": 5000,
224
+ "kills_cost": 3000,
225
+ "deaths_cost": 1000,
226
+ "assets_value": 8000,
227
+ }
228
+ with tempfile.NamedTemporaryFile(suffix=".csv", delete=False) as f:
229
+ temp_path = Path(f.name)
230
+ with patch("app.DATA_PATH", temp_path):
231
+ result = handle_api_submit(json.dumps(data))
232
+ assert "OK" in result
233
+ assert "DeathBot" in result
234
+ temp_path.unlink(missing_ok=True)
235
+
236
+ def test_agent_url_renders_link(self):
237
+ """When agent_url is set, Agent column should be a hyperlink."""
238
+ import tempfile
239
+ csv_content = (
240
+ "agent_name,agent_type,opponent,games,win_rate,score,"
241
+ "avg_kills,avg_deaths,kd_ratio,avg_economy,avg_game_length,"
242
+ "timestamp,replay_url,agent_url\n"
243
+ "DeathBot,RL,Normal,10,50.0,60.0,"
244
+ "2000,1500,1.33,9000,15000,"
245
+ "2026-02-26,,https://github.com/user/deathbot\n"
246
+ )
247
+ with tempfile.NamedTemporaryFile(
248
+ mode="w", suffix=".csv", delete=False
249
+ ) as f:
250
+ f.write(csv_content)
251
+ temp_path = Path(f.name)
252
+ with patch("app.DATA_PATH", temp_path):
253
+ df = load_data()
254
+ assert '<a href="https://github.com/user/deathbot"' in df["Agent"].iloc[0]
255
+ temp_path.unlink(missing_ok=True)
256
+
257
+ def test_no_url_renders_plain_name(self):
258
+ """When agent_url is empty, Agent column is plain text."""
259
+ import tempfile
260
+ csv_content = (
261
+ "agent_name,agent_type,opponent,games,win_rate,score,"
262
+ "avg_kills,avg_deaths,kd_ratio,avg_economy,avg_game_length,"
263
+ "timestamp,replay_url,agent_url\n"
264
+ "PlainBot,LLM,Easy,5,20.0,30.0,"
265
+ "1000,2000,0.5,5000,10000,"
266
+ "2026-02-26,,\n"
267
+ )
268
+ with tempfile.NamedTemporaryFile(
269
+ mode="w", suffix=".csv", delete=False
270
+ ) as f:
271
+ f.write(csv_content)
272
+ temp_path = Path(f.name)
273
+ with patch("app.DATA_PATH", temp_path):
274
+ df = load_data()
275
+ assert df["Agent"].iloc[0] == "PlainBot"
276
+ temp_path.unlink(missing_ok=True)
277
+
278
+
279
+ class TestReplayColumn:
280
+ """Test replay download link rendering."""
281
+
282
+ def test_replay_link_rendered(self):
283
+ """Replay column shows download link when replay_url is set."""
284
+ import tempfile
285
+ csv_content = (
286
+ "agent_name,agent_type,opponent,games,win_rate,score,"
287
+ "avg_kills,avg_deaths,kd_ratio,avg_economy,avg_game_length,"
288
+ "timestamp,replay_url,agent_url\n"
289
+ "TestBot,LLM,Easy,1,0.0,18.0,"
290
+ "1000,2000,0.5,5000,10000,"
291
+ "2026-02-26,replay-test-123.orarep,\n"
292
+ )
293
+ with tempfile.NamedTemporaryFile(
294
+ mode="w", suffix=".csv", delete=False
295
+ ) as f:
296
+ f.write(csv_content)
297
+ temp_path = Path(f.name)
298
+ with patch("app.DATA_PATH", temp_path):
299
+ df = load_data()
300
+ assert "/replays/replay-test-123.orarep" in df["Replay"].iloc[0]
301
+ assert "download" in df["Replay"].iloc[0]
302
+ temp_path.unlink(missing_ok=True)
303
+
304
+ def test_empty_replay_no_link(self):
305
+ """Replay column is empty when no replay_url."""
306
+ df = load_data()
307
+ if len(df) > 0:
308
+ # The default test data has no replay
309
+ replay_val = df["Replay"].iloc[0]
310
+ assert replay_val == "" or not str(replay_val).strip()