genarena 0.0.1__py3-none-any.whl → 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. genarena/__init__.py +49 -2
  2. genarena/__main__.py +10 -0
  3. genarena/arena.py +1685 -0
  4. genarena/battle.py +337 -0
  5. genarena/bt_elo.py +507 -0
  6. genarena/cli.py +1581 -0
  7. genarena/data.py +476 -0
  8. genarena/deploy/Dockerfile +22 -0
  9. genarena/deploy/README.md +55 -0
  10. genarena/deploy/__init__.py +5 -0
  11. genarena/deploy/app.py +84 -0
  12. genarena/experiments.py +121 -0
  13. genarena/leaderboard.py +270 -0
  14. genarena/logs.py +409 -0
  15. genarena/models.py +412 -0
  16. genarena/prompts/__init__.py +127 -0
  17. genarena/prompts/mmrb2.py +373 -0
  18. genarena/sampling.py +336 -0
  19. genarena/state.py +656 -0
  20. genarena/sync/__init__.py +105 -0
  21. genarena/sync/auto_commit.py +118 -0
  22. genarena/sync/deploy_ops.py +543 -0
  23. genarena/sync/git_ops.py +422 -0
  24. genarena/sync/hf_ops.py +891 -0
  25. genarena/sync/init_ops.py +431 -0
  26. genarena/sync/packer.py +587 -0
  27. genarena/sync/submit.py +837 -0
  28. genarena/utils.py +103 -0
  29. genarena/validation/__init__.py +19 -0
  30. genarena/validation/schema.py +327 -0
  31. genarena/validation/validator.py +329 -0
  32. genarena/visualize/README.md +148 -0
  33. genarena/visualize/__init__.py +14 -0
  34. genarena/visualize/app.py +938 -0
  35. genarena/visualize/data_loader.py +2430 -0
  36. genarena/visualize/static/app.js +3762 -0
  37. genarena/visualize/static/model_aliases.json +86 -0
  38. genarena/visualize/static/style.css +4104 -0
  39. genarena/visualize/templates/index.html +413 -0
  40. genarena/vlm.py +519 -0
  41. genarena-0.1.1.dist-info/METADATA +178 -0
  42. genarena-0.1.1.dist-info/RECORD +44 -0
  43. {genarena-0.0.1.dist-info → genarena-0.1.1.dist-info}/WHEEL +1 -2
  44. genarena-0.1.1.dist-info/entry_points.txt +2 -0
  45. genarena-0.0.1.dist-info/METADATA +0 -26
  46. genarena-0.0.1.dist-info/RECORD +0 -5
  47. genarena-0.0.1.dist-info/top_level.txt +0 -1
@@ -0,0 +1,422 @@
1
+ # Copyright 2026 Ruihang Li.
2
+ # Licensed under the Apache License, Version 2.0.
3
+ # See LICENSE file in the project root for details.
4
+
5
+ """
6
+ Git operations module for GenArena.
7
+
8
+ This module provides Git version control functionality for arena data,
9
+ including initialization, commit, remote configuration, and push operations.
10
+ """
11
+
12
+ import logging
13
+ import os
14
+ import subprocess
15
+ from datetime import datetime
16
+ from pathlib import Path
17
+ from typing import Optional
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ # Patterns to exclude from Git tracking (models directories contain large image files)
22
+ GITIGNORE_PATTERNS = [
23
+ "# GenArena: Exclude model output images (large files)",
24
+ "*/models/",
25
+ "",
26
+ "# Python cache",
27
+ "__pycache__/",
28
+ "*.pyc",
29
+ "",
30
+ "# OS files",
31
+ ".DS_Store",
32
+ "Thumbs.db",
33
+ ]
34
+
35
+
36
+ def _run_git_command(
37
+ arena_dir: str,
38
+ args: list,
39
+ check: bool = True,
40
+ capture_output: bool = True,
41
+ ) -> subprocess.CompletedProcess:
42
+ """
43
+ Run a git command in the arena directory.
44
+
45
+ Args:
46
+ arena_dir: Path to the arena directory
47
+ args: Git command arguments (without 'git' prefix)
48
+ check: If True, raise exception on non-zero exit code
49
+ capture_output: If True, capture stdout and stderr
50
+
51
+ Returns:
52
+ CompletedProcess instance
53
+
54
+ Raises:
55
+ subprocess.CalledProcessError: If check=True and command fails
56
+ """
57
+ cmd = ["git"] + args
58
+ return subprocess.run(
59
+ cmd,
60
+ cwd=arena_dir,
61
+ check=check,
62
+ capture_output=capture_output,
63
+ text=True,
64
+ )
65
+
66
+
67
+ def is_git_initialized(arena_dir: str) -> bool:
68
+ """
69
+ Check if the arena directory is a Git repository.
70
+
71
+ Args:
72
+ arena_dir: Path to the arena directory
73
+
74
+ Returns:
75
+ True if Git is initialized, False otherwise
76
+ """
77
+ git_dir = os.path.join(arena_dir, ".git")
78
+ return os.path.isdir(git_dir)
79
+
80
+
81
+ def git_init(arena_dir: str) -> tuple[bool, str]:
82
+ """
83
+ Initialize a Git repository in the arena directory.
84
+
85
+ Args:
86
+ arena_dir: Path to the arena directory
87
+
88
+ Returns:
89
+ Tuple of (success, message)
90
+ """
91
+ if is_git_initialized(arena_dir):
92
+ return True, "Git repository already initialized"
93
+
94
+ # Ensure directory exists
95
+ os.makedirs(arena_dir, exist_ok=True)
96
+
97
+ try:
98
+ result = _run_git_command(arena_dir, ["init"])
99
+ logger.info(f"Initialized Git repository in {arena_dir}")
100
+
101
+ # Ensure .gitignore is set up
102
+ ensure_gitignore(arena_dir)
103
+
104
+ return True, "Git repository initialized successfully"
105
+ except subprocess.CalledProcessError as e:
106
+ error_msg = f"Failed to initialize Git repository: {e.stderr}"
107
+ logger.error(error_msg)
108
+ return False, error_msg
109
+
110
+
111
+ def ensure_gitignore(arena_dir: str) -> tuple[bool, str]:
112
+ """
113
+ Create or update .gitignore file to exclude models directories.
114
+
115
+ Args:
116
+ arena_dir: Path to the arena directory
117
+
118
+ Returns:
119
+ Tuple of (success, message)
120
+ """
121
+ gitignore_path = os.path.join(arena_dir, ".gitignore")
122
+
123
+ existing_content = ""
124
+ if os.path.isfile(gitignore_path):
125
+ with open(gitignore_path, "r", encoding="utf-8") as f:
126
+ existing_content = f.read()
127
+
128
+ # Check if the key pattern already exists
129
+ key_pattern = "*/models/"
130
+ if key_pattern in existing_content:
131
+ return True, ".gitignore already contains required patterns"
132
+
133
+ # Append patterns to existing content
134
+ new_content = existing_content
135
+ if new_content and not new_content.endswith("\n"):
136
+ new_content += "\n"
137
+
138
+ if new_content:
139
+ new_content += "\n"
140
+
141
+ new_content += "\n".join(GITIGNORE_PATTERNS)
142
+
143
+ with open(gitignore_path, "w", encoding="utf-8") as f:
144
+ f.write(new_content)
145
+
146
+ logger.info(f"Updated .gitignore in {arena_dir}")
147
+ return True, ".gitignore updated successfully"
148
+
149
+
150
+ def git_add_all(arena_dir: str) -> tuple[bool, str]:
151
+ """
152
+ Stage all changes in the arena directory (respecting .gitignore).
153
+
154
+ Args:
155
+ arena_dir: Path to the arena directory
156
+
157
+ Returns:
158
+ Tuple of (success, message)
159
+ """
160
+ if not is_git_initialized(arena_dir):
161
+ return False, "Git repository not initialized"
162
+
163
+ try:
164
+ _run_git_command(arena_dir, ["add", "-A"])
165
+ return True, "All changes staged"
166
+ except subprocess.CalledProcessError as e:
167
+ error_msg = f"Failed to stage changes: {e.stderr}"
168
+ logger.error(error_msg)
169
+ return False, error_msg
170
+
171
+
172
+ def has_uncommitted_changes(arena_dir: str) -> bool:
173
+ """
174
+ Check if there are uncommitted changes in the repository.
175
+
176
+ Args:
177
+ arena_dir: Path to the arena directory
178
+
179
+ Returns:
180
+ True if there are uncommitted changes, False otherwise
181
+ """
182
+ if not is_git_initialized(arena_dir):
183
+ return False
184
+
185
+ try:
186
+ # Check for staged changes
187
+ result = _run_git_command(arena_dir, ["diff", "--cached", "--quiet"], check=False)
188
+ if result.returncode != 0:
189
+ return True
190
+
191
+ # Check for unstaged changes
192
+ result = _run_git_command(arena_dir, ["diff", "--quiet"], check=False)
193
+ if result.returncode != 0:
194
+ return True
195
+
196
+ # Check for untracked files (that aren't ignored)
197
+ result = _run_git_command(
198
+ arena_dir,
199
+ ["ls-files", "--others", "--exclude-standard"],
200
+ check=False
201
+ )
202
+ if result.stdout.strip():
203
+ return True
204
+
205
+ return False
206
+ except Exception as e:
207
+ logger.warning(f"Error checking for uncommitted changes: {e}")
208
+ return False
209
+
210
+
211
+ def git_commit(
212
+ arena_dir: str,
213
+ message: Optional[str] = None,
214
+ command_name: Optional[str] = None,
215
+ ) -> tuple[bool, str]:
216
+ """
217
+ Commit staged changes.
218
+
219
+ Args:
220
+ arena_dir: Path to the arena directory
221
+ message: Custom commit message (optional)
222
+ command_name: Name of the command that triggered this commit (for auto-commit)
223
+
224
+ Returns:
225
+ Tuple of (success, message)
226
+ """
227
+ if not is_git_initialized(arena_dir):
228
+ return False, "Git repository not initialized"
229
+
230
+ # Stage all changes first
231
+ success, msg = git_add_all(arena_dir)
232
+ if not success:
233
+ return False, msg
234
+
235
+ # Check if there's anything to commit
236
+ result = _run_git_command(arena_dir, ["diff", "--cached", "--quiet"], check=False)
237
+ if result.returncode == 0:
238
+ return True, "Nothing to commit, working tree clean"
239
+
240
+ # Generate commit message
241
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
242
+ if message:
243
+ commit_msg = message
244
+ elif command_name:
245
+ commit_msg = f"[genarena] Auto commit after {command_name} at {timestamp}"
246
+ else:
247
+ commit_msg = f"[genarena] Auto commit at {timestamp}"
248
+
249
+ try:
250
+ _run_git_command(arena_dir, ["commit", "-m", commit_msg])
251
+ logger.info(f"Committed changes: {commit_msg}")
252
+ return True, f"Committed: {commit_msg}"
253
+ except subprocess.CalledProcessError as e:
254
+ error_msg = f"Failed to commit: {e.stderr}"
255
+ logger.error(error_msg)
256
+ return False, error_msg
257
+
258
+
259
+ def git_remote_get_url(arena_dir: str, remote_name: str = "origin") -> Optional[str]:
260
+ """
261
+ Get the URL of a remote repository.
262
+
263
+ Args:
264
+ arena_dir: Path to the arena directory
265
+ remote_name: Name of the remote (default: origin)
266
+
267
+ Returns:
268
+ Remote URL or None if not configured
269
+ """
270
+ if not is_git_initialized(arena_dir):
271
+ return None
272
+
273
+ try:
274
+ result = _run_git_command(
275
+ arena_dir,
276
+ ["remote", "get-url", remote_name],
277
+ check=False
278
+ )
279
+ if result.returncode == 0:
280
+ return result.stdout.strip()
281
+ return None
282
+ except Exception:
283
+ return None
284
+
285
+
286
+ def git_remote_add(
287
+ arena_dir: str,
288
+ url: str,
289
+ remote_name: str = "origin",
290
+ force: bool = False,
291
+ ) -> tuple[bool, str]:
292
+ """
293
+ Configure a remote repository.
294
+
295
+ Args:
296
+ arena_dir: Path to the arena directory
297
+ url: Remote repository URL
298
+ remote_name: Name of the remote (default: origin)
299
+ force: If True, overwrite existing remote URL
300
+
301
+ Returns:
302
+ Tuple of (success, message)
303
+ """
304
+ if not is_git_initialized(arena_dir):
305
+ return False, "Git repository not initialized"
306
+
307
+ existing_url = git_remote_get_url(arena_dir, remote_name)
308
+
309
+ if existing_url:
310
+ if existing_url == url:
311
+ return True, f"Remote '{remote_name}' already configured with this URL"
312
+
313
+ if not force:
314
+ return False, (
315
+ f"Remote '{remote_name}' already exists with URL: {existing_url}. "
316
+ f"Use --force to overwrite."
317
+ )
318
+
319
+ # Remove existing remote
320
+ try:
321
+ _run_git_command(arena_dir, ["remote", "remove", remote_name])
322
+ except subprocess.CalledProcessError as e:
323
+ return False, f"Failed to remove existing remote: {e.stderr}"
324
+
325
+ # Add remote
326
+ try:
327
+ _run_git_command(arena_dir, ["remote", "add", remote_name, url])
328
+ logger.info(f"Added remote '{remote_name}': {url}")
329
+ return True, f"Remote '{remote_name}' configured: {url}"
330
+ except subprocess.CalledProcessError as e:
331
+ error_msg = f"Failed to add remote: {e.stderr}"
332
+ logger.error(error_msg)
333
+ return False, error_msg
334
+
335
+
336
+ def git_push(
337
+ arena_dir: str,
338
+ remote_name: str = "origin",
339
+ branch: Optional[str] = None,
340
+ set_upstream: bool = True,
341
+ ) -> tuple[bool, str]:
342
+ """
343
+ Push commits to the remote repository.
344
+
345
+ Args:
346
+ arena_dir: Path to the arena directory
347
+ remote_name: Name of the remote (default: origin)
348
+ branch: Branch name (default: current branch)
349
+ set_upstream: If True, set upstream tracking
350
+
351
+ Returns:
352
+ Tuple of (success, message)
353
+ """
354
+ if not is_git_initialized(arena_dir):
355
+ return False, "Git repository not initialized"
356
+
357
+ # Check if remote is configured
358
+ remote_url = git_remote_get_url(arena_dir, remote_name)
359
+ if not remote_url:
360
+ return False, f"Remote '{remote_name}' not configured. Use 'genarena git remote --url <url>' first."
361
+
362
+ # Get current branch if not specified
363
+ if not branch:
364
+ try:
365
+ result = _run_git_command(arena_dir, ["branch", "--show-current"])
366
+ branch = result.stdout.strip()
367
+ if not branch:
368
+ # Might be on a detached HEAD, try to get default branch
369
+ branch = "main"
370
+ except subprocess.CalledProcessError:
371
+ branch = "main"
372
+
373
+ # Push
374
+ try:
375
+ push_args = ["push"]
376
+ if set_upstream:
377
+ push_args.extend(["-u", remote_name, branch])
378
+ else:
379
+ push_args.extend([remote_name, branch])
380
+
381
+ _run_git_command(arena_dir, push_args)
382
+ logger.info(f"Pushed to {remote_name}/{branch}")
383
+ return True, f"Pushed to {remote_name}/{branch}"
384
+ except subprocess.CalledProcessError as e:
385
+ error_msg = f"Failed to push: {e.stderr}"
386
+ logger.error(error_msg)
387
+ return False, error_msg
388
+
389
+
390
+ def git_sync(arena_dir: str) -> tuple[bool, str]:
391
+ """
392
+ Commit all changes and push to remote (one-click sync).
393
+
394
+ Args:
395
+ arena_dir: Path to the arena directory
396
+
397
+ Returns:
398
+ Tuple of (success, message)
399
+ """
400
+ if not is_git_initialized(arena_dir):
401
+ return False, "Git repository not initialized"
402
+
403
+ messages = []
404
+
405
+ # Commit changes
406
+ success, msg = git_commit(arena_dir)
407
+ messages.append(msg)
408
+
409
+ if not success and "Nothing to commit" not in msg:
410
+ return False, msg
411
+
412
+ # Push to remote
413
+ success, msg = git_push(arena_dir)
414
+ messages.append(msg)
415
+
416
+ if not success:
417
+ # If push fails due to no remote, still return partial success
418
+ if "not configured" in msg:
419
+ return True, f"{messages[0]} (push skipped: {msg})"
420
+ return False, msg
421
+
422
+ return True, " | ".join(messages)