File size: 12,483 Bytes
4f4b027
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
# /// script
# requires-python = ">=3.11"
# dependencies = [
#     "huggingface-hub>=1.1.4",
#     "python-dotenv>=1.0.0",
#     "pyyaml>=6.0.0",
#     "requests>=2.31.0",
# ]
# ///
"""
Import evaluation results from Artificial Analysis API.

Usage:
  # Look up a specific benchmark (dry run - prints YAML)
  AA_API_KEY=... uv run scripts/import_aa.py --repo-id "org/model" --benchmark HLE

  # Look up a benchmark and create PR
  AA_API_KEY=... uv run scripts/import_aa.py --repo-id "org/model" --benchmark GPQA --create-pr

  # Import all available benchmarks
  AA_API_KEY=... uv run scripts/import_aa.py --repo-id "org/model" --all

  # Provide value manually (skip lookup)
  uv run scripts/import_aa.py --repo-id "org/model" --benchmark HLE --value 22.5 --create-pr
"""

from __future__ import annotations

import argparse
import json
import os
import re
import sys
from datetime import date
from pathlib import Path
from typing import Any

import requests


AA_INDEX_URL = "https://artificialanalysis.ai/api/v2/data/llms/models"


def load_env() -> None:
    try:
        import dotenv
        dotenv.load_dotenv()
    except ModuleNotFoundError:
        pass


def load_benchmark_mapping() -> dict[str, Any]:
    script_dir = Path(__file__).parent
    mapping_file = script_dir.parent / "examples" / "metric_mapping.json"

    if not mapping_file.exists():
        return {
            "GPQA": {"dataset_id": "Idavidrein/gpqa", "task_id": "gpqa_diamond", "aliases": ["gpqa"]},
            "HLE": {"dataset_id": "cais/hle", "task_id": "default", "aliases": ["hle"]},
            "SimpleQA": {"dataset_id": "OpenEvals/SimpleQA", "task_id": "default", "aliases": ["simpleqa"]},
            "MMLU": {"dataset_id": "cais/mmlu", "task_id": "default", "aliases": ["mmlu"]},
            "GSM8K": {"dataset_id": "openai/gsm8k", "task_id": "default", "aliases": ["gsm8k"]},
        }

    with open(mapping_file) as f:
        mapping = json.load(f)
    mapping.pop("_comment", None)
    return mapping


def find_benchmark_dataset(benchmark_name: str, mapping: dict[str, Any]) -> dict[str, str] | None:
    cleaned = re.sub(r'\[([^\]]+)\]\([^\)]+\)', r'\1', benchmark_name)
    cleaned = re.sub(r'\*\*([^\*]+)\*\*', r'\1', cleaned)
    cleaned = re.sub(r'\*([^\*]+)\*', r'\1', cleaned)
    cleaned = cleaned.strip()

    normalized = cleaned.lower().replace(" ", "_").replace("-", "_")
    base_name = re.sub(r'\s*\([^)]*\)\s*$', '', cleaned).strip()
    base_normalized = base_name.lower().replace(" ", "_").replace("-", "_")

    if cleaned in mapping:
        entry = mapping[cleaned]
        return {"dataset_id": entry["dataset_id"], "task_id": entry.get("task_id", "default")}

    for key, entry in mapping.items():
        if key.lower() == cleaned.lower():
            return {"dataset_id": entry["dataset_id"], "task_id": entry.get("task_id", "default")}

    for key, entry in mapping.items():
        aliases = entry.get("aliases", [])
        normalized_aliases = [a.lower().replace(" ", "_").replace("-", "_") for a in aliases]
        if normalized in normalized_aliases:
            return {"dataset_id": entry["dataset_id"], "task_id": entry.get("task_id", "default")}

    for key, entry in mapping.items():
        key_normalized = key.lower().replace(" ", "_").replace("-", "_")
        if normalized == key_normalized:
            return {"dataset_id": entry["dataset_id"], "task_id": entry.get("task_id", "default")}

    if base_normalized != normalized:
        for key, entry in mapping.items():
            if key.lower() == base_name.lower():
                return {"dataset_id": entry["dataset_id"], "task_id": entry.get("task_id", "default")}
            key_normalized = key.lower().replace(" ", "_").replace("-", "_")
            if base_normalized == key_normalized:
                return {"dataset_id": entry["dataset_id"], "task_id": entry.get("task_id", "default")}

    return None


def fetch_aa_models(api_key: str) -> list[dict[str, Any]]:
    response = requests.get(
        AA_INDEX_URL,
        headers={"x-api-key": api_key},
        timeout=30,
    )
    response.raise_for_status()
    data = response.json()
    return list(data.get("data", []))


def find_model_in_aa(models: list[dict[str, Any]], repo_id: str) -> dict[str, Any] | None:
    model_name = repo_id.split("/")[-1] if "/" in repo_id else repo_id
    model_name_normalized = model_name.lower().replace("-", " ").replace("_", " ")

    for model in models:
        aa_name = model.get("name", "").lower().replace("-", " ").replace("_", " ")
        aa_slug = model.get("slug", "").lower().replace("-", " ").replace("_", " ")
        if model_name_normalized in aa_name or model_name_normalized in aa_slug:
            return model

    return None


def lookup_benchmark_from_aa(
    models: list[dict[str, Any]],
    repo_id: str,
    benchmark_name: str,
) -> float | None:
    model = find_model_in_aa(models, repo_id)
    if not model:
        return None

    evaluations = model.get("evaluations", {})
    benchmark_normalized = benchmark_name.lower().replace(" ", "_").replace("-", "_")

    for key, value in evaluations.items():
        key_normalized = key.lower().replace(" ", "_").replace("-", "_")
        if benchmark_normalized == key_normalized or benchmark_normalized in key_normalized:
            if value is not None:
                return float(value)

    return None


def get_all_benchmarks_from_aa(
    models: list[dict[str, Any]],
    repo_id: str,
) -> list[dict[str, Any]]:
    model = find_model_in_aa(models, repo_id)
    if not model:
        return []

    evaluations = model.get("evaluations", {})
    metrics = []

    for key, value in evaluations.items():
        if value is not None:
            metrics.append({
                "name": key.replace("_", " ").title(),
                "type": key,
                "value": float(value),
            })

    return metrics


def convert_to_eval_results_format(
    metrics: list[dict[str, Any]],
    source_url: str | None = None,
    source_name: str | None = None,
    source_user: str | None = None,
) -> list[dict[str, Any]]:
    mapping = load_benchmark_mapping()
    results = []
    today = date.today().isoformat()

    for metric in metrics:
        benchmark_name = metric.get("name", "")
        value = metric.get("value")

        if value is None:
            continue

        dataset_info = find_benchmark_dataset(benchmark_name, mapping)
        if not dataset_info:
            print(f"Warning: Could not find Hub dataset ID for benchmark '{benchmark_name}'. Skipping.", file=sys.stderr)
            continue

        entry: dict[str, Any] = {
            "dataset": {"id": dataset_info["dataset_id"]},
            "value": value,
            "date": today,
        }

        if dataset_info.get("task_id") and dataset_info["task_id"] != "default":
            entry["dataset"]["task_id"] = dataset_info["task_id"]

        if source_url:
            entry["source"] = {"url": source_url}
            if source_name:
                entry["source"]["name"] = source_name
            if source_user:
                entry["source"]["user"] = source_user

        results.append(entry)

    return results


def upload_eval_results(
    repo_id: str,
    results: list[dict[str, Any]],
    filename: str = "evaluations.yaml",
    create_pr: bool = False,
    commit_message: str | None = None,
) -> bool:
    import yaml
    from huggingface_hub import HfApi

    load_env()
    hf_token = os.getenv("HF_TOKEN")
    if not hf_token:
        print("Error: HF_TOKEN environment variable is not set", file=sys.stderr)
        return False

    api = HfApi(token=hf_token)
    yaml_content = yaml.dump(results, sort_keys=False, allow_unicode=True, default_flow_style=False)
    file_path = f".eval_results/{filename}"

    if not commit_message:
        model_name = repo_id.split("/")[-1] if "/" in repo_id else repo_id
        commit_message = f"Add Artificial Analysis evaluation results for {model_name}"

    pr_description = """## Evaluation Results

This PR adds structured evaluation results using the new [`.eval_results/` format](https://huggingface.co/docs/hub/eval-results).

**Source:** [Artificial Analysis](https://artificialanalysis.ai)

### What This Enables

- **Model Page**: Results appear on the model page with benchmark links
- **Leaderboards**: Scores are aggregated into benchmark dataset leaderboards
- **Verification**: Support for cryptographic verification of evaluation runs

---
*Generated by [community-evals](https://github.com/huggingface/community-evals)*"""

    try:
        api.upload_file(
            path_or_fileobj=yaml_content.encode("utf-8"),
            path_in_repo=file_path,
            repo_id=repo_id,
            repo_type="model",
            commit_message=commit_message,
            commit_description=pr_description,
            create_pr=create_pr,
        )

        action = "Pull request created" if create_pr else "Evaluation results uploaded"
        print(f"✓ {action} successfully for {repo_id}")
        print(f"  File: {file_path}")
        return True

    except Exception as e:
        print(f"Error uploading evaluation results: {e}", file=sys.stderr)
        return False


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Import evaluation results from Artificial Analysis API.",
    )
    parser.add_argument("--repo-id", required=True, help="HuggingFace repository ID")
    parser.add_argument("--benchmark", help="Specific benchmark to look up (e.g., HLE, GPQA)")
    parser.add_argument("--value", type=float, help="Manually provide the score (skips AA lookup)")
    parser.add_argument("--all", action="store_true", help="Import all available benchmarks")
    parser.add_argument("--source-user", help="HF username/org for attribution")
    parser.add_argument("--filename", default="artificial_analysis.yaml", help="Output filename")
    parser.add_argument("--create-pr", action="store_true", help="Create PR instead of direct push")
    parser.add_argument("--apply", action="store_true", help="Apply changes (default is dry run)")
    parser.add_argument("--pretty", action="store_true", help="Pretty-print YAML output")
    parser.add_argument("--verbose", action="store_true", help="Print progress to stderr")
    args = parser.parse_args()

    load_env()

    if args.value is not None and args.benchmark:
        metrics = [{"name": args.benchmark, "type": args.benchmark.lower(), "value": args.value}]
    else:
        api_key = os.getenv("AA_API_KEY")
        if not api_key:
            print("Error: AA_API_KEY is required to query Artificial Analysis.", file=sys.stderr)
            sys.exit(1)

        if args.verbose:
            print("Fetching models from Artificial Analysis...", file=sys.stderr)

        models = fetch_aa_models(api_key)

        if args.all:
            metrics = get_all_benchmarks_from_aa(models, args.repo_id)
            if not metrics:
                print(f"No benchmarks found for {args.repo_id} in Artificial Analysis", file=sys.stderr)
                sys.exit(1)
        elif args.benchmark:
            value = lookup_benchmark_from_aa(models, args.repo_id, args.benchmark)
            if value is None:
                print(f"Could not find {args.benchmark} score for {args.repo_id} in Artificial Analysis", file=sys.stderr)
                sys.exit(1)
            print(f"Found: {args.benchmark} = {value}")
            metrics = [{"name": args.benchmark, "type": args.benchmark.lower(), "value": value}]
        else:
            print("Error: Either --benchmark or --all is required", file=sys.stderr)
            sys.exit(1)

    eval_results = convert_to_eval_results_format(
        metrics=metrics,
        source_url="https://artificialanalysis.ai",
        source_name="Artificial Analysis",
        source_user=args.source_user,
    )

    if not eval_results:
        print("No benchmarks could be mapped to Hub dataset IDs", file=sys.stderr)
        sys.exit(1)

    import yaml
    print("\nImported evaluations (.eval_results/ format):")
    print(yaml.dump(eval_results, sort_keys=False, allow_unicode=True, default_flow_style=False))

    if args.apply or args.create_pr:
        upload_eval_results(
            repo_id=args.repo_id,
            results=eval_results,
            filename=args.filename,
            create_pr=args.create_pr,
        )


if __name__ == "__main__":
    main()