j-chim commited on
Commit
a969e99
·
verified ·
1 Parent(s): 74aad8e

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. Dockerfile +30 -0
  2. README.md +82 -6
  3. packages/eval-entity-resolver/pyproject.toml +16 -0
  4. packages/eval-entity-resolver/src/eval_entity_resolver/__init__.py +13 -0
  5. packages/eval-entity-resolver/src/eval_entity_resolver/__pycache__/__init__.cpython-314.pyc +0 -0
  6. packages/eval-entity-resolver/src/eval_entity_resolver/__pycache__/alias_store.cpython-314.pyc +0 -0
  7. packages/eval-entity-resolver/src/eval_entity_resolver/__pycache__/eee.cpython-314.pyc +0 -0
  8. packages/eval-entity-resolver/src/eval_entity_resolver/__pycache__/models.cpython-314.pyc +0 -0
  9. packages/eval-entity-resolver/src/eval_entity_resolver/__pycache__/normalization.cpython-314.pyc +0 -0
  10. packages/eval-entity-resolver/src/eval_entity_resolver/__pycache__/resolver.cpython-314.pyc +0 -0
  11. packages/eval-entity-resolver/src/eval_entity_resolver/alias_store.py +230 -0
  12. packages/eval-entity-resolver/src/eval_entity_resolver/eee.py +198 -0
  13. packages/eval-entity-resolver/src/eval_entity_resolver/models.py +21 -0
  14. packages/eval-entity-resolver/src/eval_entity_resolver/normalization.py +23 -0
  15. packages/eval-entity-resolver/src/eval_entity_resolver/resolver.py +69 -0
  16. packages/eval-entity-resolver/src/eval_entity_resolver/strategies/__init__.py +0 -0
  17. packages/eval-entity-resolver/src/eval_entity_resolver/strategies/__pycache__/__init__.cpython-314.pyc +0 -0
  18. packages/eval-entity-resolver/src/eval_entity_resolver/strategies/__pycache__/exact.cpython-314.pyc +0 -0
  19. packages/eval-entity-resolver/src/eval_entity_resolver/strategies/__pycache__/fuzzy.cpython-314.pyc +0 -0
  20. packages/eval-entity-resolver/src/eval_entity_resolver/strategies/__pycache__/normalized.cpython-314.pyc +0 -0
  21. packages/eval-entity-resolver/src/eval_entity_resolver/strategies/exact.py +11 -0
  22. packages/eval-entity-resolver/src/eval_entity_resolver/strategies/fuzzy.py +148 -0
  23. packages/eval-entity-resolver/src/eval_entity_resolver/strategies/normalized.py +20 -0
  24. pyproject.toml +44 -0
  25. src/eval_card_registry/__init__.py +0 -0
  26. src/eval_card_registry/__pycache__/__init__.cpython-314.pyc +0 -0
  27. src/eval_card_registry/__pycache__/cli.cpython-314.pyc +0 -0
  28. src/eval_card_registry/__pycache__/config.cpython-314.pyc +0 -0
  29. src/eval_card_registry/__pycache__/main.cpython-314.pyc +0 -0
  30. src/eval_card_registry/api/__init__.py +0 -0
  31. src/eval_card_registry/api/__pycache__/__init__.cpython-314.pyc +0 -0
  32. src/eval_card_registry/api/__pycache__/routes_aliases.cpython-314.pyc +0 -0
  33. src/eval_card_registry/api/__pycache__/routes_entities.cpython-314.pyc +0 -0
  34. src/eval_card_registry/api/__pycache__/routes_health.cpython-314.pyc +0 -0
  35. src/eval_card_registry/api/__pycache__/routes_resolve.cpython-314.pyc +0 -0
  36. src/eval_card_registry/api/__pycache__/schemas.cpython-314.pyc +0 -0
  37. src/eval_card_registry/api/routes_aliases.py +42 -0
  38. src/eval_card_registry/api/routes_entities.py +183 -0
  39. src/eval_card_registry/api/routes_health.py +47 -0
  40. src/eval_card_registry/api/routes_resolve.py +72 -0
  41. src/eval_card_registry/api/schemas.py +116 -0
  42. src/eval_card_registry/cli.py +259 -0
  43. src/eval_card_registry/config.py +25 -0
  44. src/eval_card_registry/main.py +48 -0
  45. src/eval_card_registry/services/__init__.py +0 -0
  46. src/eval_card_registry/services/__pycache__/__init__.cpython-314.pyc +0 -0
  47. src/eval_card_registry/services/__pycache__/ingestion.cpython-314.pyc +0 -0
  48. src/eval_card_registry/services/__pycache__/log_writer.cpython-314.pyc +0 -0
  49. src/eval_card_registry/services/__pycache__/resolution_service.cpython-314.pyc +0 -0
  50. src/eval_card_registry/services/ingestion.py +299 -0
Dockerfile ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install uv
6
+ RUN pip install --no-cache-dir uv
7
+
8
+ # Create non-root user (HF Spaces runs as UID 1000)
9
+ RUN useradd -m -u 1000 user
10
+
11
+ # Copy workspace definition and lockfile first for layer caching
12
+ COPY --chown=user pyproject.toml uv.lock ./
13
+ COPY --chown=user packages/eval-entity-resolver/pyproject.toml packages/eval-entity-resolver/
14
+
15
+ # Copy source
16
+ COPY --chown=user packages/eval-entity-resolver/src packages/eval-entity-resolver/src
17
+ COPY --chown=user src src
18
+
19
+ # Install all workspace packages
20
+ RUN uv sync --no-dev
21
+
22
+ USER user
23
+ ENV PATH="/app/.venv/bin:$PATH"
24
+ ENV HF_HOME=/tmp/hf_cache
25
+ ENV LOCAL_MODE=false
26
+ ENV READ_ONLY=true
27
+
28
+ EXPOSE 7860
29
+
30
+ CMD ["uvicorn", "eval_card_registry.main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,11 +1,87 @@
1
  ---
2
- title: Entity Registry
3
- emoji: 🦀
4
- colorFrom: red
5
- colorTo: indigo
6
  sdk: docker
 
7
  pinned: false
8
- short_description: WIP registry to help resolve benchmarks, models, and metrics
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: eval-card-registry
3
+ emoji: 🗂️
4
+ colorFrom: blue
5
+ colorTo: green
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
 
9
  ---
10
 
11
+ # eval-card-registry
12
+
13
+ Query-only disambiguation API for AI evaluation entity names. Resolves raw benchmark / model / metric / harness strings (e.g. `"MATH Level 5"`) to stable canonical IDs (`math`).
14
+
15
+ This Space runs in **read-only mode** — it serves lookups against pre-built entity data. Write operations (entity creation, alias edits) happen in a separate pipeline.
16
+
17
+ ## Base URL
18
+
19
+ ```
20
+ https://evaleval-entity-registry.hf.space/api/v1
21
+ ```
22
+
23
+ ## Resolve
24
+
25
+ ```bash
26
+ curl -X POST https://evaleval-entity-registry.hf.space/api/v1/resolve \
27
+ -H 'Content-Type: application/json' \
28
+ -d '{"raw_value": "MATH Level 5", "entity_type": "benchmark"}'
29
+ ```
30
+
31
+ Response:
32
+
33
+ ```json
34
+ {
35
+ "canonical_id": "math-level-5",
36
+ "strategy": "exact",
37
+ "confidence": 1.0,
38
+ "created_new": false,
39
+ "review_status": "reviewed"
40
+ }
41
+ ```
42
+
43
+ If nothing matches, `canonical_id` is `null` and `strategy` is `"no_match"`. In read-only mode, no draft entity is created.
44
+
45
+ `entity_type` is one of: `benchmark`, `model`, `metric`, `harness`. Optional `source_config` scopes the lookup to a specific source.
46
+
47
+ **Batch resolve:**
48
+
49
+ ```bash
50
+ curl -X POST https://evaleval-entity-registry.hf.space/api/v1/resolve/batch \
51
+ -H 'Content-Type: application/json' \
52
+ -d '[
53
+ {"raw_value": "MATH Level 5", "entity_type": "benchmark"},
54
+ {"raw_value": "meta-llama/Llama-3.1-8B", "entity_type": "model"}
55
+ ]'
56
+ ```
57
+
58
+ ## Browse entities
59
+
60
+ ```
61
+ GET /api/v1/benchmarks?search=math
62
+ GET /api/v1/benchmarks/{id}
63
+ GET /api/v1/models
64
+ GET /api/v1/metrics
65
+ GET /api/v1/harnesses
66
+ GET /api/v1/aliases?status=uncertain&entity_type=benchmark
67
+ ```
68
+
69
+ ## Health
70
+
71
+ ```
72
+ GET /api/v1/health
73
+ GET /api/v1/stats
74
+ ```
75
+
76
+ ## Write endpoints
77
+
78
+ Disabled in this Space. `POST`/`PATCH` on entities and aliases return `405 Method Not Allowed`. Mutations happen in the data pipeline (separate from this Space).
79
+
80
+ ## Interactive docs
81
+
82
+ OpenAPI docs at `/docs`.
83
+
84
+ ## Data sources
85
+
86
+ - Entity data: HF Dataset repo `evaleval/entity-registry-data` (read at startup)
87
+ - Resolve logs: HF Storage Bucket `evaleval/entity-registry-storage` (written asynchronously for resolver improvement)
packages/eval-entity-resolver/pyproject.toml ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "eval-entity-resolver"
7
+ version = "0.1.0"
8
+ requires-python = ">=3.11"
9
+ dependencies = [
10
+ "pandas>=2.2.0",
11
+ "pyarrow>=16.0.0",
12
+ "huggingface-hub>=0.23.0",
13
+ ]
14
+
15
+ [tool.hatch.build.targets.wheel]
16
+ packages = ["src/eval_entity_resolver"]
packages/eval-entity-resolver/src/eval_entity_resolver/__init__.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from eval_entity_resolver.alias_store import AliasStore
2
+ from eval_entity_resolver.eee import clean_eval_name, extract_metric
3
+ from eval_entity_resolver.models import ResolutionResult, ResolverConfig
4
+ from eval_entity_resolver.resolver import Resolver
5
+
6
+ __all__ = [
7
+ "AliasStore",
8
+ "Resolver",
9
+ "ResolverConfig",
10
+ "ResolutionResult",
11
+ "clean_eval_name",
12
+ "extract_metric",
13
+ ]
packages/eval-entity-resolver/src/eval_entity_resolver/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (578 Bytes). View file
 
packages/eval-entity-resolver/src/eval_entity_resolver/__pycache__/alias_store.cpython-314.pyc ADDED
Binary file (12.5 kB). View file
 
packages/eval-entity-resolver/src/eval_entity_resolver/__pycache__/eee.cpython-314.pyc ADDED
Binary file (7.55 kB). View file
 
packages/eval-entity-resolver/src/eval_entity_resolver/__pycache__/models.cpython-314.pyc ADDED
Binary file (1.72 kB). View file
 
packages/eval-entity-resolver/src/eval_entity_resolver/__pycache__/normalization.cpython-314.pyc ADDED
Binary file (1.55 kB). View file
 
packages/eval-entity-resolver/src/eval_entity_resolver/__pycache__/resolver.cpython-314.pyc ADDED
Binary file (2.68 kB). View file
 
packages/eval-entity-resolver/src/eval_entity_resolver/alias_store.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import uuid
4
+ from datetime import datetime, timezone
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ import pandas as pd
9
+
10
+
11
+ _SCHEMA = {
12
+ "id": pd.StringDtype(),
13
+ "raw_value": pd.StringDtype(),
14
+ "entity_type": pd.StringDtype(),
15
+ "canonical_id": pd.StringDtype(),
16
+ "source_config": pd.StringDtype(),
17
+ "source_field": pd.StringDtype(),
18
+ "status": pd.StringDtype(),
19
+ "strategy": pd.StringDtype(),
20
+ "confidence": "float64",
21
+ "notes": pd.StringDtype(),
22
+ "created_at": pd.StringDtype(),
23
+ "updated_at": pd.StringDtype(),
24
+ }
25
+
26
+
27
+ def _empty_df() -> pd.DataFrame:
28
+ return pd.DataFrame({col: pd.Series(dtype=dtype) for col, dtype in _SCHEMA.items()})
29
+
30
+
31
+ class AliasStore:
32
+ """Wraps the aliases table. Loaded into memory; writes are in-memory only."""
33
+
34
+ def __init__(self, df: pd.DataFrame, read_only: bool = False) -> None:
35
+ self._df = df.copy()
36
+ self.read_only = read_only
37
+ # Per-entity_type caches — built lazily on first access
38
+ # Normalized lookup key: (entity_type, source_config or None)
39
+ self._normalized_cache: dict[tuple[str, Optional[str]], dict[str, str]] = {}
40
+ self._candidates_cache: dict[tuple[str, Optional[str]], list[tuple[str, str]]] = {}
41
+ self._lookup_index: dict[tuple[str, str, Optional[str]], str] | None = None
42
+
43
+ def _ensure_lookup_index(self) -> None:
44
+ """Build a dict index for O(1) exact lookups."""
45
+ if self._lookup_index is not None:
46
+ return
47
+ self._lookup_index = {}
48
+ df = self._df[self._df["status"] != "rejected"]
49
+ for _, row in df.iterrows():
50
+ key = (row["raw_value"], row["entity_type"], row.get("source_config"))
51
+ self._lookup_index[key] = row["canonical_id"]
52
+
53
+ def _invalidate_caches(self) -> None:
54
+ self._normalized_cache.clear()
55
+ self._candidates_cache.clear()
56
+ self._lookup_index = None
57
+
58
+ # ------------------------------------------------------------------
59
+ # Constructors
60
+ # ------------------------------------------------------------------
61
+
62
+ @classmethod
63
+ def from_parquet(cls, path: str | Path, read_only: bool = False) -> "AliasStore":
64
+ p = Path(path) / "aliases.parquet"
65
+ if p.exists():
66
+ df = pd.read_parquet(p)
67
+ else:
68
+ df = _empty_df()
69
+ return cls(df, read_only=read_only)
70
+
71
+ @classmethod
72
+ def from_hf(cls, repo_id: str, read_only: bool = False) -> "AliasStore":
73
+ from huggingface_hub import hf_hub_download
74
+
75
+ try:
76
+ local = hf_hub_download(
77
+ repo_id=repo_id,
78
+ filename="aliases/part-0.parquet",
79
+ repo_type="dataset",
80
+ )
81
+ df = pd.read_parquet(local)
82
+ except Exception:
83
+ df = _empty_df()
84
+ return cls(df, read_only=read_only)
85
+
86
+ # ------------------------------------------------------------------
87
+ # Lookup
88
+ # ------------------------------------------------------------------
89
+
90
+ def lookup(
91
+ self,
92
+ raw_value: str,
93
+ entity_type: str,
94
+ source_config: Optional[str],
95
+ ) -> Optional[str]:
96
+ """Return canonical_id for first non-rejected match. Config-scoped before global."""
97
+ self._ensure_lookup_index()
98
+ # Config-scoped
99
+ if source_config:
100
+ result = self._lookup_index.get((raw_value, entity_type, source_config))
101
+ if result is not None:
102
+ return result
103
+ # Global
104
+ return self._lookup_index.get((raw_value, entity_type, None))
105
+
106
+ # ------------------------------------------------------------------
107
+ # Writes (in-memory only; caller is responsible for persistence)
108
+ # ------------------------------------------------------------------
109
+
110
+ def add_alias(
111
+ self,
112
+ raw_value: str,
113
+ entity_type: str,
114
+ canonical_id: str,
115
+ source_config: Optional[str],
116
+ source_field: Optional[str],
117
+ status: str,
118
+ strategy: str,
119
+ confidence: float,
120
+ ) -> None:
121
+ if self.read_only:
122
+ raise RuntimeError("AliasStore is read-only")
123
+ now = datetime.now(timezone.utc).isoformat()
124
+ row = {
125
+ "id": str(uuid.uuid4()),
126
+ "raw_value": raw_value,
127
+ "entity_type": entity_type,
128
+ "canonical_id": canonical_id,
129
+ "source_config": source_config,
130
+ "source_field": source_field,
131
+ "status": status,
132
+ "strategy": strategy,
133
+ "confidence": confidence,
134
+ "notes": None,
135
+ "created_at": now,
136
+ "updated_at": now,
137
+ }
138
+ self._df = pd.concat([self._df, pd.DataFrame([row])], ignore_index=True)
139
+ self._invalidate_caches()
140
+
141
+ def update_alias(
142
+ self,
143
+ raw_value: str,
144
+ entity_type: str,
145
+ source_config: Optional[str],
146
+ canonical_id: str,
147
+ status: str,
148
+ strategy: str,
149
+ confidence: float,
150
+ ) -> None:
151
+ """Upsert: update existing alias row or add new one."""
152
+ if self.read_only:
153
+ raise RuntimeError("AliasStore is read-only")
154
+ df = self._df
155
+ mask = (df["raw_value"] == raw_value) & (df["entity_type"] == entity_type)
156
+ if source_config:
157
+ mask = mask & (df["source_config"] == source_config)
158
+ else:
159
+ mask = mask & df["source_config"].isna()
160
+ if mask.any():
161
+ now = datetime.now(timezone.utc).isoformat()
162
+ self._df.loc[mask, "canonical_id"] = canonical_id
163
+ self._df.loc[mask, "status"] = status
164
+ self._df.loc[mask, "strategy"] = strategy
165
+ self._df.loc[mask, "confidence"] = confidence
166
+ self._df.loc[mask, "updated_at"] = now
167
+ self._invalidate_caches()
168
+ else:
169
+ self.add_alias(raw_value, entity_type, canonical_id, source_config, None, status, strategy, confidence)
170
+
171
+ # ------------------------------------------------------------------
172
+ # Export
173
+ # ------------------------------------------------------------------
174
+
175
+ def to_dataframe(self) -> pd.DataFrame:
176
+ return self._df.copy()
177
+
178
+ def get_normalized_lookup(
179
+ self, entity_type: str, source_config: Optional[str] = None
180
+ ) -> dict[str, str]:
181
+ """Return {normalized_raw_value: canonical_id} for use by strategies.
182
+
183
+ When ``source_config`` is given, the returned map merges config-scoped
184
+ aliases on top of global (source_config IS NULL) aliases, so scoped
185
+ matches win over global for the same normalized form. When
186
+ ``source_config`` is None, only global aliases are included — scoped
187
+ aliases do NOT leak into unrelated lookups.
188
+ """
189
+ key = (entity_type, source_config)
190
+ if key in self._normalized_cache:
191
+ return self._normalized_cache[key]
192
+
193
+ from eval_entity_resolver.normalization import normalize
194
+
195
+ base = self._df[(self._df["entity_type"] == entity_type) & (self._df["status"] != "rejected")]
196
+ # Start from global aliases.
197
+ global_df = base[base["source_config"].isna()]
198
+ result: dict[str, str] = {}
199
+ for _, row in global_df.iterrows():
200
+ result[normalize(row["raw_value"])] = row["canonical_id"]
201
+ # Overlay scoped aliases for the requested source_config.
202
+ if source_config:
203
+ scoped_df = base[base["source_config"] == source_config]
204
+ for _, row in scoped_df.iterrows():
205
+ result[normalize(row["raw_value"])] = row["canonical_id"]
206
+ self._normalized_cache[key] = result
207
+ return result
208
+
209
+ def get_all_for_type(
210
+ self, entity_type: str, source_config: Optional[str] = None
211
+ ) -> list[tuple[str, str]]:
212
+ """Return [(raw_value, canonical_id)] for non-rejected aliases of ``entity_type``.
213
+
214
+ Filtering matches ``get_normalized_lookup`` — when ``source_config`` is
215
+ given, includes global + that config's scoped aliases; otherwise global
216
+ only. Cached per (entity_type, source_config).
217
+ """
218
+ key = (entity_type, source_config)
219
+ if key in self._candidates_cache:
220
+ return self._candidates_cache[key]
221
+
222
+ base = self._df[(self._df["entity_type"] == entity_type) & (self._df["status"] != "rejected")]
223
+ if source_config:
224
+ mask = base["source_config"].isna() | (base["source_config"] == source_config)
225
+ df = base[mask]
226
+ else:
227
+ df = base[base["source_config"].isna()]
228
+ result = list(zip(df["raw_value"].tolist(), df["canonical_id"].tolist()))
229
+ self._candidates_cache[key] = result
230
+ return result
packages/eval-entity-resolver/src/eval_entity_resolver/eee.py ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ EEE-specific preprocessing for entity resolution.
3
+
4
+ Raw strings from the EEE datastore often encode multiple entity types in a
5
+ single field (e.g. ``evaluation_name`` contains both benchmark and metric).
6
+ These helpers extract clean, resolvable strings before passing them to the
7
+ resolver.
8
+
9
+ Usage::
10
+
11
+ from eval_entity_resolver.eee import extract_metric, clean_eval_name
12
+
13
+ metric_raw = extract_metric("Accuracy on IFEval") # → "Accuracy"
14
+ bench_raw = clean_eval_name("bfcl.live.live_accuracy") # → "bfcl live"
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import re
19
+
20
+
21
+ # ------------------------------------------------------------------
22
+ # Metric extraction
23
+ # ------------------------------------------------------------------
24
+
25
+ def extract_metric(metric_desc: str) -> str:
26
+ """Extract a reusable metric name from an EEE evaluation description.
27
+
28
+ EEE configs rarely provide a structured metric_id. Instead the metric
29
+ lives inside ``evaluation_description`` in one of several formats:
30
+
31
+ * **"X on Y"** — ``"Accuracy on IFEval"`` → ``"Accuracy"``
32
+ * **Dot notation** — ``"bfcl.live.live_accuracy"`` → ``"accuracy"``
33
+ * **Verbose description** — ``"Chat accuracy - includes easy subsets"``
34
+ → ``"accuracy"`` (keyword extraction)
35
+ * **No keyword** — ``"Global MMLU Lite - Arabic"`` → ``"score"``
36
+ (generic fallback)
37
+
38
+ The returned string is passed to the resolver, which maps it to a
39
+ canonical metric entity via alias lookup / normalized match.
40
+ """
41
+ text = metric_desc.strip()
42
+ if not text:
43
+ return text
44
+
45
+ from_dot = False
46
+
47
+ # 1. Dot notation: "bfcl.live.live_accuracy" → last segment → "live accuracy"
48
+ if "." in text and " " not in text:
49
+ text = text.rsplit(".", 1)[1].replace("_", " ").strip()
50
+ from_dot = True
51
+
52
+ # 2. "X on Y" pattern: "Accuracy on IFEval" → "Accuracy"
53
+ if not from_dot:
54
+ m = re.match(r"^(.+?)\s+on\s+\S+", text, re.IGNORECASE)
55
+ if m:
56
+ text = m.group(1).strip()
57
+
58
+ # 3. Try keyword extraction on any multi-word text or dot-notation segment.
59
+ # Single bare words ("Accuracy", "F1", "EM") pass straight to the resolver.
60
+ word_count = len(text.split())
61
+ needs_extraction = from_dot or word_count > 1
62
+
63
+ if needs_extraction:
64
+ canonical = _keyword_extract(text)
65
+ if canonical:
66
+ return canonical
67
+ # No keyword found — verbose descriptions (4+ words) → generic fallback.
68
+ # Short phrases (2-3 words) pass through so the resolver can still
69
+ # match them via alias (e.g. "Equivalent (CoT)" → cot-correct).
70
+ if not from_dot and word_count > 3:
71
+ return "score"
72
+
73
+ return text
74
+
75
+
76
+ # Ordered from most-specific to most-generic. When multiple patterns
77
+ # match, the earliest *position* in the input text wins (see
78
+ # _keyword_extract).
79
+ _METRIC_KEYWORDS: list[tuple[str, str]] = [
80
+ # Multi-word / compound patterns
81
+ (r"pass@8", "Pass@8"),
82
+ (r"pass@1", "Pass@1"),
83
+ (r"mean[\s_-]*win[\s_-]*rate", "Mean Win Rate"),
84
+ (r"win[\s_-]*rate", "Win Rate"),
85
+ (r"mean[\s_-]*response[\s_-]*time", "Mean Response Time"),
86
+ (r"mean[\s_-]*score", "Mean Score"),
87
+ (r"exact[\s_-]*match", "Exact Match"),
88
+ (r"bleu[\s_-]*4", "BLEU-4"),
89
+ (r"cot[\s_-]*correct", "COT correct"),
90
+ (r"wb[\s_-]*score", "WB Score"),
91
+ (r"avg[\s_-]*attempts", "Average Attempts"),
92
+ (r"latency[\s_-]*mean", "mean-latency"),
93
+ (r"latency.*(?:p95|95th)", "p95-latency"),
94
+ (r"latency.*(?:std|standard)", "latency-stddev"),
95
+ (r"max[\s_-]*delta", "max-delta"),
96
+ (r"benchmark\s+evaluation", "score"),
97
+ (r"outperform", "rank"),
98
+ # Compound accuracy types (before generic accuracy)
99
+ # Patterns sourced from metric_names in evaleval/card_backend eval-list.
100
+ (r"ast[\s_-]*accuracy", "AST Accuracy"),
101
+ (r"overall[\s_-]*accuracy", "Accuracy"),
102
+ (r"(?:ir)?relevance[\s_-]*detection[\s_-]*accuracy", "Accuracy"),
103
+ (r"no[\s_-]*snippet[\s_-]*accuracy", "Accuracy"),
104
+ (r"long[\s_-]*context[\s_-]*accuracy", "Accuracy"),
105
+ (r"kv[\s_-]*accuracy", "Accuracy"),
106
+ (r"vector[\s_-]*accuracy", "Accuracy"),
107
+ (r"recursive[\s_-]*summarization[\s_-]*accuracy", "Accuracy"),
108
+ (r"total[\s_-]*cost", "cost"),
109
+ (r"cost[\s_-]*per[\s_-]*task", "cost-per-task"),
110
+ # Single-word patterns (generic, checked last by position)
111
+ (r"\baccuracy\b", "Accuracy"),
112
+ (r"\bacc\b", "Accuracy"),
113
+ (r"\bscores?\b", "score"),
114
+ (r"\bf1\b", "F1"),
115
+ (r"\bem\b", "Exact Match"),
116
+ (r"\belo\b", "Elo Rating"),
117
+ (r"\branks?\b", "rank"),
118
+ (r"\bcosts?\b", "cost"),
119
+ (r"\bharmlessness\b", "harmlessness"),
120
+ (r"\bstddev\b", "stddev"),
121
+ ]
122
+
123
+
124
+ def _keyword_extract(text: str) -> str | None:
125
+ """Return the canonical metric name for the first keyword found in *text*."""
126
+ lower = text.lower()
127
+ best: str | None = None
128
+ best_pos = len(lower) + 1
129
+ for pattern, canonical in _METRIC_KEYWORDS:
130
+ m = re.search(pattern, lower)
131
+ if m and m.start() < best_pos:
132
+ best_pos = m.start()
133
+ best = canonical
134
+ return best
135
+
136
+
137
+ # ------------------------------------------------------------------
138
+ # Benchmark-name cleaning
139
+ # ------------------------------------------------------------------
140
+
141
+ # Trailing metric patterns for space-separated names (e.g.
142
+ # "Gaming Score" → "Gaming"). Checked with ``re.search`` against
143
+ # the lowered name; the first match wins.
144
+ _TRAILING_METRIC_RE: list[str] = [
145
+ r"mean\s+win\s+rate$",
146
+ r"mean\s+response\s+time$",
147
+ r"mean\s+score$",
148
+ r"win\s+rate$",
149
+ r"avg\s+attempts$",
150
+ r"avg\s+latency\s+ms$",
151
+ r"cost\s+per\s+\d+\s+calls\s+usd$",
152
+ r"cost\s+per\s+task$",
153
+ r"pass@\d+$",
154
+ r"\b(?:score|accuracy|acc|elo|rank|f1|em)$",
155
+ ]
156
+
157
+
158
+ def clean_eval_name(eval_name: str) -> str:
159
+ """Strip embedded metric information from an ``evaluation_name``.
160
+
161
+ EEE configs often encode both benchmark *and* metric in a single
162
+ ``evaluation_name`` string. This function extracts the benchmark
163
+ portion so that the metric lives only in ``metric_id``.
164
+
165
+ Patterns handled:
166
+
167
+ * **Dot notation** — ``"bfcl.live.live_accuracy"`` → ``"bfcl live"``
168
+ (last segment is the metric, everything before is the benchmark)
169
+ * **Underscore suffix** — ``"fibble1_arena_win_rate"`` → ``"fibble1 arena"``
170
+ * **Trailing words** — ``"Gaming Score"`` → ``"Gaming"``
171
+ """
172
+ name = eval_name.strip()
173
+ if not name:
174
+ return name
175
+
176
+ # --- 1. Dot notation: split on last dot ------------------------------
177
+ # The last segment is the metric; everything before is the benchmark.
178
+ # e.g. "bfcl.live.live_simple_ast_accuracy" → "bfcl live"
179
+ if "." in name and " " not in name:
180
+ parts = name.rsplit(".", 1)[0].split(".")
181
+ return " ".join(p.replace("_", " ") for p in parts)
182
+
183
+ # --- 2. Underscore/space names: strip trailing metric keywords -------
184
+ # Normalise underscores to spaces so "fibble1_arena_win_rate" and
185
+ # "Gaming Score" use the same codepath.
186
+ has_underscores = "_" in name and " " not in name
187
+ normalized = name.replace("_", " ") if has_underscores else name
188
+
189
+ lower = normalized.lower()
190
+ for pattern in _TRAILING_METRIC_RE:
191
+ m = re.search(pattern, lower)
192
+ if m:
193
+ prefix = normalized[: m.start()].strip()
194
+ if prefix:
195
+ return prefix
196
+ break # matched but prefix is empty — fall through
197
+
198
+ return name
packages/eval-entity-resolver/src/eval_entity_resolver/models.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass, field
2
+ from typing import Literal, Optional
3
+
4
+
5
+ ResolutionStrategy = Literal["exact", "normalized", "fuzzy", "no_match"]
6
+ EntityType = Literal["model", "benchmark", "metric", "harness"]
7
+
8
+
9
+ @dataclass
10
+ class ResolutionResult:
11
+ raw_value: str
12
+ entity_type: EntityType
13
+ source_config: Optional[str]
14
+ canonical_id: Optional[str]
15
+ strategy: ResolutionStrategy
16
+ confidence: float
17
+
18
+
19
+ @dataclass
20
+ class ResolverConfig:
21
+ threshold: float = 0.85
packages/eval-entity-resolver/src/eval_entity_resolver/normalization.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+
3
+
4
+ def normalize(value: str) -> str:
5
+ """Lowercase, strip, collapse all separators (space/_/-/slash) to one space.
6
+
7
+ Collapsing ``/`` with the other separators means ``tau-bench-2/airline``
8
+ and ``tau-bench-2_airline`` normalize identically — critical for generalized
9
+ ingestion where the same benchmark may appear with slash, underscore, or
10
+ hyphen separators across configs. False merges across distinct canonical
11
+ IDs are prevented by fuzzy's suffix-stripping being the *only* stem rewrite
12
+ we apply (no generic similarity).
13
+
14
+ Dots between digits are converted to spaces first so that version
15
+ numbers like ``4.5`` and ``4-5`` normalize identically (both → ``4 5``).
16
+ """
17
+ value = value.lower()
18
+ value = value.strip()
19
+ # Convert dots between digits to spaces (e.g. "4.5" → "4 5")
20
+ value = re.sub(r"(?<=\d)\.(?=\d)", " ", value)
21
+ value = re.sub(r"[^\w\s\-/]", "", value) # remove punctuation first
22
+ value = re.sub(r"[\s_\-/]+", " ", value).strip() # collapse separators
23
+ return value
packages/eval-entity-resolver/src/eval_entity_resolver/resolver.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+
3
+ from eval_entity_resolver.alias_store import AliasStore
4
+ from eval_entity_resolver.models import ResolutionResult, ResolverConfig
5
+ from eval_entity_resolver.strategies.exact import exact_match
6
+ from eval_entity_resolver.strategies.normalized import normalized_match
7
+ from eval_entity_resolver.strategies.fuzzy import fuzzy_match
8
+
9
+
10
+ class Resolver:
11
+ def __init__(self, store: AliasStore, config: Optional[ResolverConfig] = None) -> None:
12
+ self.store = store
13
+ self.config = config or ResolverConfig()
14
+
15
+ def resolve(
16
+ self,
17
+ raw_value: str,
18
+ entity_type: str,
19
+ source_config: Optional[str] = None,
20
+ ) -> ResolutionResult:
21
+ # 1. Exact
22
+ canonical_id = exact_match(raw_value, entity_type, source_config, self.store)
23
+ if canonical_id is not None:
24
+ return ResolutionResult(
25
+ raw_value=raw_value,
26
+ entity_type=entity_type,
27
+ source_config=source_config,
28
+ canonical_id=canonical_id,
29
+ strategy="exact",
30
+ confidence=1.0,
31
+ )
32
+
33
+ # 2. Normalized (confidence 0.95 — only return if above threshold)
34
+ _NORMALIZED_CONFIDENCE = 0.95
35
+ if _NORMALIZED_CONFIDENCE >= self.config.threshold:
36
+ canonical_id = normalized_match(raw_value, entity_type, self.store, source_config)
37
+ if canonical_id is not None:
38
+ return ResolutionResult(
39
+ raw_value=raw_value,
40
+ entity_type=entity_type,
41
+ source_config=source_config,
42
+ canonical_id=canonical_id,
43
+ strategy="normalized",
44
+ confidence=_NORMALIZED_CONFIDENCE,
45
+ )
46
+
47
+ # 3. Fuzzy
48
+ canonical_id, confidence = fuzzy_match(
49
+ raw_value, entity_type, self.config.threshold, self.store, source_config
50
+ )
51
+ if canonical_id is not None:
52
+ return ResolutionResult(
53
+ raw_value=raw_value,
54
+ entity_type=entity_type,
55
+ source_config=source_config,
56
+ canonical_id=canonical_id,
57
+ strategy="fuzzy",
58
+ confidence=confidence,
59
+ )
60
+
61
+ # 4. No match
62
+ return ResolutionResult(
63
+ raw_value=raw_value,
64
+ entity_type=entity_type,
65
+ source_config=source_config,
66
+ canonical_id=None,
67
+ strategy="no_match",
68
+ confidence=0.0,
69
+ )
packages/eval-entity-resolver/src/eval_entity_resolver/strategies/__init__.py ADDED
File without changes
packages/eval-entity-resolver/src/eval_entity_resolver/strategies/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (216 Bytes). View file
 
packages/eval-entity-resolver/src/eval_entity_resolver/strategies/__pycache__/exact.cpython-314.pyc ADDED
Binary file (841 Bytes). View file
 
packages/eval-entity-resolver/src/eval_entity_resolver/strategies/__pycache__/fuzzy.cpython-314.pyc ADDED
Binary file (4.78 kB). View file
 
packages/eval-entity-resolver/src/eval_entity_resolver/strategies/__pycache__/normalized.cpython-314.pyc ADDED
Binary file (1.19 kB). View file
 
packages/eval-entity-resolver/src/eval_entity_resolver/strategies/exact.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+
3
+
4
+ def exact_match(
5
+ raw_value: str,
6
+ entity_type: str,
7
+ source_config: Optional[str],
8
+ alias_store,
9
+ ) -> Optional[str]:
10
+ """Direct lookup in alias store — config-scoped then global."""
11
+ return alias_store.lookup(raw_value, entity_type, source_config)
packages/eval-entity-resolver/src/eval_entity_resolver/strategies/fuzzy.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Fuzzy matching strategy.
3
+
4
+ Instead of generic string similarity (which falsely merges distinct versions,
5
+ sizes, and variants), this uses two targeted approaches:
6
+
7
+ 1. **Stem matching** — strip known non-semantic suffixes (evaluation mode,
8
+ hosting provider, effort level) and check if the stem has an exact or
9
+ normalized match. This catches real duplicates like
10
+ ``model-name-fc`` → ``model-name`` without collapsing ``gpt-5-mini`` into
11
+ ``gpt-5``.
12
+
13
+ 2. **Org normalization** — handle cases where the org prefix differs between
14
+ configs (``deepseek-ai/model`` vs ``deepseek/model``).
15
+
16
+ If neither approach produces a match the strategy returns None so the resolver
17
+ can fall through to auto-draft.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import re
22
+ from typing import Optional
23
+
24
+ from eval_entity_resolver.normalization import normalize
25
+
26
+
27
+ # Suffixes stripped only from the *end* of the raw value. Order matters:
28
+ # longer suffixes first to avoid partial stripping.
29
+ #
30
+ # Many of these patterns were identified from raw_model_ids in the
31
+ # evaleval/card_backend dataset, where a single canonical model family
32
+ # (e.g. ``anthropic/claude-opus-4-5``) has variants like
33
+ # ``claude-opus-4-5-20251101-thinking-16k``, ``-fc``, ``-prompt``, etc.
34
+ _STRIP_SUFFIXES = [
35
+ # Evaluation-mode suffixes (BFCL, etc.)
36
+ "-fc",
37
+ "-prompt",
38
+ # Hosting-provider suffixes
39
+ "-together",
40
+ "-bedrock",
41
+ # Reasoning-effort suffixes
42
+ "-high",
43
+ "-medium",
44
+ "-low",
45
+ "-minimal",
46
+ # Thinking-style suffixes
47
+ "-nothink",
48
+ "-thinking-none",
49
+ ]
50
+
51
+ # Regex-based suffix patterns applied after the literal suffixes.
52
+ # These capture variants with numeric parameters (thinking budgets, dates).
53
+ # Each pattern must anchor with $ and match only the tail of the string.
54
+ _STRIP_SUFFIX_PATTERNS: list[re.Pattern[str]] = [
55
+ # Thinking-budget suffix: "-thinking-8k", "-thinking-16k", "-thinking-64k"
56
+ re.compile(r"-thinking-\d+k$", re.IGNORECASE),
57
+ # Date version suffix (YYYYMMDD): "-20251101", "-20240315"
58
+ # Only strip dates (8 consecutive digits) to avoid touching version numbers.
59
+ re.compile(r"-\d{8}$"),
60
+ ]
61
+
62
+ # Known org aliases: {variant_prefix: canonical_prefix}
63
+ # Convention: simplify HF org names (e.g. "deepseek-ai" → "deepseek") to the
64
+ # shorter form used as canonical in this registry.
65
+ _ORG_ALIASES: dict[str, str] = {
66
+ "deepseek-ai": "deepseek",
67
+ "cohereforai": "cohere",
68
+ "cohere-labs": "cohere",
69
+ "tii-uae": "tiiuae",
70
+ "meta-llama": "meta",
71
+ "mistral-ai": "mistralai",
72
+ "nvidia-nemo": "nvidia",
73
+ }
74
+
75
+ # Confidence assigned to stem-match results. Below 1.0 (exact) and 0.95
76
+ # (normalized) so the provenance is clear in the resolution log.
77
+ _STEM_CONFIDENCE = 0.90
78
+
79
+
80
+ def _strip_suffix(value: str) -> str | None:
81
+ """Strip a single known suffix. Returns the stem or None if no suffix matched."""
82
+ lower = value.lower()
83
+ for suffix in _STRIP_SUFFIXES:
84
+ if lower.endswith(suffix):
85
+ return value[: len(value) - len(suffix)]
86
+ for pattern in _STRIP_SUFFIX_PATTERNS:
87
+ m = pattern.search(value)
88
+ if m:
89
+ return value[: m.start()]
90
+ return None
91
+
92
+
93
+ def _normalize_org(value: str) -> str | None:
94
+ """Replace a known org-alias prefix. Returns the rewritten string or None."""
95
+ if "/" not in value:
96
+ return None
97
+ org, rest = value.split("/", 1)
98
+ canonical_org = _ORG_ALIASES.get(org.lower())
99
+ if canonical_org is None:
100
+ return None
101
+ return f"{canonical_org}/{rest}"
102
+
103
+
104
+ def fuzzy_match(
105
+ raw_value: str,
106
+ entity_type: str,
107
+ threshold: float, # kept for API compat; not used by stem matching
108
+ alias_store,
109
+ source_config: Optional[str] = None,
110
+ ) -> tuple[Optional[str], float]:
111
+ """
112
+ Attempt targeted fuzzy resolution.
113
+
114
+ Returns ``(canonical_id, confidence)``; canonical_id is None on no match.
115
+ """
116
+ candidates_to_try: list[str] = []
117
+
118
+ # 1. Suffix stripping (may produce multiple stems: strip one, strip two, etc.)
119
+ stripped = _strip_suffix(raw_value)
120
+ if stripped:
121
+ candidates_to_try.append(stripped)
122
+ # Try double-strip (e.g. "model-fc-together" — unlikely but cheap)
123
+ double = _strip_suffix(stripped)
124
+ if double:
125
+ candidates_to_try.append(double)
126
+
127
+ # 2. Org normalization — on both original and stripped forms
128
+ for val in [raw_value] + candidates_to_try[:]:
129
+ rewritten = _normalize_org(val)
130
+ if rewritten:
131
+ candidates_to_try.append(rewritten)
132
+
133
+ # 3. Check each candidate against exact then normalized lookups.
134
+ # Scoped-aware: config-scoped aliases for ``source_config`` count as
135
+ # candidates; unrelated scoped aliases are excluded.
136
+ norm_lookup = alias_store.get_normalized_lookup(entity_type, source_config)
137
+
138
+ for candidate in candidates_to_try:
139
+ exact_id = alias_store.lookup(candidate, entity_type, source_config)
140
+ if exact_id is not None:
141
+ return exact_id, _STEM_CONFIDENCE
142
+
143
+ norm = normalize(candidate)
144
+ canonical_id = norm_lookup.get(norm)
145
+ if canonical_id is not None:
146
+ return canonical_id, _STEM_CONFIDENCE
147
+
148
+ return None, 0.0
packages/eval-entity-resolver/src/eval_entity_resolver/strategies/normalized.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+
3
+ from eval_entity_resolver.normalization import normalize
4
+
5
+
6
+ def normalized_match(
7
+ raw_value: str,
8
+ entity_type: str,
9
+ alias_store,
10
+ source_config: Optional[str] = None,
11
+ ) -> Optional[str]:
12
+ """Normalize input and look up against normalized alias index.
13
+
14
+ When ``source_config`` is given, scoped aliases for that config are
15
+ considered in addition to global aliases; otherwise scoped aliases are
16
+ excluded.
17
+ """
18
+ norm = normalize(raw_value)
19
+ lookup = alias_store.get_normalized_lookup(entity_type, source_config)
20
+ return lookup.get(norm)
pyproject.toml ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "eval-card-registry"
7
+ version = "0.1.0"
8
+ requires-python = ">=3.11"
9
+ dependencies = [
10
+ "fastapi>=0.111.0",
11
+ "uvicorn[standard]>=0.29.0",
12
+ "typer>=0.12.0",
13
+ "pydantic>=2.7.0",
14
+ "pydantic-settings>=2.2.0",
15
+ "pandas>=2.2.0",
16
+ "pyarrow>=16.0.0",
17
+ "huggingface-hub>=0.23.0",
18
+ "datasets>=2.19.0",
19
+ "pyyaml>=6.0",
20
+ "eval-entity-resolver",
21
+ ]
22
+
23
+ [project.scripts]
24
+ eval-card-registry = "eval_card_registry.cli:app"
25
+
26
+ [tool.hatch.build.targets.wheel]
27
+ packages = ["src/eval_card_registry"]
28
+
29
+ [tool.uv.workspace]
30
+ members = ["packages/eval-entity-resolver"]
31
+
32
+ [tool.uv.sources]
33
+ eval-entity-resolver = { workspace = true }
34
+
35
+ [dependency-groups]
36
+ dev = [
37
+ "pytest>=8.0.0",
38
+ "pytest-asyncio>=0.23.0",
39
+ "httpx>=0.27.0",
40
+ ]
41
+
42
+ [tool.pytest.ini_options]
43
+ asyncio_mode = "auto"
44
+ testpaths = ["tests", "packages/eval-entity-resolver/tests"]
src/eval_card_registry/__init__.py ADDED
File without changes
src/eval_card_registry/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (173 Bytes). View file
 
src/eval_card_registry/__pycache__/cli.cpython-314.pyc ADDED
Binary file (12.1 kB). View file
 
src/eval_card_registry/__pycache__/config.cpython-314.pyc ADDED
Binary file (1.63 kB). View file
 
src/eval_card_registry/__pycache__/main.cpython-314.pyc ADDED
Binary file (2.34 kB). View file
 
src/eval_card_registry/api/__init__.py ADDED
File without changes
src/eval_card_registry/api/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (177 Bytes). View file
 
src/eval_card_registry/api/__pycache__/routes_aliases.cpython-314.pyc ADDED
Binary file (2.95 kB). View file
 
src/eval_card_registry/api/__pycache__/routes_entities.cpython-314.pyc ADDED
Binary file (13.3 kB). View file
 
src/eval_card_registry/api/__pycache__/routes_health.cpython-314.pyc ADDED
Binary file (3.7 kB). View file
 
src/eval_card_registry/api/__pycache__/routes_resolve.cpython-314.pyc ADDED
Binary file (4.3 kB). View file
 
src/eval_card_registry/api/__pycache__/schemas.cpython-314.pyc ADDED
Binary file (9.15 kB). View file
 
src/eval_card_registry/api/routes_aliases.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Alias management routes — v0-defer (read + patch only; no review UI yet)."""
2
+ from typing import Optional
3
+
4
+ from fastapi import APIRouter, Depends, HTTPException
5
+
6
+ from eval_card_registry.api.schemas import AliasPatch, AliasStatus, EntityType
7
+ from eval_card_registry.config import settings
8
+ from eval_card_registry.store.hf_store import get_store, RegistryStore
9
+ from eval_card_registry.store import queries
10
+
11
+
12
+ def _check_writable():
13
+ if settings.read_only:
14
+ raise HTTPException(status_code=405, detail="Write operations disabled in read-only mode")
15
+
16
+
17
+ router = APIRouter()
18
+ _writable = [Depends(_check_writable)]
19
+
20
+
21
+ @router.get("/aliases")
22
+ def list_aliases(
23
+ status: Optional[AliasStatus] = None,
24
+ entity_type: Optional[EntityType] = None,
25
+ source_config: Optional[str] = None,
26
+ store: RegistryStore = Depends(get_store),
27
+ ):
28
+ return queries.list_entities(
29
+ store,
30
+ "aliases",
31
+ review_status=None,
32
+ **{k: v for k, v in {"status": status, "entity_type": entity_type, "source_config": source_config}.items() if v is not None},
33
+ )
34
+
35
+
36
+ @router.patch("/aliases/{alias_id}", dependencies=_writable)
37
+ def patch_alias(alias_id: str, body: AliasPatch, store: RegistryStore = Depends(get_store)):
38
+ updates = {k: v for k, v in body.model_dump().items() if v is not None}
39
+ result = queries.update_alias(store, alias_id, updates)
40
+ if result is None:
41
+ raise HTTPException(status_code=404, detail=f"Alias '{alias_id}' not found")
42
+ return result
src/eval_card_registry/api/routes_entities.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from typing import Optional
3
+
4
+ from fastapi import APIRouter, Depends, HTTPException
5
+
6
+ from eval_card_registry.api.schemas import (
7
+ BenchmarkCreate, BenchmarkPatch,
8
+ HarnessCreate, HarnessPatch,
9
+ MetricCreate, MetricPatch,
10
+ ModelCreate, ModelPatch,
11
+ ReviewStatus,
12
+ )
13
+ from eval_card_registry.config import settings
14
+ from eval_card_registry.store.hf_store import get_store, RegistryStore
15
+ from eval_card_registry.store import queries
16
+
17
+
18
+ def _check_writable():
19
+ if settings.read_only:
20
+ raise HTTPException(status_code=405, detail="Write operations disabled in read-only mode")
21
+
22
+ router = APIRouter()
23
+ _writable = [Depends(_check_writable)]
24
+
25
+
26
+ def _get_or_404(store, table, entity_id):
27
+ entity = queries.get_entity(store, table, entity_id)
28
+ if entity is None:
29
+ raise HTTPException(status_code=404, detail=f"{table} '{entity_id}' not found")
30
+ return entity
31
+
32
+
33
+ _JSON_FIELDS = {"tags", "metadata"}
34
+
35
+
36
+ def _encode(data: dict) -> dict:
37
+ """JSON-encode list/dict fields for parquet storage."""
38
+ out = {}
39
+ for k, v in data.items():
40
+ if k in _JSON_FIELDS and isinstance(v, (list, dict)):
41
+ out[k] = json.dumps(v)
42
+ else:
43
+ out[k] = v
44
+ return out
45
+
46
+
47
+ def _decode(entity: dict) -> dict:
48
+ """JSON-decode string fields that should be list/dict in API responses."""
49
+ out = {}
50
+ for k, v in entity.items():
51
+ if k in _JSON_FIELDS and isinstance(v, str):
52
+ try:
53
+ out[k] = json.loads(v)
54
+ except (json.JSONDecodeError, TypeError):
55
+ out[k] = v
56
+ else:
57
+ out[k] = v
58
+ return out
59
+
60
+
61
+ # ------------------------------------------------------------------
62
+ # Models
63
+ # ------------------------------------------------------------------
64
+
65
+ @router.get("/models")
66
+ def list_models(
67
+ search: Optional[str] = None,
68
+ developer: Optional[str] = None,
69
+ review_status: Optional[ReviewStatus] = None,
70
+ store: RegistryStore = Depends(get_store),
71
+ ):
72
+ return [_decode(e) for e in queries.list_entities(store, "canonical_models", search=search, review_status=review_status, developer=developer)]
73
+
74
+
75
+ @router.get("/models/{model_id:path}")
76
+ def get_model(model_id: str, store: RegistryStore = Depends(get_store)):
77
+ return _decode(_get_or_404(store, "canonical_models", model_id))
78
+
79
+
80
+ @router.post("/models", status_code=201, dependencies=_writable)
81
+ def create_model(body: ModelCreate, store: RegistryStore = Depends(get_store)):
82
+ return _decode(queries.upsert_entity(store, "canonical_models", _encode(body.model_dump())))
83
+
84
+
85
+ @router.patch("/models/{model_id:path}", dependencies=_writable)
86
+ def patch_model(model_id: str, body: ModelPatch, store: RegistryStore = Depends(get_store)):
87
+ _get_or_404(store, "canonical_models", model_id)
88
+ data = {k: v for k, v in body.model_dump().items() if v is not None}
89
+ data["id"] = model_id
90
+ return _decode(queries.upsert_entity(store, "canonical_models", _encode(data)))
91
+
92
+
93
+ # ------------------------------------------------------------------
94
+ # Benchmarks
95
+ # ------------------------------------------------------------------
96
+
97
+ @router.get("/benchmarks")
98
+ def list_benchmarks(
99
+ search: Optional[str] = None,
100
+ review_status: Optional[ReviewStatus] = None,
101
+ store: RegistryStore = Depends(get_store),
102
+ ):
103
+ return [_decode(e) for e in queries.list_entities(store, "canonical_benchmarks", search=search, review_status=review_status)]
104
+
105
+
106
+ @router.get("/benchmarks/{benchmark_id}")
107
+ def get_benchmark(benchmark_id: str, store: RegistryStore = Depends(get_store)):
108
+ return _decode(_get_or_404(store, "canonical_benchmarks", benchmark_id))
109
+
110
+
111
+ @router.post("/benchmarks", status_code=201, dependencies=_writable)
112
+ def create_benchmark(body: BenchmarkCreate, store: RegistryStore = Depends(get_store)):
113
+ return _decode(queries.upsert_entity(store, "canonical_benchmarks", _encode(body.model_dump())))
114
+
115
+
116
+ @router.patch("/benchmarks/{benchmark_id}", dependencies=_writable)
117
+ def patch_benchmark(benchmark_id: str, body: BenchmarkPatch, store: RegistryStore = Depends(get_store)):
118
+ _get_or_404(store, "canonical_benchmarks", benchmark_id)
119
+ data = {k: v for k, v in body.model_dump().items() if v is not None}
120
+ data["id"] = benchmark_id
121
+ return _decode(queries.upsert_entity(store, "canonical_benchmarks", _encode(data)))
122
+
123
+
124
+ # ------------------------------------------------------------------
125
+ # Metrics
126
+ # ------------------------------------------------------------------
127
+
128
+ @router.get("/metrics")
129
+ def list_metrics(
130
+ search: Optional[str] = None,
131
+ review_status: Optional[ReviewStatus] = None,
132
+ store: RegistryStore = Depends(get_store),
133
+ ):
134
+ return [_decode(e) for e in queries.list_entities(store, "canonical_metrics", search=search, review_status=review_status)]
135
+
136
+
137
+ @router.get("/metrics/{metric_id}")
138
+ def get_metric(metric_id: str, store: RegistryStore = Depends(get_store)):
139
+ return _decode(_get_or_404(store, "canonical_metrics", metric_id))
140
+
141
+
142
+ @router.post("/metrics", status_code=201, dependencies=_writable)
143
+ def create_metric(body: MetricCreate, store: RegistryStore = Depends(get_store)):
144
+ return _decode(queries.upsert_entity(store, "canonical_metrics", _encode(body.model_dump())))
145
+
146
+
147
+ @router.patch("/metrics/{metric_id}", dependencies=_writable)
148
+ def patch_metric(metric_id: str, body: MetricPatch, store: RegistryStore = Depends(get_store)):
149
+ _get_or_404(store, "canonical_metrics", metric_id)
150
+ data = {k: v for k, v in body.model_dump().items() if v is not None}
151
+ data["id"] = metric_id
152
+ return _decode(queries.upsert_entity(store, "canonical_metrics", _encode(data)))
153
+
154
+
155
+ # ------------------------------------------------------------------
156
+ # Harnesses
157
+ # ------------------------------------------------------------------
158
+
159
+ @router.get("/harnesses")
160
+ def list_harnesses(
161
+ search: Optional[str] = None,
162
+ review_status: Optional[ReviewStatus] = None,
163
+ store: RegistryStore = Depends(get_store),
164
+ ):
165
+ return [_decode(e) for e in queries.list_entities(store, "eval_harnesses", search=search, review_status=review_status)]
166
+
167
+
168
+ @router.get("/harnesses/{harness_id}")
169
+ def get_harness(harness_id: str, store: RegistryStore = Depends(get_store)):
170
+ return _decode(_get_or_404(store, "eval_harnesses", harness_id))
171
+
172
+
173
+ @router.post("/harnesses", status_code=201, dependencies=_writable)
174
+ def create_harness(body: HarnessCreate, store: RegistryStore = Depends(get_store)):
175
+ return _decode(queries.upsert_entity(store, "eval_harnesses", _encode(body.model_dump())))
176
+
177
+
178
+ @router.patch("/harnesses/{harness_id}", dependencies=_writable)
179
+ def patch_harness(harness_id: str, body: HarnessPatch, store: RegistryStore = Depends(get_store)):
180
+ _get_or_404(store, "eval_harnesses", harness_id)
181
+ data = {k: v for k, v in body.model_dump().items() if v is not None}
182
+ data["id"] = harness_id
183
+ return _decode(queries.upsert_entity(store, "eval_harnesses", _encode(data)))
src/eval_card_registry/api/routes_health.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends
2
+
3
+ from eval_card_registry.store.hf_store import get_store, RegistryStore
4
+
5
+ router = APIRouter()
6
+
7
+
8
+ @router.get("/health")
9
+ def health(store: RegistryStore = Depends(get_store)):
10
+ return {
11
+ "status": "ok",
12
+ "store": "loaded" if store.loaded else "not_loaded",
13
+ "entities": {
14
+ "models": len(store.table("canonical_models")) if store.has_table("canonical_models") else 0,
15
+ "benchmarks": len(store.table("canonical_benchmarks")) if store.has_table("canonical_benchmarks") else 0,
16
+ "metrics": len(store.table("canonical_metrics")) if store.has_table("canonical_metrics") else 0,
17
+ "harnesses": len(store.table("eval_harnesses")) if store.has_table("eval_harnesses") else 0,
18
+ },
19
+ }
20
+
21
+
22
+ @router.get("/stats")
23
+ def stats(store: RegistryStore = Depends(get_store)):
24
+ def _counts(table: str) -> dict:
25
+ if not store.has_table(table):
26
+ return {"total": 0, "draft": 0, "reviewed": 0}
27
+ df = store.table(table)
28
+ total = len(df)
29
+ draft = int((df["review_status"] == "draft").sum()) if "review_status" in df.columns else 0
30
+ return {"total": total, "draft": draft, "reviewed": total - draft}
31
+
32
+ if store.has_table("aliases"):
33
+ aliases_df = store.table("aliases")
34
+ uncertain = int((aliases_df["status"] == "uncertain").sum()) if "status" in aliases_df.columns else 0
35
+ aliases_stats = {"total": len(aliases_df), "uncertain": uncertain}
36
+ else:
37
+ aliases_stats = {"total": 0, "uncertain": 0}
38
+
39
+ return {
40
+ "models": _counts("canonical_models"),
41
+ "benchmarks": _counts("canonical_benchmarks"),
42
+ "metrics": _counts("canonical_metrics"),
43
+ "harnesses": _counts("eval_harnesses"),
44
+ "aliases": aliases_stats,
45
+ "resolution_log": {"total": len(store.table("resolution_log")) if store.has_table("resolution_log") else 0},
46
+ "sync_runs": {"total": len(store.table("sync_runs")) if store.has_table("sync_runs") else 0},
47
+ }
src/eval_card_registry/api/routes_resolve.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uuid
2
+
3
+ from fastapi import APIRouter, Depends, Request
4
+ from datetime import datetime, timezone
5
+
6
+ from eval_card_registry.api.schemas import ResolveRequest, ResolveResponse
7
+ from eval_card_registry.services.resolution_service import ResolutionService
8
+ from eval_card_registry.services.log_writer import ResolveLogWriter
9
+
10
+ router = APIRouter()
11
+
12
+
13
+ def _svc(request: Request) -> ResolutionService:
14
+ return request.app.state.resolution_service
15
+
16
+
17
+ def _log_writer(request: Request) -> ResolveLogWriter:
18
+ return request.app.state.log_writer
19
+
20
+
21
+ def _log_resolve(
22
+ log_writer: ResolveLogWriter,
23
+ request_id: str,
24
+ req: ResolveRequest,
25
+ result: dict,
26
+ ) -> None:
27
+ log_writer.append({
28
+ "request_id": request_id,
29
+ "raw_value": req.raw_value,
30
+ "entity_type": req.entity_type,
31
+ "source_config": req.source_config,
32
+ "canonical_id": result.get("canonical_id"),
33
+ "strategy": result.get("strategy"),
34
+ "confidence": result.get("confidence"),
35
+ "timestamp": datetime.now(timezone.utc).isoformat(),
36
+ })
37
+
38
+
39
+ @router.post("/resolve", response_model=ResolveResponse)
40
+ def resolve(
41
+ req: ResolveRequest,
42
+ svc: ResolutionService = Depends(_svc),
43
+ log_writer: ResolveLogWriter = Depends(_log_writer),
44
+ ):
45
+ result = svc.resolve(
46
+ raw_value=req.raw_value,
47
+ entity_type=req.entity_type,
48
+ source_config=req.source_config,
49
+ source_field=req.source_field,
50
+ )
51
+ _log_resolve(log_writer, str(uuid.uuid4()), req, result)
52
+ return ResolveResponse(**result)
53
+
54
+
55
+ @router.post("/resolve/batch", response_model=list[ResolveResponse])
56
+ def resolve_batch(
57
+ reqs: list[ResolveRequest],
58
+ svc: ResolutionService = Depends(_svc),
59
+ log_writer: ResolveLogWriter = Depends(_log_writer),
60
+ ):
61
+ request_id = str(uuid.uuid4())
62
+ responses = []
63
+ for r in reqs:
64
+ result = svc.resolve(
65
+ raw_value=r.raw_value,
66
+ entity_type=r.entity_type,
67
+ source_config=r.source_config,
68
+ source_field=r.source_field,
69
+ )
70
+ _log_resolve(log_writer, request_id, r, result)
71
+ responses.append(ResolveResponse(**result))
72
+ return responses
src/eval_card_registry/api/schemas.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, Literal, Optional
2
+ from pydantic import BaseModel
3
+
4
+
5
+ EntityType = Literal["benchmark", "model", "metric", "harness"]
6
+ ReviewStatus = Literal["draft", "reviewed"]
7
+ AliasStatus = Literal["auto", "uncertain", "confirmed", "rejected"]
8
+
9
+
10
+ # --- Resolve ---
11
+
12
+ class ResolveRequest(BaseModel):
13
+ raw_value: str
14
+ entity_type: EntityType
15
+ source_config: Optional[str] = None
16
+ source_field: Optional[str] = None
17
+
18
+
19
+ class ResolveResponse(BaseModel):
20
+ canonical_id: Optional[str]
21
+ strategy: str
22
+ confidence: float
23
+ created_new: bool
24
+ review_status: Optional[str]
25
+
26
+
27
+ # --- Entities ---
28
+
29
+ class ModelCreate(BaseModel):
30
+ id: str
31
+ display_name: str
32
+ developer: Optional[str] = None
33
+ family: Optional[str] = None
34
+ architecture: Optional[str] = None
35
+ params_billions: Optional[float] = None
36
+ tags: list[str] = []
37
+ metadata: dict[str, Any] = {}
38
+ review_status: str = "draft"
39
+
40
+
41
+ class ModelPatch(BaseModel):
42
+ display_name: Optional[str] = None
43
+ developer: Optional[str] = None
44
+ family: Optional[str] = None
45
+ architecture: Optional[str] = None
46
+ params_billions: Optional[float] = None
47
+ tags: Optional[list[str]] = None
48
+ metadata: Optional[dict[str, Any]] = None
49
+ review_status: Optional[str] = None
50
+
51
+
52
+ class BenchmarkCreate(BaseModel):
53
+ id: str
54
+ display_name: str
55
+ description: Optional[str] = None
56
+ dataset_repo: Optional[str] = None
57
+ parent_benchmark_id: Optional[str] = None
58
+ tags: list[str] = []
59
+ metadata: dict[str, Any] = {}
60
+ review_status: str = "draft"
61
+
62
+
63
+ class BenchmarkPatch(BaseModel):
64
+ display_name: Optional[str] = None
65
+ description: Optional[str] = None
66
+ dataset_repo: Optional[str] = None
67
+ parent_benchmark_id: Optional[str] = None
68
+ tags: Optional[list[str]] = None
69
+ metadata: Optional[dict[str, Any]] = None
70
+ review_status: Optional[str] = None
71
+
72
+
73
+ class MetricCreate(BaseModel):
74
+ id: str
75
+ display_name: str
76
+ score_type: Optional[str] = None
77
+ lower_is_better: bool = False
78
+ min_score: Optional[float] = None
79
+ max_score: Optional[float] = None
80
+ metadata: dict[str, Any] = {}
81
+ review_status: str = "draft"
82
+
83
+
84
+ class MetricPatch(BaseModel):
85
+ display_name: Optional[str] = None
86
+ score_type: Optional[str] = None
87
+ lower_is_better: Optional[bool] = None
88
+ min_score: Optional[float] = None
89
+ max_score: Optional[float] = None
90
+ metadata: Optional[dict[str, Any]] = None
91
+ review_status: Optional[str] = None
92
+
93
+
94
+ class HarnessCreate(BaseModel):
95
+ id: str
96
+ display_name: str
97
+ version: Optional[str] = None
98
+ fork_url: Optional[str] = None
99
+ metadata: dict[str, Any] = {}
100
+ review_status: str = "draft"
101
+
102
+
103
+ class HarnessPatch(BaseModel):
104
+ display_name: Optional[str] = None
105
+ version: Optional[str] = None
106
+ fork_url: Optional[str] = None
107
+ metadata: Optional[dict[str, Any]] = None
108
+ review_status: Optional[str] = None
109
+
110
+
111
+ # --- Aliases ---
112
+
113
+ class AliasPatch(BaseModel):
114
+ canonical_id: Optional[str] = None
115
+ status: Optional[str] = None
116
+ notes: Optional[str] = None
src/eval_card_registry/cli.py ADDED
@@ -0,0 +1,259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ eval-card-registry CLI.
3
+
4
+ Commands:
5
+ seed Load known entities from seed/ YAML files
6
+ stats Print registry summary
7
+ sync Batch sync one or all EEE configs → eval_results table
8
+ """
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ import typer
13
+ import yaml
14
+
15
+ from eval_card_registry.store.hf_store import get_store
16
+ from eval_card_registry.store import queries
17
+
18
+ app = typer.Typer(help="eval-card-registry CLI")
19
+
20
+
21
+ def _load_store():
22
+ store = get_store()
23
+ if not store.loaded:
24
+ store.load()
25
+ return store
26
+
27
+
28
+ # ------------------------------------------------------------------
29
+ # seed
30
+ # ------------------------------------------------------------------
31
+
32
+ @app.command()
33
+ def seed(
34
+ local: bool = typer.Option(False, "--local", help="Write to fixtures/ instead of HF Hub"),
35
+ seed_dir: str = typer.Option("./seed", "--seed-dir"),
36
+ ):
37
+ """Load known canonical entities from seed YAML files."""
38
+ import os
39
+ if local:
40
+ os.environ["LOCAL_MODE"] = "true"
41
+
42
+ store = _load_store()
43
+ seed_path = Path(seed_dir)
44
+
45
+ # table name, yaml file, label, entity_type (for alias creation)
46
+ seed_specs = [
47
+ ("canonical_benchmarks", seed_path / "benchmarks.yaml", "benchmarks", "benchmark"),
48
+ ("canonical_metrics", seed_path / "metrics.yaml", "metrics", "metric"),
49
+ ("eval_harnesses", seed_path / "harnesses.yaml", "harnesses", "harness"),
50
+ ]
51
+
52
+ alias_count = 0
53
+ # Track all seed entity IDs and alias keys so we can remove stale ones.
54
+ # Alias key: (raw_value, entity_type, canonical_id, source_config)
55
+ seed_snapshot: list[tuple[str, str, set[str], set[tuple[str, str, str, Optional[str]]]]] = []
56
+
57
+ for table, yaml_file, label, entity_type in seed_specs:
58
+ if not yaml_file.exists():
59
+ typer.echo(f" [skip] {yaml_file} not found")
60
+ continue
61
+ with open(yaml_file) as f:
62
+ items = yaml.safe_load(f) or []
63
+
64
+ yaml_ids: set[str] = set()
65
+ yaml_alias_keys: set[tuple[str, str, str, Optional[str]]] = set()
66
+
67
+ for item in items:
68
+ # Pop 'aliases' / 'scoped_aliases' before upserting — not table columns.
69
+ extra_aliases = item.pop("aliases", []) or []
70
+ scoped_aliases = item.pop("scoped_aliases", {}) or {}
71
+ queries.upsert_entity(store, table, item)
72
+ canonical_id = item["id"]
73
+ display_name = item.get("display_name", "")
74
+ yaml_ids.add(canonical_id)
75
+
76
+ # Global aliases (source_config=None): matched regardless of caller's source_config.
77
+ # Scoped aliases (source_config=<name>): matched only when the caller passes that
78
+ # source_config — lets short tokens ("Overall", "Arabic") map to different
79
+ # benchmarks depending on which EEE config they came from.
80
+ global_aliases = {canonical_id, display_name} | set(extra_aliases)
81
+
82
+ alias_specs: list[tuple[str, Optional[str]]] = [
83
+ (raw, None) for raw in global_aliases if raw
84
+ ]
85
+ for source_cfg, raw_values in scoped_aliases.items():
86
+ for raw in raw_values or []:
87
+ if raw:
88
+ alias_specs.append((raw, source_cfg))
89
+
90
+ for raw_value, source_cfg in alias_specs:
91
+ # Index stale-removal by (raw_value, entity_type, canonical_id, source_config)
92
+ yaml_alias_keys.add((raw_value, entity_type, canonical_id, source_cfg))
93
+ try:
94
+ queries.add_alias(store, {
95
+ "raw_value": raw_value,
96
+ "entity_type": entity_type,
97
+ "canonical_id": canonical_id,
98
+ "source_config": source_cfg,
99
+ "source_field": "seed",
100
+ "status": "confirmed",
101
+ "strategy": "seed",
102
+ "confidence": 1.0,
103
+ "notes": None,
104
+ })
105
+ alias_count += 1
106
+ except ValueError:
107
+ pass # alias already exists (e.g. re-seeding)
108
+
109
+ seed_snapshot.append((table, entity_type, yaml_ids, yaml_alias_keys))
110
+ typer.echo(f" {label}: {len(items)}")
111
+
112
+ # Remove seed-originated entities and aliases that are no longer in the YAML.
113
+ # Only touches rows that were created by seed (strategy == "seed"), never
114
+ # sync-created aliases or auto-draft entities.
115
+ removed_entities = 0
116
+ removed_aliases = 0
117
+ for table, entity_type, yaml_ids, yaml_alias_keys in seed_snapshot:
118
+ # Remove stale seed aliases for this entity type
119
+ aliases_df = store.table("aliases")
120
+ seed_mask = (aliases_df["strategy"] == "seed") & (aliases_df["entity_type"] == entity_type)
121
+ if seed_mask.any():
122
+ seed_aliases = aliases_df[seed_mask]
123
+ stale_alias_mask = seed_mask.copy()
124
+ for idx in seed_aliases.index:
125
+ row = seed_aliases.loc[idx]
126
+ sc = row.get("source_config")
127
+ # pandas NA → None so comparison matches how aliases were added.
128
+ if sc is None or (isinstance(sc, float) and sc != sc) or (hasattr(sc, "__class__") and sc.__class__.__name__ == "NAType"):
129
+ sc = None
130
+ key = (row["raw_value"], row["entity_type"], row["canonical_id"], sc)
131
+ if key in yaml_alias_keys:
132
+ stale_alias_mask[idx] = False
133
+ n_stale = stale_alias_mask.sum()
134
+ if n_stale > 0:
135
+ store.set_table("aliases", aliases_df[~stale_alias_mask].reset_index(drop=True))
136
+ removed_aliases += int(n_stale)
137
+
138
+ # Remove stale seed entities — only those with review_status "reviewed"
139
+ # that came from seed and are no longer in the YAML.
140
+ entity_df = store.table(table)
141
+ if len(entity_df) > 0:
142
+ stale = entity_df["id"].isin(yaml_ids)
143
+ stale_entities = entity_df[~stale & (entity_df["review_status"] == "reviewed")]
144
+ # Only remove if every alias for this entity is also seed-originated,
145
+ # meaning it wasn't referenced by sync data.
146
+ current_aliases = store.table("aliases")
147
+ for eid in stale_entities["id"]:
148
+ entity_aliases = current_aliases[
149
+ (current_aliases["canonical_id"] == eid)
150
+ & (current_aliases["entity_type"] == entity_type)
151
+ ]
152
+ if len(entity_aliases) == 0 or (entity_aliases["strategy"] == "seed").all():
153
+ entity_df = entity_df[entity_df["id"] != eid]
154
+ # Also remove any remaining aliases pointing to it
155
+ current_aliases = current_aliases[
156
+ ~((current_aliases["canonical_id"] == eid)
157
+ & (current_aliases["entity_type"] == entity_type))
158
+ ]
159
+ removed_entities += 1
160
+ store.set_table(table, entity_df.reset_index(drop=True))
161
+ store.set_table("aliases", current_aliases.reset_index(drop=True))
162
+
163
+ typer.echo(f" aliases: {alias_count} added, {removed_aliases} removed")
164
+ if removed_entities:
165
+ typer.echo(f" stale entities removed: {removed_entities}")
166
+
167
+ store.push_to_hub()
168
+ typer.echo("Seed complete.")
169
+
170
+
171
+ # ------------------------------------------------------------------
172
+ # stats
173
+ # ------------------------------------------------------------------
174
+
175
+ @app.command()
176
+ def stats(
177
+ local: bool = typer.Option(False, "--local", help="Read from fixtures/ instead of HF Hub"),
178
+ ):
179
+ """Print registry entity counts and pending review summary."""
180
+ import os
181
+ if local:
182
+ os.environ["LOCAL_MODE"] = "true"
183
+
184
+ store = _load_store()
185
+
186
+ def _row(table):
187
+ df = store.table(table)
188
+ total = len(df)
189
+ draft = int((df["review_status"] == "draft").sum()) if "review_status" in df.columns else 0
190
+ return total, draft
191
+
192
+ for label, table in [
193
+ ("models ", "canonical_models"),
194
+ ("benchmarks", "canonical_benchmarks"),
195
+ ("metrics ", "canonical_metrics"),
196
+ ("harnesses ", "eval_harnesses"),
197
+ ]:
198
+ total, draft = _row(table)
199
+ typer.echo(f" {label} total={total} draft={draft}")
200
+
201
+ aliases_df = store.table("aliases")
202
+ uncertain = int((aliases_df["status"] == "uncertain").sum()) if "status" in aliases_df.columns else 0
203
+ typer.echo(f"\n aliases total={len(aliases_df)} uncertain={uncertain}")
204
+ typer.echo(f" eval_results total={len(store.table('eval_results'))}")
205
+ typer.echo(f" resolution_log total={len(store.table('resolution_log'))}")
206
+ typer.echo(f" sync_runs total={len(store.table('sync_runs'))}")
207
+
208
+
209
+ # ------------------------------------------------------------------
210
+ # sync
211
+ # ------------------------------------------------------------------
212
+
213
+ @app.command()
214
+ def sync(
215
+ config: Optional[str] = typer.Option(None, "--config", help="EEE config name"),
216
+ all_configs: bool = typer.Option(False, "--all", help="Sync all EEE configs"),
217
+ rerun: bool = typer.Option(False, "--rerun", help="Re-resolve all raw strings even if already aliased"),
218
+ local: bool = typer.Option(False, "--local"),
219
+ ):
220
+ """
221
+ Batch sync EEE config(s) → writes resolved results to eval_results table.
222
+ Each result row is one (model × benchmark × metric) combination with resolved canonical IDs.
223
+ """
224
+ import os
225
+ if local:
226
+ os.environ["LOCAL_MODE"] = "true"
227
+
228
+ if not config and not all_configs:
229
+ typer.echo("Specify --config <name> or --all", err=True)
230
+ raise typer.Exit(1)
231
+
232
+ from eval_card_registry.services.ingestion import run_sync
233
+ import datasets as ds_lib
234
+
235
+ store = _load_store()
236
+
237
+ configs_to_run: list[str] = []
238
+ if all_configs:
239
+ configs_to_run = ds_lib.get_dataset_config_names("evaleval/EEE_datastore")
240
+ else:
241
+ configs_to_run = [config]
242
+
243
+ failed = []
244
+ for cfg in configs_to_run:
245
+ typer.echo(f"Syncing {cfg}...")
246
+ try:
247
+ counts = run_sync(cfg, store, rerun=rerun)
248
+ typer.echo(f" {cfg}: {counts}")
249
+ except Exception as e:
250
+ typer.echo(f" {cfg}: FAILED — {e}", err=True)
251
+ failed.append(cfg)
252
+
253
+ typer.echo("Persisting tables...")
254
+ store.push_to_hub()
255
+
256
+ if failed:
257
+ typer.echo(f"Done with {len(failed)} failed config(s): {', '.join(failed)}")
258
+ else:
259
+ typer.echo("Done.")
src/eval_card_registry/config.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ from pydantic_settings import BaseSettings, SettingsConfigDict
4
+
5
+
6
+ class Settings(BaseSettings):
7
+ model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
8
+
9
+ local_mode: bool = False
10
+ fixtures_path: str = "./fixtures"
11
+ hf_dataset_repo: str = ""
12
+ hf_token: str = ""
13
+ resolver_auto_merge_threshold: float = 0.85
14
+ read_only: bool = False
15
+ hf_log_bucket: str = ""
16
+ log_flush_interval_seconds: int = 300
17
+
18
+
19
+ settings = Settings()
20
+
21
+ # Export HF_TOKEN to the environment so that libraries that read it directly
22
+ # (e.g. `datasets.load_dataset`) pick it up, not just code that uses
23
+ # `settings.hf_token`.
24
+ if settings.hf_token and not os.environ.get("HF_TOKEN"):
25
+ os.environ["HF_TOKEN"] = settings.hf_token
src/eval_card_registry/main.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from contextlib import asynccontextmanager
2
+
3
+ from fastapi import FastAPI
4
+
5
+ from eval_card_registry.config import settings
6
+ from eval_card_registry.store.hf_store import get_store, _QUERY_TABLE_NAMES
7
+ from eval_card_registry.services.resolution_service import ResolutionService
8
+ from eval_card_registry.services.log_writer import ResolveLogWriter
9
+ from eval_card_registry.api.routes_resolve import router as resolve_router
10
+ from eval_card_registry.api.routes_entities import router as entities_router
11
+ from eval_card_registry.api.routes_aliases import router as aliases_router
12
+ from eval_card_registry.api.routes_health import router as health_router
13
+
14
+
15
+ @asynccontextmanager
16
+ async def lifespan(app: FastAPI):
17
+ store = get_store()
18
+ if settings.read_only:
19
+ store.load(tables=_QUERY_TABLE_NAMES)
20
+ else:
21
+ store.load()
22
+
23
+ # Singleton ResolutionService — avoids rebuilding AliasStore per request
24
+ app.state.resolution_service = ResolutionService(store)
25
+
26
+ # Resolve log writer
27
+ log_writer = ResolveLogWriter(settings.hf_log_bucket)
28
+ app.state.log_writer = log_writer
29
+ log_writer.start(settings.log_flush_interval_seconds)
30
+
31
+ yield
32
+
33
+ await log_writer.stop()
34
+
35
+
36
+ app = FastAPI(
37
+ title="eval-card-registry",
38
+ description="Entity resolution registry for EEE evaluation data.",
39
+ version="0.1.0",
40
+ lifespan=lifespan,
41
+ )
42
+
43
+ PREFIX = "/api/v1"
44
+
45
+ app.include_router(resolve_router, prefix=PREFIX)
46
+ app.include_router(entities_router, prefix=PREFIX)
47
+ app.include_router(aliases_router, prefix=PREFIX)
48
+ app.include_router(health_router, prefix=PREFIX)
src/eval_card_registry/services/__init__.py ADDED
File without changes
src/eval_card_registry/services/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (182 Bytes). View file
 
src/eval_card_registry/services/__pycache__/ingestion.cpython-314.pyc ADDED
Binary file (12.8 kB). View file
 
src/eval_card_registry/services/__pycache__/log_writer.cpython-314.pyc ADDED
Binary file (7.82 kB). View file
 
src/eval_card_registry/services/__pycache__/resolution_service.cpython-314.pyc ADDED
Binary file (11.4 kB). View file
 
src/eval_card_registry/services/ingestion.py ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ EEE sync pipeline.
3
+
4
+ Per-record processing → sub-category detection → resolution → eval_results table.
5
+ Pushes HF Hub at end of run.
6
+
7
+ Known data-quality issues
8
+ ---------------------------------------------------
9
+ Some configs use a metric keyword as ``evaluation_name`` for aggregate /
10
+ summary rows instead of a real benchmark name. These create problematic benchmark
11
+ entities (``score``, ``overall``, ``mean-score``, ``mean-win-rate``).
12
+
13
+ * ``reward-bench``: ``"Score"`` — composite metric across rewardbench
14
+ subcategories (chat, chat-hard, safety, reasoning).
15
+ * ``ace``: ``"Overall"`` / ``"overall"`` — rollup across ACE/APEX
16
+ sub-evaluations.
17
+ * ``helm_capabilities``: ``"Mean score"`` — mean across all HELM capability
18
+ benchmarks for a model.
19
+ * ``helm_instruct``: ``"Mean win rate"`` — mean win rate across all HELM
20
+ instruct benchmarks.
21
+
22
+ Fix should happen upstream as part of wider discussion on schema design.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import json
27
+ import sys
28
+ from collections import defaultdict
29
+ from typing import Any, Iterator, Optional
30
+
31
+ from eval_entity_resolver.eee import clean_eval_name, extract_metric
32
+
33
+ from eval_card_registry.config import settings
34
+ from eval_card_registry.store.hf_store import RegistryStore
35
+ from eval_card_registry.store import queries
36
+ from eval_card_registry.services.resolution_service import ResolutionService
37
+
38
+
39
+ def _iter_eee_config(source_config: str) -> Iterator[dict]:
40
+ """Load EEE datastore config and yield rows as dicts."""
41
+ import datasets
42
+
43
+ ds = datasets.load_dataset("evaleval/EEE_datastore", source_config)["train"]
44
+ for i in range(len(ds)):
45
+ yield dict(ds[i])
46
+
47
+
48
+ def _detect_sub_categories(
49
+ evaluation_results: list[dict],
50
+ ) -> dict[str, Optional[str]]:
51
+ """
52
+ Inspect all evaluation_results in a record.
53
+ Returns {evaluation_name: parent_dataset_name or None}.
54
+
55
+ If multiple evaluation_name values share the same dataset_name → sub-category pattern.
56
+ The dataset_name becomes the parent benchmark; each evaluation_name is a child.
57
+ """
58
+ dataset_to_eval_names: dict[str, list[str]] = defaultdict(list)
59
+ for result in evaluation_results:
60
+ eval_name = result.get("evaluation_name", "")
61
+ dataset_name = (result.get("source_data") or {}).get("dataset_name", "")
62
+ if eval_name and dataset_name:
63
+ dataset_to_eval_names[dataset_name].append(eval_name)
64
+
65
+ # Build mapping: eval_name → parent dataset_name (if sub-category) or None
66
+ eval_to_parent: dict[str, Optional[str]] = {}
67
+ for result in evaluation_results:
68
+ eval_name = result.get("evaluation_name", "")
69
+ dataset_name = (result.get("source_data") or {}).get("dataset_name", "")
70
+ if not eval_name:
71
+ continue
72
+ if dataset_name and len(dataset_to_eval_names.get(dataset_name, [])) > 1:
73
+ eval_to_parent[eval_name] = dataset_name
74
+ else:
75
+ eval_to_parent[eval_name] = None
76
+ return eval_to_parent
77
+
78
+
79
+ def process_record(
80
+ record: dict,
81
+ source_config: str,
82
+ svc: ResolutionService,
83
+ sync_run_id: str,
84
+ rerun: bool,
85
+ ) -> Optional[list[dict]]:
86
+ """
87
+ Resolve all entities in one EEE record.
88
+ Returns a flat list of result rows (one per evaluation result) or None if
89
+ the record has no model info.
90
+ """
91
+ # --- Model ---
92
+ model_info = record.get("model_info") or {}
93
+ model_raw = model_info.get("id") or model_info.get("model_name") or model_info.get("name")
94
+ if not model_raw:
95
+ return None
96
+
97
+ model_res = svc.resolve(
98
+ raw_value=model_raw,
99
+ entity_type="model",
100
+ source_config=source_config,
101
+ source_field="model_info.id",
102
+ sync_run_id=sync_run_id,
103
+ rerun=rerun,
104
+ )
105
+
106
+ # --- Harness ---
107
+ eval_lib = record.get("eval_library") or {}
108
+ harness_raw = eval_lib.get("name") or eval_lib.get("library_name")
109
+ harness_res = None
110
+ if harness_raw:
111
+ harness_res = svc.resolve(
112
+ raw_value=harness_raw,
113
+ entity_type="harness",
114
+ source_config=source_config,
115
+ source_field="eval_library.name",
116
+ sync_run_id=sync_run_id,
117
+ rerun=rerun,
118
+ )
119
+
120
+ # --- Benchmarks & Metrics ---
121
+ evaluation_results = record.get("evaluation_results") or []
122
+ if not isinstance(evaluation_results, list):
123
+ evaluation_results = []
124
+
125
+ eval_to_parent = _detect_sub_categories(evaluation_results)
126
+
127
+ # Resolve parent benchmarks once
128
+ parent_cache: dict[str, str] = {}
129
+ for dataset_name in set(v for v in eval_to_parent.values() if v is not None):
130
+ parent_res = svc.resolve(
131
+ raw_value=dataset_name,
132
+ entity_type="benchmark",
133
+ source_config=source_config,
134
+ source_field="source_data.dataset_name",
135
+ sync_run_id=sync_run_id,
136
+ rerun=rerun,
137
+ )
138
+ parent_cache[dataset_name] = parent_res["canonical_id"]
139
+
140
+ # Build evaluation_id — must be deterministic for stable eval_results row keys.
141
+ # The EEE schema requires evaluation_id, but we fall back to a hash of
142
+ # model + source_config if missing, to avoid non-deterministic id(record).
143
+ source_meta = record.get("source_metadata") or {}
144
+ eval_id = record.get("evaluation_id") or source_meta.get("evaluation_id") or source_meta.get("id")
145
+ if not eval_id:
146
+ import hashlib
147
+ fallback_key = f"{source_config}:{model_raw}"
148
+ eval_id = f"{source_config}/auto-{hashlib.sha256(fallback_key.encode()).hexdigest()[:12]}"
149
+
150
+ result_rows = []
151
+ for idx, er in enumerate(evaluation_results):
152
+ eval_name = er.get("evaluation_name")
153
+ if not eval_name:
154
+ continue
155
+
156
+ parent_dataset = eval_to_parent.get(eval_name)
157
+ parent_benchmark_id = parent_cache.get(parent_dataset) if parent_dataset else None
158
+
159
+ bench_name = clean_eval_name(eval_name)
160
+ bench_res = svc.resolve(
161
+ raw_value=bench_name,
162
+ entity_type="benchmark",
163
+ source_config=source_config,
164
+ source_field="evaluation_results[].evaluation_name",
165
+ sync_run_id=sync_run_id,
166
+ rerun=rerun,
167
+ )
168
+
169
+ # Metric — try metric_name first (human-readable, e.g. "Win Rate"),
170
+ # then metric_id (may be dot-notation like "bfcl.live.accuracy"),
171
+ # then evaluation_description (verbose, e.g. "Accuracy on IFEval").
172
+ # extract_metric normalises all three forms to a reusable metric name.
173
+ metric_config = er.get("metric_config") or {}
174
+ metric_raw = extract_metric(
175
+ metric_config.get("metric_name")
176
+ or metric_config.get("metric_id")
177
+ or metric_config.get("evaluation_description")
178
+ or ""
179
+ )
180
+ metric_res = None
181
+ if metric_raw:
182
+ metric_res = svc.resolve(
183
+ raw_value=metric_raw,
184
+ entity_type="metric",
185
+ source_config=source_config,
186
+ source_field="metric_config",
187
+ sync_run_id=sync_run_id,
188
+ rerun=rerun,
189
+ )
190
+
191
+ # Score — use `is not None` checks to preserve valid 0 / 0.0 scores.
192
+ score_details_raw = er.get("score_details") or er.get("details") or {}
193
+ score = None
194
+ if isinstance(score_details_raw, dict):
195
+ score = score_details_raw.get("score")
196
+ if score is None:
197
+ for key in ("score", "value", "result"):
198
+ val = er.get(key)
199
+ if val is not None:
200
+ score = val
201
+ break
202
+
203
+ result_rows.append(
204
+ {
205
+ "evaluation_id": eval_id,
206
+ "result_index": idx,
207
+ "source_config": source_config,
208
+ "model_id": model_res["canonical_id"],
209
+ "harness_id": harness_res["canonical_id"] if harness_res else None,
210
+ "benchmark_id": bench_res["canonical_id"],
211
+ "parent_benchmark_id": parent_benchmark_id,
212
+ "metric_id": metric_res["canonical_id"] if metric_res else None,
213
+ "benchmark_card_id": None,
214
+ "score": score,
215
+ "score_details": json.dumps(score_details_raw) if score_details_raw else None,
216
+ }
217
+ )
218
+
219
+ return result_rows
220
+
221
+
222
+ def run_sync(
223
+ source_config: str,
224
+ registry_store: RegistryStore,
225
+ rerun: bool = False,
226
+ ) -> dict:
227
+ """
228
+ Sync one EEE config. Returns counts dict.
229
+ Does NOT push to HF Hub — caller is responsible for calling push_to_hub()
230
+ once after all configs are done.
231
+ """
232
+ svc = ResolutionService(registry_store)
233
+
234
+ # Reset module-level caches from any prior (possibly crashed) sync
235
+ queries._alias_index.clear()
236
+ queries._pending_result_ids.clear()
237
+
238
+ # Build alias index for fast lookups during sync
239
+ queries._rebuild_alias_index(registry_store)
240
+
241
+ run_id = queries.start_sync_run(registry_store, source_config, rerun)
242
+
243
+ counts = {
244
+ "entities_created": 0,
245
+ "entities_updated": 0,
246
+ "aliases_created": 0,
247
+ "aliases_updated": 0,
248
+ }
249
+ errors = []
250
+
251
+ # Snapshot table lengths before sync to count actual changes
252
+ aliases_before = len(registry_store.table("aliases")) + len(queries._get_pending(registry_store, "aliases"))
253
+ models_before = len(registry_store.table("canonical_models"))
254
+ benchmarks_before = len(registry_store.table("canonical_benchmarks"))
255
+ metrics_before = len(registry_store.table("canonical_metrics"))
256
+ harnesses_before = len(registry_store.table("eval_harnesses"))
257
+
258
+ record_count = 0
259
+ try:
260
+ for record in _iter_eee_config(source_config):
261
+ try:
262
+ result_rows = process_record(record, source_config, svc, run_id, rerun)
263
+ if result_rows:
264
+ for row in result_rows:
265
+ queries.upsert_eval_result(registry_store, row)
266
+ except Exception as e:
267
+ errors.append(str(e))
268
+
269
+ record_count += 1
270
+ if record_count % 500 == 0:
271
+ print(f" [{source_config}] {record_count} records processed...", file=sys.stderr)
272
+ finally:
273
+ # Always flush pending rows — even on crash, preserve successfully
274
+ # processed records rather than silently losing them.
275
+ queries.flush_pending(registry_store)
276
+
277
+ # Reset module-level caches for next sync run
278
+ queries._alias_index.clear()
279
+ queries._pending_result_ids.clear()
280
+
281
+ print(f" [{source_config}] {record_count} records total, flushed.", file=sys.stderr)
282
+
283
+ # Count changes by comparing table lengths
284
+ entities_after = (
285
+ len(registry_store.table("canonical_models"))
286
+ + len(registry_store.table("canonical_benchmarks"))
287
+ + len(registry_store.table("canonical_metrics"))
288
+ + len(registry_store.table("eval_harnesses"))
289
+ )
290
+ entities_before = models_before + benchmarks_before + metrics_before + harnesses_before
291
+ aliases_after = len(registry_store.table("aliases"))
292
+
293
+ counts["entities_created"] = max(0, entities_after - entities_before)
294
+ counts["aliases_created"] = max(0, aliases_after - aliases_before) if not rerun else 0
295
+ counts["aliases_updated"] = max(0, aliases_after - aliases_before) if rerun else 0
296
+
297
+ queries.finish_sync_run(registry_store, run_id, counts, errors)
298
+
299
+ return counts