zhimin-z commited on
Commit
d277f5a
·
0 Parent(s):

first commit

Browse files
Files changed (7) hide show
  1. .gitattributes +35 -0
  2. .github/workflows/hf_sync.yml +35 -0
  3. .gitignore +6 -0
  4. README.md +66 -0
  5. app.py +661 -0
  6. msr.py +715 -0
  7. requirements.txt +10 -0
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.github/workflows/hf_sync.yml ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Sync to Hugging Face Space
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ jobs:
9
+ sync:
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - name: Checkout GitHub Repository
14
+ uses: actions/checkout@v3
15
+ with:
16
+ fetch-depth: 0 # Fetch the entire history to avoid shallow clone issues
17
+
18
+ - name: Install Git LFS
19
+ run: |
20
+ curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | sudo bash
21
+ sudo apt-get install git-lfs
22
+ git lfs install
23
+
24
+ - name: Configure Git
25
+ run: |
26
+ git config --global user.name "GitHub Actions Bot"
27
+ git config --global user.email "actions@github.com"
28
+
29
+ - name: Push to Hugging Face
30
+ env:
31
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
32
+ run: |
33
+ git remote add huggingface https://user:${HF_TOKEN}@huggingface.co/spaces/SWE-Arena/SWE-Community
34
+ git fetch huggingface
35
+ git push huggingface main --force
.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ *.claude
2
+ *.env
3
+ *.venv
4
+ *.ipynb
5
+ *.pyc
6
+ *.duckdb
README.md ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: SWE-Community
3
+ emoji: 🌐
4
+ colorFrom: green
5
+ colorTo: blue
6
+ sdk: gradio
7
+ sdk_version: 5.50.0
8
+ app_file: app.py
9
+ hf_oauth: true
10
+ pinned: false
11
+ short_description: Track GitHub community statistics for SWE assistants
12
+ ---
13
+
14
+ # SWE Assistant Community Leaderboard
15
+
16
+ SWE-Community ranks software engineering assistants by their real-world GitHub community activity: wiki edits and team membership events.
17
+
18
+ No benchmarks. No sandboxes. Just real community activity tracked from public repositories.
19
+
20
+ ## Why This Exists
21
+
22
+ Most AI coding assistant benchmarks use synthetic tasks and simulated environments. This leaderboard measures real-world activity: how many wiki pages is the assistant editing? How many membership events is it generating? Is the assistant's community engagement growing?
23
+
24
+ If an assistant is consistently active across different projects, that tells you something no benchmark can.
25
+
26
+ ## What We Track
27
+
28
+ Key metrics from the last 180 days:
29
+
30
+ **Leaderboard Table**
31
+ - **Assistant Name**: Display name of the assistant
32
+ - **Website**: Link to the assistant's homepage or documentation
33
+ - **Total Wiki Edits**: Total number of wiki pages edited by the assistant
34
+ - **Total Membership Events**: Number of team membership changes performed by the assistant
35
+
36
+ **Monthly Trends**
37
+ - Wiki edit volume over time (bar charts)
38
+ - Membership event volume over time (bar charts)
39
+ - Activity patterns across months
40
+
41
+ We focus on 180 days to highlight current capabilities and active assistants.
42
+
43
+ ## How It Works
44
+
45
+ **Data Collection**
46
+ We mine GitHub activity from [GHArchive](https://www.gharchive.org/), tracking:
47
+ - Wiki pages edited by the assistant (`GollumEvent` data)
48
+ - Membership events by the assistant (`MemberEvent` data)
49
+
50
+ **Regular Updates**
51
+ Leaderboard refreshes daily
52
+
53
+ **Community Submissions**
54
+ Anyone can submit an assistant. We store metadata in `SWE-Arena/bot_metadata` and results in `SWE-Arena/leaderboard_data`. All submissions are validated via GitHub API.
55
+
56
+ ## What's Next
57
+
58
+ Planned improvements:
59
+ - Repository-based analysis (which repos are assistants active in)
60
+ - Extended metrics (wiki page types, membership roles, access levels)
61
+ - Organization and team breakdown
62
+ - Activity patterns (page creations, updates, invitations, removals)
63
+
64
+ ## Questions or Issues?
65
+
66
+ [Open an issue](https://github.com/SWE-Arena/SWE-Community/issues) for bugs, feature requests, or data concerns.
app.py ADDED
@@ -0,0 +1,661 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from gradio_leaderboard import Leaderboard
3
+ import json
4
+ import os
5
+ import time
6
+ import requests
7
+ from huggingface_hub import HfApi, hf_hub_download
8
+ from huggingface_hub.errors import HfHubHTTPError
9
+ import backoff
10
+ from dotenv import load_dotenv
11
+ import pandas as pd
12
+ import random
13
+ import plotly.graph_objects as go
14
+ from apscheduler.schedulers.background import BackgroundScheduler
15
+ from apscheduler.triggers.cron import CronTrigger
16
+
17
+ # Load environment variables
18
+ load_dotenv(override=True)
19
+
20
+ # =============================================================================
21
+ # CONFIGURATION
22
+ # =============================================================================
23
+
24
+ AGENTS_REPO = "SWE-Arena/bot_data" # HuggingFace dataset for assistant metadata
25
+ LEADERBOARD_FILENAME = f"{os.getenv('COMPOSE_PROJECT_NAME')}.json"
26
+ LEADERBOARD_REPO = "SWE-Arena/leaderboard_data" # HuggingFace dataset for leaderboard data
27
+ MAX_RETRIES = 5
28
+
29
+ LEADERBOARD_COLUMNS = [
30
+ ("Assistant", "string"),
31
+ ("Website", "string"),
32
+ ("Total Wiki Edits", "number"),
33
+ ("Total Membership Events", "number"),
34
+ ]
35
+
36
+ # =============================================================================
37
+ # HUGGINGFACE API WRAPPERS WITH BACKOFF
38
+ # =============================================================================
39
+
40
+ def is_rate_limit_error(e):
41
+ """Check if exception is a HuggingFace rate limit error (429)."""
42
+ if isinstance(e, HfHubHTTPError):
43
+ return e.response.status_code == 429
44
+ return False
45
+
46
+
47
+ @backoff.on_exception(
48
+ backoff.expo,
49
+ HfHubHTTPError,
50
+ max_tries=MAX_RETRIES,
51
+ base=300,
52
+ max_value=3600,
53
+ giveup=lambda e: not is_rate_limit_error(e),
54
+ on_backoff=lambda details: print(
55
+ f"Rate limited. Retrying in {details['wait']/60:.1f} minutes ({details['wait']:.0f}s) - attempt {details['tries']}/5..."
56
+ )
57
+ )
58
+ def list_repo_files_with_backoff(api, **kwargs):
59
+ """Wrapper for api.list_repo_files() with exponential backoff for rate limits."""
60
+ return api.list_repo_files(**kwargs)
61
+
62
+
63
+ @backoff.on_exception(
64
+ backoff.expo,
65
+ HfHubHTTPError,
66
+ max_tries=MAX_RETRIES,
67
+ base=300,
68
+ max_value=3600,
69
+ giveup=lambda e: not is_rate_limit_error(e),
70
+ on_backoff=lambda details: print(
71
+ f"Rate limited. Retrying in {details['wait']/60:.1f} minutes ({details['wait']:.0f}s) - attempt {details['tries']}/5..."
72
+ )
73
+ )
74
+ def hf_hub_download_with_backoff(**kwargs):
75
+ """Wrapper for hf_hub_download() with exponential backoff for rate limits."""
76
+ return hf_hub_download(**kwargs)
77
+
78
+
79
+ # =============================================================================
80
+ # GITHUB USERNAME VALIDATION
81
+ # =============================================================================
82
+
83
+ def validate_github_username(identifier):
84
+ """Verify that a GitHub identifier exists."""
85
+ try:
86
+ response = requests.get(f'https://api.github.com/users/{identifier}', timeout=10)
87
+ return (True, "Username is valid") if response.status_code == 200 else (False, "GitHub identifier not found" if response.status_code == 404 else f"Validation error: HTTP {response.status_code}")
88
+ except Exception as e:
89
+ return False, f"Validation error: {str(e)}"
90
+
91
+
92
+ # =============================================================================
93
+ # HUGGINGFACE DATASET OPERATIONS
94
+ # =============================================================================
95
+
96
+ def load_agents_from_hf():
97
+ """Load all assistant metadata JSON files from HuggingFace dataset."""
98
+ try:
99
+ api = HfApi()
100
+ assistants = []
101
+
102
+ # List all files in the repository
103
+ files = list_repo_files_with_backoff(api=api, repo_id=AGENTS_REPO, repo_type="dataset")
104
+
105
+ # Filter for JSON files only
106
+ json_files = [f for f in files if f.endswith('.json')]
107
+
108
+ # Download and parse each JSON file
109
+ for json_file in json_files:
110
+ try:
111
+ file_path = hf_hub_download_with_backoff(
112
+ repo_id=AGENTS_REPO,
113
+ filename=json_file,
114
+ repo_type="dataset"
115
+ )
116
+
117
+ with open(file_path, 'r') as f:
118
+ agent_data = json.load(f)
119
+
120
+ # Only process assistants with status == "active"
121
+ if agent_data.get('status') != 'active':
122
+ continue
123
+
124
+ # Extract github_identifier from filename (e.g., "assistant[bot].json" -> "assistant[bot]")
125
+ filename_identifier = json_file.replace('.json', '')
126
+
127
+ # Add or override github_identifier to match filename
128
+ agent_data['github_identifier'] = filename_identifier
129
+
130
+ assistants.append(agent_data)
131
+
132
+ except Exception as e:
133
+ print(f"Warning: Could not load {json_file}: {str(e)}")
134
+ continue
135
+
136
+ print(f"Loaded {len(assistants)} assistants from HuggingFace")
137
+ return assistants
138
+
139
+ except Exception as e:
140
+ print(f"Could not load assistants from HuggingFace: {str(e)}")
141
+ return None
142
+
143
+
144
+ def get_hf_token():
145
+ """Get HuggingFace token from environment variables."""
146
+ token = os.getenv('HF_TOKEN')
147
+ if not token:
148
+ print("Warning: HF_TOKEN not found in environment variables")
149
+ return token
150
+
151
+
152
+ def upload_with_retry(api, path_or_fileobj, path_in_repo, repo_id, repo_type, token, max_retries=5):
153
+ """Upload file to HuggingFace with exponential backoff retry logic."""
154
+ delay = 2.0
155
+
156
+ for attempt in range(max_retries):
157
+ try:
158
+ api.upload_file(
159
+ path_or_fileobj=path_or_fileobj,
160
+ path_in_repo=path_in_repo,
161
+ repo_id=repo_id,
162
+ repo_type=repo_type,
163
+ token=token
164
+ )
165
+ if attempt > 0:
166
+ print(f" Upload succeeded on attempt {attempt + 1}/{max_retries}")
167
+ return True
168
+
169
+ except Exception as e:
170
+ if attempt < max_retries - 1:
171
+ wait_time = delay + random.uniform(0, 1.0)
172
+ print(f" Upload failed (attempt {attempt + 1}/{max_retries}): {str(e)}")
173
+ print(f" Retrying in {wait_time:.1f} seconds...")
174
+ time.sleep(wait_time)
175
+ delay = min(delay * 2, 60.0)
176
+ else:
177
+ print(f" Upload failed after {max_retries} attempts: {str(e)}")
178
+ raise
179
+
180
+
181
+ def save_agent_to_hf(data):
182
+ """Save a new assistant to HuggingFace dataset as {identifier}.json in root."""
183
+ try:
184
+ api = HfApi()
185
+ token = get_hf_token()
186
+
187
+ if not token:
188
+ raise Exception("No HuggingFace token found. Please set HF_TOKEN in your Space settings.")
189
+
190
+ identifier = data['github_identifier']
191
+ filename = f"{identifier}.json"
192
+
193
+ # Save locally first
194
+ with open(filename, 'w') as f:
195
+ json.dump(data, f, indent=2)
196
+
197
+ try:
198
+ # Upload to HuggingFace (root directory)
199
+ upload_with_retry(
200
+ api=api,
201
+ path_or_fileobj=filename,
202
+ path_in_repo=filename,
203
+ repo_id=AGENTS_REPO,
204
+ repo_type="dataset",
205
+ token=token
206
+ )
207
+ print(f"Saved assistant to HuggingFace: {filename}")
208
+ return True
209
+ finally:
210
+ # Always clean up local file, even if upload fails
211
+ if os.path.exists(filename):
212
+ os.remove(filename)
213
+
214
+ except Exception as e:
215
+ print(f"Error saving assistant: {str(e)}")
216
+ return False
217
+
218
+
219
+ def load_leaderboard_data_from_hf():
220
+ """Load leaderboard data and monthly metrics from HuggingFace dataset."""
221
+ try:
222
+ token = get_hf_token()
223
+
224
+ # Download file
225
+ file_path = hf_hub_download_with_backoff(
226
+ repo_id=LEADERBOARD_REPO,
227
+ filename=LEADERBOARD_FILENAME,
228
+ repo_type="dataset",
229
+ token=token
230
+ )
231
+
232
+ # Load JSON data
233
+ with open(file_path, 'r') as f:
234
+ data = json.load(f)
235
+
236
+ last_updated = data.get('metadata', {}).get('last_updated', 'Unknown')
237
+ print(f"Loaded leaderboard data from HuggingFace (last updated: {last_updated})")
238
+
239
+ return data
240
+
241
+ except Exception as e:
242
+ print(f"Could not load leaderboard data from HuggingFace: {str(e)}")
243
+ return None
244
+
245
+
246
+ # =============================================================================
247
+ # UI FUNCTIONS
248
+ # =============================================================================
249
+
250
+ def _empty_plot(message="No data available for visualization"):
251
+ """Return an empty Plotly figure with a message."""
252
+ fig = go.Figure()
253
+ fig.add_annotation(
254
+ text=message,
255
+ xref="paper", yref="paper",
256
+ x=0.5, y=0.5, showarrow=False,
257
+ font=dict(size=16)
258
+ )
259
+ fig.update_layout(title=None, xaxis_title=None, height=500)
260
+ return fig
261
+
262
+
263
+ def _generate_color(index, total):
264
+ """Generate distinct colors using HSL color space for better distribution."""
265
+ hue = (index * 360 / total) % 360
266
+ saturation = 70 + (index % 3) * 10
267
+ lightness = 45 + (index % 2) * 10
268
+ return f'hsl({hue}, {saturation}%, {lightness}%)'
269
+
270
+
271
+ def create_monthly_wiki_plot(top_n=5):
272
+ """Create a Plotly figure showing monthly wiki edits as bar charts."""
273
+ saved_data = load_leaderboard_data_from_hf()
274
+
275
+ if not saved_data or 'monthly_metrics' not in saved_data:
276
+ return _empty_plot()
277
+
278
+ metrics = saved_data['monthly_metrics']
279
+
280
+ # Apply top_n filter
281
+ if top_n is not None and top_n > 0 and metrics.get('assistants'):
282
+ agent_totals = []
283
+ for agent_name in metrics['assistants']:
284
+ agent_data = metrics['data'].get(agent_name, {})
285
+ wiki_edits = sum(agent_data.get('total_wiki_edits', []))
286
+ agent_totals.append((agent_name, wiki_edits))
287
+
288
+ agent_totals.sort(key=lambda x: x[1], reverse=True)
289
+ top_agents = [name for name, _ in agent_totals[:top_n]]
290
+
291
+ metrics = {
292
+ 'assistants': top_agents,
293
+ 'months': metrics['months'],
294
+ 'data': {a: metrics['data'][a] for a in top_agents if a in metrics['data']}
295
+ }
296
+
297
+ if not metrics['assistants'] or not metrics['months']:
298
+ return _empty_plot()
299
+
300
+ fig = go.Figure()
301
+ assistants = metrics['assistants']
302
+ months = metrics['months']
303
+ data = metrics['data']
304
+
305
+ for idx, agent_name in enumerate(assistants):
306
+ color = _generate_color(idx, len(assistants))
307
+ agent_data = data[agent_name]
308
+
309
+ x_bars = []
310
+ y_bars = []
311
+ for month, count in zip(months, agent_data.get('total_wiki_edits', [])):
312
+ if count > 0:
313
+ x_bars.append(month)
314
+ y_bars.append(count)
315
+
316
+ if x_bars and y_bars:
317
+ fig.add_trace(
318
+ go.Bar(
319
+ x=x_bars, y=y_bars, name=agent_name,
320
+ marker=dict(color=color, opacity=0.7),
321
+ hovertemplate='<b>%{fullData.name}</b><br>Month: %{x}<br>Wiki Edits: %{y}<extra></extra>',
322
+ offsetgroup=agent_name
323
+ )
324
+ )
325
+
326
+ fig.update_xaxes(title_text=None)
327
+ fig.update_yaxes(title_text="<b>Wiki Edits</b>")
328
+
329
+ show_legend = (top_n is not None and top_n <= 10)
330
+ fig.update_layout(
331
+ title=None, hovermode='closest', barmode='group', height=600,
332
+ showlegend=show_legend,
333
+ margin=dict(l=50, r=150 if show_legend else 50, t=50, b=50)
334
+ )
335
+ return fig
336
+
337
+
338
+ def create_monthly_members_plot(top_n=5):
339
+ """Create a Plotly figure showing monthly membership events as bar charts."""
340
+ saved_data = load_leaderboard_data_from_hf()
341
+
342
+ if not saved_data or 'monthly_metrics' not in saved_data:
343
+ return _empty_plot()
344
+
345
+ metrics = saved_data['monthly_metrics']
346
+
347
+ # Apply top_n filter
348
+ if top_n is not None and top_n > 0 and metrics.get('assistants'):
349
+ agent_totals = []
350
+ for agent_name in metrics['assistants']:
351
+ agent_data = metrics['data'].get(agent_name, {})
352
+ total_members = sum(agent_data.get('total_members', []))
353
+ agent_totals.append((agent_name, total_members))
354
+
355
+ agent_totals.sort(key=lambda x: x[1], reverse=True)
356
+ top_agents = [name for name, _ in agent_totals[:top_n]]
357
+
358
+ metrics = {
359
+ 'assistants': top_agents,
360
+ 'months': metrics['months'],
361
+ 'data': {a: metrics['data'][a] for a in top_agents if a in metrics['data']}
362
+ }
363
+
364
+ if not metrics['assistants'] or not metrics['months']:
365
+ return _empty_plot()
366
+
367
+ fig = go.Figure()
368
+ assistants = metrics['assistants']
369
+ months = metrics['months']
370
+ data = metrics['data']
371
+
372
+ for idx, agent_name in enumerate(assistants):
373
+ color = _generate_color(idx, len(assistants))
374
+ agent_data = data[agent_name]
375
+
376
+ x_bars = []
377
+ y_bars = []
378
+ for month, count in zip(months, agent_data.get('total_members', [])):
379
+ if count > 0:
380
+ x_bars.append(month)
381
+ y_bars.append(count)
382
+
383
+ if x_bars and y_bars:
384
+ fig.add_trace(
385
+ go.Bar(
386
+ x=x_bars, y=y_bars, name=agent_name,
387
+ marker=dict(color=color, opacity=0.7),
388
+ hovertemplate='<b>%{fullData.name}</b><br>Month: %{x}<br>Membership Events: %{y}<extra></extra>',
389
+ offsetgroup=agent_name
390
+ )
391
+ )
392
+
393
+ fig.update_xaxes(title_text=None)
394
+ fig.update_yaxes(title_text="<b>Membership Events</b>")
395
+
396
+ show_legend = (top_n is not None and top_n <= 10)
397
+ fig.update_layout(
398
+ title=None, hovermode='closest', barmode='group', height=600,
399
+ showlegend=show_legend,
400
+ margin=dict(l=50, r=150 if show_legend else 50, t=50, b=50)
401
+ )
402
+ return fig
403
+
404
+
405
+ def get_leaderboard_dataframe():
406
+ """Load leaderboard from saved dataset and convert to pandas DataFrame for display."""
407
+ saved_data = load_leaderboard_data_from_hf()
408
+
409
+ if not saved_data or 'leaderboard' not in saved_data:
410
+ print(f"No leaderboard data available")
411
+ column_names = [col[0] for col in LEADERBOARD_COLUMNS]
412
+ return pd.DataFrame(columns=column_names)
413
+
414
+ cache_dict = saved_data['leaderboard']
415
+ last_updated = saved_data.get('metadata', {}).get('last_updated', 'Unknown')
416
+ print(f"Loaded leaderboard from saved dataset (last updated: {last_updated})")
417
+ print(f"Cache dict size: {len(cache_dict)}")
418
+
419
+ if not cache_dict:
420
+ print("WARNING: cache_dict is empty!")
421
+ column_names = [col[0] for col in LEADERBOARD_COLUMNS]
422
+ return pd.DataFrame(columns=column_names)
423
+
424
+ rows = []
425
+ filtered_count = 0
426
+ for identifier, data in cache_dict.items():
427
+ wiki_edits = data.get('total_wiki_edits', 0)
428
+ total_members = data.get('total_members', 0)
429
+
430
+ # Filter out assistants with zero activity across both metrics
431
+ if wiki_edits == 0 and total_members == 0:
432
+ filtered_count += 1
433
+ continue
434
+
435
+ rows.append([
436
+ data.get('name', 'Unknown'),
437
+ data.get('website', 'N/A'),
438
+ wiki_edits,
439
+ total_members,
440
+ ])
441
+
442
+ print(f"Filtered out {filtered_count} assistants with 0 activity")
443
+ print(f"Leaderboard will show {len(rows)} assistants")
444
+
445
+ # Create DataFrame
446
+ column_names = [col[0] for col in LEADERBOARD_COLUMNS]
447
+ df = pd.DataFrame(rows, columns=column_names)
448
+
449
+ # Ensure numeric types
450
+ numeric_cols = ["Total Wiki Edits", "Total Membership Events"]
451
+ for col in numeric_cols:
452
+ if col in df.columns:
453
+ df[col] = pd.to_numeric(df[col], errors='coerce').fillna(0)
454
+
455
+ # Sort by combined activity descending
456
+ if not df.empty:
457
+ df = df.sort_values(by=["Total Wiki Edits", "Total Membership Events"], ascending=False).reset_index(drop=True)
458
+
459
+ # Workaround for gradio_leaderboard bug: single-row tables don't render properly
460
+ if len(df) == 1:
461
+ placeholder_row = pd.DataFrame([[
462
+ "Submit yours to join!", "—", 0, 0
463
+ ]], columns=df.columns)
464
+ df = pd.concat([df, placeholder_row], ignore_index=True)
465
+ print("Added placeholder row for single-record workaround")
466
+
467
+ print(f"Final DataFrame shape: {df.shape}")
468
+ print("="*60 + "\n")
469
+
470
+ return df
471
+
472
+
473
+ def submit_agent(identifier, agent_name, organization, website):
474
+ """Submit a new assistant to the leaderboard."""
475
+ # Validate required fields
476
+ if not identifier or not identifier.strip():
477
+ return "ERROR: GitHub identifier is required", gr.update()
478
+ if not agent_name or not agent_name.strip():
479
+ return "ERROR: Assistant name is required", gr.update()
480
+ if not organization or not organization.strip():
481
+ return "ERROR: Organization name is required", gr.update()
482
+ if not website or not website.strip():
483
+ return "ERROR: Website URL is required", gr.update()
484
+
485
+ # Clean inputs
486
+ identifier = identifier.strip()
487
+ agent_name = agent_name.strip()
488
+ organization = organization.strip()
489
+ website = website.strip()
490
+
491
+ # Validate GitHub identifier
492
+ is_valid, message = validate_github_username(identifier)
493
+ if not is_valid:
494
+ return f"ERROR: {message}", gr.update()
495
+
496
+ # Check for duplicates by loading assistants from HuggingFace
497
+ assistants = load_agents_from_hf()
498
+ if assistants:
499
+ existing_names = {assistant['github_identifier'] for assistant in assistants}
500
+ if identifier in existing_names:
501
+ return f"WARNING: Assistant with identifier '{identifier}' already exists", gr.update()
502
+
503
+ # Create submission
504
+ submission = {
505
+ 'name': agent_name,
506
+ 'organization': organization,
507
+ 'github_identifier': identifier,
508
+ 'website': website,
509
+ 'status': 'active'
510
+ }
511
+
512
+ # Save to HuggingFace
513
+ if not save_agent_to_hf(submission):
514
+ return "ERROR: Failed to save submission", gr.update()
515
+
516
+ return f"SUCCESS: Successfully submitted {agent_name}! Community data will be automatically populated by the backend system via the maintainers.", gr.update()
517
+
518
+
519
+ # =============================================================================
520
+ # DATA RELOAD FUNCTION
521
+ # =============================================================================
522
+
523
+ def reload_leaderboard_data():
524
+ """Reload leaderboard data from HuggingFace. Called by scheduler daily."""
525
+ print(f"\n{'='*80}")
526
+ print(f"Reloading leaderboard data from HuggingFace...")
527
+ print(f"{'='*80}\n")
528
+
529
+ try:
530
+ data = load_leaderboard_data_from_hf()
531
+ if data:
532
+ print(f"Successfully reloaded leaderboard data")
533
+ print(f" Last updated: {data.get('metadata', {}).get('last_updated', 'Unknown')}")
534
+ print(f" Agents: {len(data.get('leaderboard', {}))}")
535
+ else:
536
+ print(f"No data available")
537
+ except Exception as e:
538
+ print(f"Error reloading leaderboard data: {str(e)}")
539
+
540
+ print(f"{'='*80}\n")
541
+
542
+
543
+ # =============================================================================
544
+ # GRADIO APPLICATION
545
+ # =============================================================================
546
+
547
+ print(f"\nStarting SWE Assistant Community Leaderboard")
548
+ print(f" Data source: {LEADERBOARD_REPO}")
549
+ print(f" Reload frequency: Daily at 12:00 AM UTC\n")
550
+
551
+ # Start APScheduler for daily data reload at 12:00 AM UTC
552
+ scheduler = BackgroundScheduler(timezone="UTC")
553
+ scheduler.add_job(
554
+ reload_leaderboard_data,
555
+ trigger=CronTrigger(hour=0, minute=0),
556
+ id='daily_data_reload',
557
+ name='Daily Data Reload',
558
+ replace_existing=True
559
+ )
560
+ scheduler.start()
561
+ print(f"\n{'='*80}")
562
+ print(f"Scheduler initialized successfully")
563
+ print(f"Reload schedule: Daily at 12:00 AM UTC")
564
+ print(f"On startup: Loads cached data from HuggingFace on demand")
565
+ print(f"{'='*80}\n")
566
+
567
+ # Create Gradio interface
568
+ with gr.Blocks(title="SWE Assistant Community Leaderboard", theme=gr.themes.Soft()) as app:
569
+ gr.Markdown("# SWE Assistant Community Leaderboard")
570
+ gr.Markdown(f"Track and compare community activity (wiki edits & membership events) by SWE assistants")
571
+
572
+ with gr.Tabs():
573
+
574
+ # Leaderboard Tab
575
+ with gr.Tab("Leaderboard"):
576
+ gr.Markdown("*Statistics are based on wiki edits and membership events by assistants*")
577
+ leaderboard_table = Leaderboard(
578
+ value=pd.DataFrame(columns=[col[0] for col in LEADERBOARD_COLUMNS]),
579
+ datatype=LEADERBOARD_COLUMNS,
580
+ search_columns=["Assistant", "Website"],
581
+ filter_columns=[]
582
+ )
583
+
584
+ # Load leaderboard data when app starts
585
+ app.load(
586
+ fn=get_leaderboard_dataframe,
587
+ inputs=[],
588
+ outputs=[leaderboard_table]
589
+ )
590
+
591
+ # Monthly Performance Metrics
592
+ gr.Markdown("---")
593
+ gr.Markdown("## Monthly Performance Metrics - Top 5 Assistants")
594
+
595
+ with gr.Row():
596
+ with gr.Column():
597
+ gr.Markdown("*Wiki edit volume over time*")
598
+ wiki_plot = gr.Plot()
599
+
600
+ with gr.Column():
601
+ gr.Markdown("*Membership event volume over time*")
602
+ members_plot = gr.Plot()
603
+
604
+ app.load(
605
+ fn=lambda: create_monthly_wiki_plot(),
606
+ inputs=[],
607
+ outputs=[wiki_plot]
608
+ )
609
+ app.load(
610
+ fn=lambda: create_monthly_members_plot(),
611
+ inputs=[],
612
+ outputs=[members_plot]
613
+ )
614
+
615
+
616
+ # Submit Assistant Tab
617
+ with gr.Tab("Submit Your Assistant"):
618
+
619
+ gr.Markdown("Fill in the details below to add your assistant to the leaderboard.")
620
+
621
+ with gr.Row():
622
+ with gr.Column():
623
+ github_input = gr.Textbox(
624
+ label="GitHub Identifier*",
625
+ placeholder="Your assistant username (e.g., my-assistant[bot])"
626
+ )
627
+ name_input = gr.Textbox(
628
+ label="Assistant Name*",
629
+ placeholder="Your assistant's display name"
630
+ )
631
+
632
+ with gr.Column():
633
+ organization_input = gr.Textbox(
634
+ label="Organization*",
635
+ placeholder="Your organization or team name"
636
+ )
637
+ website_input = gr.Textbox(
638
+ label="Website*",
639
+ placeholder="https://your-assistant-website.com"
640
+ )
641
+
642
+ submit_button = gr.Button(
643
+ "Submit Assistant",
644
+ variant="primary"
645
+ )
646
+ submission_status = gr.Textbox(
647
+ label="Submission Status",
648
+ interactive=False
649
+ )
650
+
651
+ # Event handler
652
+ submit_button.click(
653
+ fn=submit_agent,
654
+ inputs=[github_input, name_input, organization_input, website_input],
655
+ outputs=[submission_status, leaderboard_table]
656
+ )
657
+
658
+
659
+ # Launch application
660
+ if __name__ == "__main__":
661
+ app.launch()
msr.py ADDED
@@ -0,0 +1,715 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ from datetime import datetime, timezone, timedelta
4
+ from collections import defaultdict
5
+ from huggingface_hub import HfApi, hf_hub_download
6
+ from huggingface_hub.errors import HfHubHTTPError
7
+ from dotenv import load_dotenv
8
+ import duckdb
9
+ import backoff
10
+ import requests
11
+ import requests.exceptions
12
+ import traceback
13
+ import re
14
+
15
+ # Load environment variables
16
+ load_dotenv(override=True)
17
+
18
+ # =============================================================================
19
+ # CONFIGURATION
20
+ # =============================================================================
21
+
22
+ # Get script directory for relative paths
23
+ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
24
+ BASE_DIR = os.path.dirname(SCRIPT_DIR) # Parent directory
25
+
26
+ AGENTS_REPO = "SWE-Arena/bot_data"
27
+ AGENTS_REPO_LOCAL_PATH = os.path.join(BASE_DIR, "bot_data") # Local git clone path
28
+ DUCKDB_CACHE_FILE = os.path.join(SCRIPT_DIR, "cache.duckdb")
29
+ GHARCHIVE_DATA_LOCAL_PATH = os.path.join(BASE_DIR, "gharchive/data")
30
+ LEADERBOARD_FILENAME = f"{os.getenv('COMPOSE_PROJECT_NAME')}.json"
31
+ LEADERBOARD_REPO = "SWE-Arena/leaderboard_data"
32
+ LEADERBOARD_TIME_FRAME_DAYS = 180
33
+
34
+ # Git sync configuration (mandatory to get latest bot data)
35
+ GIT_SYNC_TIMEOUT = 300 # 5 minutes timeout for git pull
36
+
37
+ # Streaming batch configuration
38
+ BATCH_SIZE_DAYS = 1 # Process 1 day at a time (~24 hourly files)
39
+
40
+ # Retry configuration
41
+ MAX_RETRIES = 5
42
+
43
+ # =============================================================================
44
+ # UTILITY FUNCTIONS
45
+ # =============================================================================
46
+
47
+ def load_jsonl(filename):
48
+ """Load JSONL file and return list of dictionaries."""
49
+ if not os.path.exists(filename):
50
+ return []
51
+
52
+ data = []
53
+ with open(filename, 'r', encoding='utf-8') as f:
54
+ for line in f:
55
+ line = line.strip()
56
+ if line:
57
+ try:
58
+ data.append(json.loads(line))
59
+ except json.JSONDecodeError as e:
60
+ print(f"Warning: Skipping invalid JSON line: {e}")
61
+ return data
62
+
63
+
64
+ def save_jsonl(filename, data):
65
+ """Save list of dictionaries to JSONL file."""
66
+ with open(filename, 'w', encoding='utf-8') as f:
67
+ for item in data:
68
+ f.write(json.dumps(item) + '\n')
69
+
70
+
71
+ def normalize_date_format(date_string):
72
+ """Convert date strings or datetime objects to standardized ISO 8601 format with Z suffix."""
73
+ if not date_string or date_string == 'N/A':
74
+ return 'N/A'
75
+
76
+ try:
77
+ if isinstance(date_string, datetime):
78
+ return date_string.strftime('%Y-%m-%dT%H:%M:%SZ')
79
+
80
+ date_string = re.sub(r'\s+', ' ', date_string.strip())
81
+ date_string = date_string.replace(' ', 'T')
82
+
83
+ if len(date_string) >= 3:
84
+ if date_string[-3:-2] in ('+', '-') and ':' not in date_string[-3:]:
85
+ date_string = date_string + ':00'
86
+
87
+ dt = datetime.fromisoformat(date_string.replace('Z', '+00:00'))
88
+ return dt.strftime('%Y-%m-%dT%H:%M:%SZ')
89
+ except Exception as e:
90
+ print(f"Warning: Could not parse date '{date_string}': {e}")
91
+ return date_string
92
+
93
+
94
+ def get_hf_token():
95
+ """Get HuggingFace token from environment variables."""
96
+ token = os.getenv('HF_TOKEN')
97
+ if not token:
98
+ print("Warning: HF_TOKEN not found in environment variables")
99
+ return token
100
+
101
+
102
+ # =============================================================================
103
+ # GHARCHIVE DOWNLOAD FUNCTIONS
104
+ # =============================================================================
105
+
106
+ def download_file(url):
107
+ """Download a GHArchive file with retry logic."""
108
+ filename = url.split("/")[-1]
109
+ filepath = os.path.join(GHARCHIVE_DATA_LOCAL_PATH, filename)
110
+
111
+ if os.path.exists(filepath):
112
+ return True
113
+
114
+ try:
115
+ response = requests.get(url, timeout=30)
116
+ response.raise_for_status()
117
+ with open(filepath, "wb") as f:
118
+ f.write(response.content)
119
+ return True
120
+ except Exception as e:
121
+ print(f" ⚠ {filename}: {e}")
122
+ return False
123
+
124
+
125
+ def download_all_gharchive_data():
126
+ """Download all GHArchive data files for the last LEADERBOARD_TIME_FRAME_DAYS."""
127
+ os.makedirs(GHARCHIVE_DATA_LOCAL_PATH, exist_ok=True)
128
+
129
+ end_date = datetime.now(timezone.utc)
130
+ start_date = end_date - timedelta(days=LEADERBOARD_TIME_FRAME_DAYS)
131
+
132
+ urls = []
133
+ current_date = start_date
134
+ while current_date <= end_date:
135
+ date_str = current_date.strftime("%Y-%m-%d")
136
+ for hour in range(24):
137
+ url = f"https://data.gharchive.org/{date_str}-{hour}.json.gz"
138
+ urls.append(url)
139
+ current_date += timedelta(days=1)
140
+
141
+ success = True
142
+ for url in urls:
143
+ if not download_file(url):
144
+ success = False
145
+
146
+ return success
147
+
148
+
149
+ # =============================================================================
150
+ # HUGGINGFACE API WRAPPERS
151
+ # =============================================================================
152
+
153
+ def is_retryable_error(e):
154
+ """Check if exception is retryable (rate limit or timeout error)."""
155
+ if isinstance(e, HfHubHTTPError):
156
+ if e.response.status_code == 429:
157
+ return True
158
+
159
+ if isinstance(e, (requests.exceptions.Timeout,
160
+ requests.exceptions.ReadTimeout,
161
+ requests.exceptions.ConnectTimeout)):
162
+ return True
163
+
164
+ if isinstance(e, Exception):
165
+ error_str = str(e).lower()
166
+ if 'timeout' in error_str or 'timed out' in error_str:
167
+ return True
168
+
169
+ return False
170
+
171
+
172
+ @backoff.on_exception(
173
+ backoff.expo,
174
+ (HfHubHTTPError, requests.exceptions.Timeout, requests.exceptions.RequestException, Exception),
175
+ max_tries=MAX_RETRIES,
176
+ base=300,
177
+ max_value=3600,
178
+ giveup=lambda e: not is_retryable_error(e),
179
+ on_backoff=lambda details: print(
180
+ f" {details['exception']} error. Retrying in {details['wait']/60:.1f} minutes ({details['wait']:.0f}s) - attempt {details['tries']}/5..."
181
+ )
182
+ )
183
+ def list_repo_files_with_backoff(api, **kwargs):
184
+ """Wrapper for api.list_repo_files() with exponential backoff."""
185
+ return api.list_repo_files(**kwargs)
186
+
187
+
188
+ @backoff.on_exception(
189
+ backoff.expo,
190
+ (HfHubHTTPError, requests.exceptions.Timeout, requests.exceptions.RequestException, Exception),
191
+ max_tries=MAX_RETRIES,
192
+ base=300,
193
+ max_value=3600,
194
+ giveup=lambda e: not is_retryable_error(e),
195
+ on_backoff=lambda details: print(
196
+ f" {details['exception']} error. Retrying in {details['wait']/60:.1f} minutes ({details['wait']:.0f}s) - attempt {details['tries']}/5..."
197
+ )
198
+ )
199
+ def hf_hub_download_with_backoff(**kwargs):
200
+ """Wrapper for hf_hub_download() with exponential backoff."""
201
+ return hf_hub_download(**kwargs)
202
+
203
+
204
+ @backoff.on_exception(
205
+ backoff.expo,
206
+ (HfHubHTTPError, requests.exceptions.Timeout, requests.exceptions.RequestException, Exception),
207
+ max_tries=MAX_RETRIES,
208
+ base=300,
209
+ max_value=3600,
210
+ giveup=lambda e: not is_retryable_error(e),
211
+ on_backoff=lambda details: print(
212
+ f" {details['exception']} error. Retrying in {details['wait']/60:.1f} minutes ({details['wait']:.0f}s) - attempt {details['tries']}/5..."
213
+ )
214
+ )
215
+ def upload_file_with_backoff(api, **kwargs):
216
+ """Wrapper for api.upload_file() with exponential backoff."""
217
+ return api.upload_file(**kwargs)
218
+
219
+
220
+ @backoff.on_exception(
221
+ backoff.expo,
222
+ (HfHubHTTPError, requests.exceptions.Timeout, requests.exceptions.RequestException, Exception),
223
+ max_tries=MAX_RETRIES,
224
+ base=300,
225
+ max_value=3600,
226
+ giveup=lambda e: not is_retryable_error(e),
227
+ on_backoff=lambda details: print(
228
+ f" {details['exception']} error. Retrying in {details['wait']/60:.1f} minutes ({details['wait']:.0f}s) - attempt {details['tries']}/5..."
229
+ )
230
+ )
231
+ def upload_folder_with_backoff(api, **kwargs):
232
+ """Wrapper for api.upload_folder() with exponential backoff."""
233
+ return api.upload_folder(**kwargs)
234
+
235
+
236
+ def get_duckdb_connection():
237
+ """
238
+ Initialize DuckDB connection with OPTIMIZED memory settings.
239
+ Uses persistent database and reduced memory footprint.
240
+ Automatically removes cache file if lock conflict is detected.
241
+ """
242
+ try:
243
+ conn = duckdb.connect(DUCKDB_CACHE_FILE)
244
+ except Exception as e:
245
+ # Check if it's a locking error
246
+ error_msg = str(e)
247
+ if "lock" in error_msg.lower() or "conflicting" in error_msg.lower():
248
+ print(f" ⚠ Lock conflict detected, removing {DUCKDB_CACHE_FILE}...")
249
+ if os.path.exists(DUCKDB_CACHE_FILE):
250
+ os.remove(DUCKDB_CACHE_FILE)
251
+ print(f" ✓ Cache file removed, retrying connection...")
252
+ # Retry connection after removing cache
253
+ conn = duckdb.connect(DUCKDB_CACHE_FILE)
254
+ else:
255
+ # Re-raise if it's not a locking error
256
+ raise
257
+
258
+ # CORE MEMORY & THREADING SETTINGS
259
+ conn.execute(f"SET threads TO 4;")
260
+ conn.execute(f"SET max_memory = '50GB';")
261
+ conn.execute("SET temp_directory = '/tmp/duckdb_temp';")
262
+
263
+ # PERFORMANCE OPTIMIZATIONS
264
+ conn.execute("SET preserve_insertion_order = false;") # Disable expensive ordering
265
+ conn.execute("SET enable_object_cache = true;") # Cache repeatedly read files
266
+
267
+ return conn
268
+
269
+
270
+ def generate_file_path_patterns(start_date, end_date, data_dir=GHARCHIVE_DATA_LOCAL_PATH):
271
+ """Generate file path patterns for GHArchive data in date range (only existing files)."""
272
+ file_patterns = []
273
+ missing_dates = set()
274
+
275
+ current_date = start_date.replace(hour=0, minute=0, second=0, microsecond=0)
276
+ end_day = end_date.replace(hour=0, minute=0, second=0, microsecond=0)
277
+
278
+ while current_date <= end_day:
279
+ date_has_files = False
280
+ for hour in range(24):
281
+ pattern = os.path.join(data_dir, f"{current_date.strftime('%Y-%m-%d')}-{hour}.json.gz")
282
+ if os.path.exists(pattern):
283
+ file_patterns.append(pattern)
284
+ date_has_files = True
285
+
286
+ if not date_has_files:
287
+ missing_dates.add(current_date.strftime('%Y-%m-%d'))
288
+
289
+ current_date += timedelta(days=1)
290
+
291
+ if missing_dates:
292
+ print(f" ○ Skipping {len(missing_dates)} date(s) with no data")
293
+
294
+ return file_patterns
295
+
296
+
297
+ # =============================================================================
298
+ # STREAMING BATCH PROCESSING
299
+ # =============================================================================
300
+
301
+ def fetch_all_community_metadata_streaming(conn, identifiers, start_date, end_date):
302
+ """
303
+ QUERY: Fetch community metadata (wiki edits + member events) using streaming batch processing.
304
+
305
+ Args:
306
+ conn: DuckDB connection instance
307
+ identifiers: List of GitHub usernames/bot identifiers
308
+ start_date: Start datetime (timezone-aware)
309
+ end_date: End datetime (timezone-aware)
310
+
311
+ Returns:
312
+ Tuple of (wiki_metadata_by_agent, member_metadata_by_agent)
313
+ """
314
+ identifier_list = ', '.join([f"'{id}'" for id in identifiers])
315
+ wiki_metadata_by_agent = defaultdict(list)
316
+ member_metadata_by_agent = defaultdict(list)
317
+
318
+ # Calculate total batches
319
+ total_days = (end_date - start_date).days
320
+ total_batches = (total_days // BATCH_SIZE_DAYS) + 1
321
+
322
+ # Process in configurable batches
323
+ current_date = start_date
324
+ batch_num = 0
325
+ total_wiki_edits = 0
326
+ total_members = 0
327
+
328
+ print(f" Streaming {total_batches} batches of {BATCH_SIZE_DAYS}-day intervals...")
329
+
330
+ while current_date <= end_date:
331
+ batch_num += 1
332
+ batch_end = min(current_date + timedelta(days=BATCH_SIZE_DAYS - 1), end_date)
333
+
334
+ # Get file patterns for THIS BATCH ONLY
335
+ file_patterns = generate_file_path_patterns(current_date, batch_end)
336
+
337
+ if not file_patterns:
338
+ print(f" Batch {batch_num}/{total_batches}: {current_date.date()} to {batch_end.date()} - NO DATA")
339
+ current_date = batch_end + timedelta(days=1)
340
+ continue
341
+
342
+ print(f" Batch {batch_num}/{total_batches}: {current_date.date()} to {batch_end.date()} ({len(file_patterns)} files)... ", end="", flush=True)
343
+
344
+ file_patterns_sql = '[' + ', '.join([f"'{fp}'" for fp in file_patterns]) + ']'
345
+
346
+ # --- Wiki query (GollumEvent) ---
347
+ wiki_query = f"""
348
+ SELECT
349
+ TRY_CAST(json_extract_string(to_json(actor), '$.login') AS VARCHAR) as assistant,
350
+ TRY_CAST(json_array_length(json_extract(to_json(payload), '$.pages')) AS INTEGER) as page_count,
351
+ created_at
352
+ FROM read_json(
353
+ {file_patterns_sql},
354
+ union_by_name=true,
355
+ filename=true,
356
+ compression='gzip',
357
+ format='newline_delimited',
358
+ ignore_errors=true
359
+ )
360
+ WHERE type = 'GollumEvent'
361
+ AND json_extract(to_json(payload), '$.pages') IS NOT NULL
362
+ AND TRY_CAST(json_extract_string(to_json(actor), '$.login') AS VARCHAR) IN ({identifier_list})
363
+ """
364
+
365
+ # --- Member query (MemberEvent) ---
366
+ member_query = f"""
367
+ SELECT DISTINCT
368
+ actor.login as assistant,
369
+ TRY_CAST(json_extract_string(to_json(payload), '$.member.login') AS VARCHAR) as member_login,
370
+ TRY_CAST(json_extract_string(to_json(payload), '$.action') AS VARCHAR) as action,
371
+ created_at
372
+ FROM read_json(
373
+ {file_patterns_sql},
374
+ union_by_name=true,
375
+ filename=true,
376
+ compression='gzip',
377
+ format='newline_delimited',
378
+ ignore_errors=true
379
+ )
380
+ WHERE type = 'MemberEvent'
381
+ AND TRY_CAST(json_extract_string(to_json(payload), '$.member.login') AS VARCHAR) IS NOT NULL
382
+ AND TRY_CAST(json_extract_string(to_json(actor), '$.login') AS VARCHAR) IN ({identifier_list})
383
+ """
384
+
385
+ try:
386
+ # Wiki results
387
+ batch_wiki_edits = 0
388
+ results = conn.execute(wiki_query).fetchall()
389
+ for row in results:
390
+ assistant = row[0]
391
+ page_count = row[1] if row[1] is not None else 0
392
+ created_at = normalize_date_format(row[2]) if row[2] else None
393
+
394
+ if not assistant or page_count == 0:
395
+ continue
396
+
397
+ wiki_metadata_by_agent[assistant].append({
398
+ 'page_count': page_count,
399
+ 'created_at': created_at,
400
+ })
401
+ batch_wiki_edits += page_count
402
+ total_wiki_edits += page_count
403
+
404
+ # Member results
405
+ batch_members = 0
406
+ results = conn.execute(member_query).fetchall()
407
+ for row in results:
408
+ assistant = row[0]
409
+ member_login = row[1]
410
+ action = row[2]
411
+ created_at = normalize_date_format(row[3]) if row[3] else None
412
+
413
+ if not assistant or not member_login:
414
+ continue
415
+
416
+ member_metadata_by_agent[assistant].append({
417
+ 'member_login': member_login,
418
+ 'action': action,
419
+ 'created_at': created_at,
420
+ })
421
+ batch_members += 1
422
+ total_members += 1
423
+
424
+ print(f"✓ {batch_wiki_edits} wiki edits, {batch_members} members")
425
+
426
+ except Exception as e:
427
+ print(f"\n ✗ Batch {batch_num} error: {str(e)}")
428
+ traceback.print_exc()
429
+
430
+ current_date = batch_end + timedelta(days=1)
431
+
432
+ # Final summary
433
+ wiki_agents = sum(1 for v in wiki_metadata_by_agent.values() if v)
434
+ member_agents = sum(1 for v in member_metadata_by_agent.values() if v)
435
+ print(f"\n ✓ Complete: {total_wiki_edits} wiki edits ({wiki_agents} assistants), {total_members} members ({member_agents} assistants)")
436
+
437
+ return dict(wiki_metadata_by_agent), dict(member_metadata_by_agent)
438
+
439
+
440
+ def load_agents_from_hf():
441
+ """
442
+ Load all assistant metadata JSON files from local git repository.
443
+ """
444
+ assistants = []
445
+
446
+ # Scan local directory for JSON files
447
+ if not os.path.exists(AGENTS_REPO_LOCAL_PATH):
448
+ raise FileNotFoundError(f"Local repository not found at {AGENTS_REPO_LOCAL_PATH}")
449
+
450
+ # Walk through the directory to find all JSON files
451
+ files_processed = 0
452
+ print(f" Loading assistant metadata from {AGENTS_REPO_LOCAL_PATH}...")
453
+
454
+ for root, dirs, files in os.walk(AGENTS_REPO_LOCAL_PATH):
455
+ # Skip .git directory
456
+ if '.git' in root:
457
+ continue
458
+
459
+ for filename in files:
460
+ if not filename.endswith('.json'):
461
+ continue
462
+
463
+ files_processed += 1
464
+ file_path = os.path.join(root, filename)
465
+
466
+ try:
467
+ with open(file_path, 'r', encoding='utf-8') as f:
468
+ agent_data = json.load(f)
469
+
470
+ # Only include active assistants
471
+ if agent_data.get('status') != 'active':
472
+ continue
473
+
474
+ # Extract github_identifier from filename
475
+ github_identifier = filename.replace('.json', '')
476
+ agent_data['github_identifier'] = github_identifier
477
+
478
+ assistants.append(agent_data)
479
+
480
+ except Exception as e:
481
+ print(f" ○ Error loading {filename}: {str(e)}")
482
+ continue
483
+
484
+ print(f" ✓ Loaded {len(assistants)} active assistants (from {files_processed} total files)")
485
+ return assistants
486
+
487
+
488
+ def calculate_community_stats(wiki_metadata, member_metadata):
489
+ """Calculate combined community statistics."""
490
+ total_wiki_edits = sum(item.get('page_count', 0) for item in wiki_metadata)
491
+ total_members = len(member_metadata)
492
+
493
+ return {
494
+ 'total_wiki_edits': total_wiki_edits,
495
+ 'total_members': total_members,
496
+ }
497
+
498
+
499
+ def calculate_monthly_metrics_by_agent(wiki_metadata_dict, member_metadata_dict, assistants):
500
+ """Calculate monthly metrics for all assistants for visualization."""
501
+ identifier_to_name = {assistant.get('github_identifier'): assistant.get('name') for assistant in assistants if assistant.get('github_identifier')}
502
+
503
+ if not wiki_metadata_dict and not member_metadata_dict:
504
+ return {'assistants': [], 'months': [], 'data': {}}
505
+
506
+ # Collect all agent identifiers that have any data
507
+ all_agent_ids = set(wiki_metadata_dict.keys()) | set(member_metadata_dict.keys())
508
+
509
+ agent_month_wiki = defaultdict(lambda: defaultdict(list))
510
+ agent_month_member = defaultdict(lambda: defaultdict(list))
511
+
512
+ # Process wiki metadata
513
+ for agent_identifier, metadata_list in wiki_metadata_dict.items():
514
+ agent_name = identifier_to_name.get(agent_identifier, agent_identifier)
515
+ for wiki_meta in metadata_list:
516
+ created_at = wiki_meta.get('created_at')
517
+ if not created_at:
518
+ continue
519
+ try:
520
+ dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))
521
+ month_key = f"{dt.year}-{dt.month:02d}"
522
+ agent_month_wiki[agent_name][month_key].append(wiki_meta)
523
+ except Exception as e:
524
+ print(f"Warning: Could not parse date '{created_at}': {e}")
525
+
526
+ # Process member metadata
527
+ for agent_identifier, metadata_list in member_metadata_dict.items():
528
+ agent_name = identifier_to_name.get(agent_identifier, agent_identifier)
529
+ for member_meta in metadata_list:
530
+ created_at = member_meta.get('created_at')
531
+ if not created_at:
532
+ continue
533
+ try:
534
+ dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))
535
+ month_key = f"{dt.year}-{dt.month:02d}"
536
+ agent_month_member[agent_name][month_key].append(member_meta)
537
+ except Exception as e:
538
+ print(f"Warning: Could not parse date '{created_at}': {e}")
539
+
540
+ # Collect all months and agent names
541
+ all_months = set()
542
+ all_agent_names = set()
543
+ for agent_data in agent_month_wiki.values():
544
+ all_months.update(agent_data.keys())
545
+ for agent_data in agent_month_member.values():
546
+ all_months.update(agent_data.keys())
547
+ all_agent_names.update(agent_month_wiki.keys())
548
+ all_agent_names.update(agent_month_member.keys())
549
+ months = sorted(list(all_months))
550
+
551
+ result_data = {}
552
+ for agent_name in all_agent_names:
553
+ total_wiki_edits_list = []
554
+ total_members_list = []
555
+
556
+ for month in months:
557
+ wiki_events = agent_month_wiki.get(agent_name, {}).get(month, [])
558
+ member_events = agent_month_member.get(agent_name, {}).get(month, [])
559
+
560
+ total_wiki_edits_list.append(sum(item.get('page_count', 0) for item in wiki_events))
561
+ total_members_list.append(len(member_events))
562
+
563
+ result_data[agent_name] = {
564
+ 'total_wiki_edits': total_wiki_edits_list,
565
+ 'total_members': total_members_list,
566
+ }
567
+
568
+ agents_list = sorted(list(all_agent_names))
569
+
570
+ return {
571
+ 'assistants': agents_list,
572
+ 'months': months,
573
+ 'data': result_data
574
+ }
575
+
576
+
577
+ def construct_leaderboard_from_metadata(wiki_metadata_dict, member_metadata_dict, assistants):
578
+ """Construct leaderboard from in-memory community metadata."""
579
+ if not assistants:
580
+ print("Error: No assistants found")
581
+ return {}
582
+
583
+ cache_dict = {}
584
+
585
+ for assistant in assistants:
586
+ identifier = assistant.get('github_identifier')
587
+ agent_name = assistant.get('name', 'Unknown')
588
+
589
+ wiki_data = wiki_metadata_dict.get(identifier, [])
590
+ member_data = member_metadata_dict.get(identifier, [])
591
+ stats = calculate_community_stats(wiki_data, member_data)
592
+
593
+ cache_dict[identifier] = {
594
+ 'name': agent_name,
595
+ 'website': assistant.get('website', 'N/A'),
596
+ 'github_identifier': identifier,
597
+ **stats
598
+ }
599
+
600
+ return cache_dict
601
+
602
+
603
+ def save_leaderboard_data_to_hf(leaderboard_dict, monthly_metrics):
604
+ """Save leaderboard data and monthly metrics to HuggingFace dataset."""
605
+ try:
606
+ token = get_hf_token()
607
+ if not token:
608
+ raise Exception("No HuggingFace token found")
609
+
610
+ api = HfApi(token=token)
611
+
612
+ combined_data = {
613
+ 'last_updated': datetime.now(timezone.utc).isoformat(),
614
+ 'leaderboard': leaderboard_dict,
615
+ 'monthly_metrics': monthly_metrics,
616
+ 'metadata': {
617
+ 'leaderboard_time_frame_days': LEADERBOARD_TIME_FRAME_DAYS
618
+ }
619
+ }
620
+
621
+ with open(LEADERBOARD_FILENAME, 'w') as f:
622
+ json.dump(combined_data, f, indent=2)
623
+
624
+ try:
625
+ upload_file_with_backoff(
626
+ api=api,
627
+ path_or_fileobj=LEADERBOARD_FILENAME,
628
+ path_in_repo=LEADERBOARD_FILENAME,
629
+ repo_id=LEADERBOARD_REPO,
630
+ repo_type="dataset"
631
+ )
632
+ return True
633
+ finally:
634
+ if os.path.exists(LEADERBOARD_FILENAME):
635
+ os.remove(LEADERBOARD_FILENAME)
636
+
637
+ except Exception as e:
638
+ print(f"Error saving leaderboard data: {str(e)}")
639
+ traceback.print_exc()
640
+ return False
641
+
642
+
643
+ # =============================================================================
644
+ # MINING FUNCTION
645
+ # =============================================================================
646
+
647
+ def mine_all_agents():
648
+ """
649
+ Mine community metadata (wiki + members) for all assistants using STREAMING batch processing.
650
+ Downloads GHArchive data, then uses BATCH-based DuckDB queries.
651
+ """
652
+ print(f"\n[1/4] Downloading GHArchive data...")
653
+
654
+ if not download_all_gharchive_data():
655
+ print("Warning: Download had errors, continuing with available data...")
656
+
657
+ print(f"\n[2/4] Loading assistant metadata...")
658
+
659
+ assistants = load_agents_from_hf()
660
+ if not assistants:
661
+ print("Error: No assistants found")
662
+ return
663
+
664
+ identifiers = [assistant['github_identifier'] for assistant in assistants if assistant.get('github_identifier')]
665
+ if not identifiers:
666
+ print("Error: No valid assistant identifiers found")
667
+ return
668
+
669
+ print(f"\n[3/4] Mining community metadata ({len(identifiers)} assistants, {LEADERBOARD_TIME_FRAME_DAYS} days)...")
670
+
671
+ try:
672
+ conn = get_duckdb_connection()
673
+ except Exception as e:
674
+ print(f"Failed to initialize DuckDB connection: {str(e)}")
675
+ return
676
+
677
+ current_time = datetime.now(timezone.utc)
678
+ end_date = current_time.replace(hour=0, minute=0, second=0, microsecond=0)
679
+ start_date = end_date - timedelta(days=LEADERBOARD_TIME_FRAME_DAYS)
680
+
681
+ try:
682
+ wiki_metadata, member_metadata = fetch_all_community_metadata_streaming(
683
+ conn, identifiers, start_date, end_date
684
+ )
685
+ except Exception as e:
686
+ print(f"Error during DuckDB fetch: {str(e)}")
687
+ traceback.print_exc()
688
+ return
689
+ finally:
690
+ conn.close()
691
+
692
+ print(f"\n[4/4] Saving leaderboard...")
693
+
694
+ try:
695
+ leaderboard_dict = construct_leaderboard_from_metadata(wiki_metadata, member_metadata, assistants)
696
+ monthly_metrics = calculate_monthly_metrics_by_agent(wiki_metadata, member_metadata, assistants)
697
+ save_leaderboard_data_to_hf(leaderboard_dict, monthly_metrics)
698
+ except Exception as e:
699
+ print(f"Error saving leaderboard: {str(e)}")
700
+ traceback.print_exc()
701
+ finally:
702
+ # Clean up DuckDB cache file to save storage
703
+ if os.path.exists(DUCKDB_CACHE_FILE):
704
+ try:
705
+ os.remove(DUCKDB_CACHE_FILE)
706
+ print(f" ✓ Cache file removed: {DUCKDB_CACHE_FILE}")
707
+ except Exception as e:
708
+ print(f" ⚠ Failed to remove cache file: {str(e)}")
709
+
710
+ # =============================================================================
711
+ # ENTRY POINT
712
+ # =============================================================================
713
+
714
+ if __name__ == "__main__":
715
+ mine_all_agents()
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ APScheduler
2
+ backoff
3
+ duckdb[all]
4
+ gradio
5
+ gradio_leaderboard
6
+ huggingface_hub
7
+ pandas
8
+ plotly
9
+ python-dotenv
10
+ requests