Spaces:
Running
Running
Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- Dockerfile +30 -0
- README.md +82 -6
- packages/eval-entity-resolver/pyproject.toml +16 -0
- packages/eval-entity-resolver/src/eval_entity_resolver/__init__.py +13 -0
- packages/eval-entity-resolver/src/eval_entity_resolver/__pycache__/__init__.cpython-314.pyc +0 -0
- packages/eval-entity-resolver/src/eval_entity_resolver/__pycache__/alias_store.cpython-314.pyc +0 -0
- packages/eval-entity-resolver/src/eval_entity_resolver/__pycache__/eee.cpython-314.pyc +0 -0
- packages/eval-entity-resolver/src/eval_entity_resolver/__pycache__/models.cpython-314.pyc +0 -0
- packages/eval-entity-resolver/src/eval_entity_resolver/__pycache__/normalization.cpython-314.pyc +0 -0
- packages/eval-entity-resolver/src/eval_entity_resolver/__pycache__/resolver.cpython-314.pyc +0 -0
- packages/eval-entity-resolver/src/eval_entity_resolver/alias_store.py +230 -0
- packages/eval-entity-resolver/src/eval_entity_resolver/eee.py +198 -0
- packages/eval-entity-resolver/src/eval_entity_resolver/models.py +21 -0
- packages/eval-entity-resolver/src/eval_entity_resolver/normalization.py +23 -0
- packages/eval-entity-resolver/src/eval_entity_resolver/resolver.py +69 -0
- packages/eval-entity-resolver/src/eval_entity_resolver/strategies/__init__.py +0 -0
- packages/eval-entity-resolver/src/eval_entity_resolver/strategies/__pycache__/__init__.cpython-314.pyc +0 -0
- packages/eval-entity-resolver/src/eval_entity_resolver/strategies/__pycache__/exact.cpython-314.pyc +0 -0
- packages/eval-entity-resolver/src/eval_entity_resolver/strategies/__pycache__/fuzzy.cpython-314.pyc +0 -0
- packages/eval-entity-resolver/src/eval_entity_resolver/strategies/__pycache__/normalized.cpython-314.pyc +0 -0
- packages/eval-entity-resolver/src/eval_entity_resolver/strategies/exact.py +11 -0
- packages/eval-entity-resolver/src/eval_entity_resolver/strategies/fuzzy.py +148 -0
- packages/eval-entity-resolver/src/eval_entity_resolver/strategies/normalized.py +20 -0
- pyproject.toml +44 -0
- src/eval_card_registry/__init__.py +0 -0
- src/eval_card_registry/__pycache__/__init__.cpython-314.pyc +0 -0
- src/eval_card_registry/__pycache__/cli.cpython-314.pyc +0 -0
- src/eval_card_registry/__pycache__/config.cpython-314.pyc +0 -0
- src/eval_card_registry/__pycache__/main.cpython-314.pyc +0 -0
- src/eval_card_registry/api/__init__.py +0 -0
- src/eval_card_registry/api/__pycache__/__init__.cpython-314.pyc +0 -0
- src/eval_card_registry/api/__pycache__/routes_aliases.cpython-314.pyc +0 -0
- src/eval_card_registry/api/__pycache__/routes_entities.cpython-314.pyc +0 -0
- src/eval_card_registry/api/__pycache__/routes_health.cpython-314.pyc +0 -0
- src/eval_card_registry/api/__pycache__/routes_resolve.cpython-314.pyc +0 -0
- src/eval_card_registry/api/__pycache__/schemas.cpython-314.pyc +0 -0
- src/eval_card_registry/api/routes_aliases.py +42 -0
- src/eval_card_registry/api/routes_entities.py +183 -0
- src/eval_card_registry/api/routes_health.py +47 -0
- src/eval_card_registry/api/routes_resolve.py +72 -0
- src/eval_card_registry/api/schemas.py +116 -0
- src/eval_card_registry/cli.py +259 -0
- src/eval_card_registry/config.py +25 -0
- src/eval_card_registry/main.py +48 -0
- src/eval_card_registry/services/__init__.py +0 -0
- src/eval_card_registry/services/__pycache__/__init__.cpython-314.pyc +0 -0
- src/eval_card_registry/services/__pycache__/ingestion.cpython-314.pyc +0 -0
- src/eval_card_registry/services/__pycache__/log_writer.cpython-314.pyc +0 -0
- src/eval_card_registry/services/__pycache__/resolution_service.cpython-314.pyc +0 -0
- 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:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
|
|
|
| 7 |
pinned: false
|
| 8 |
-
short_description: WIP registry to help resolve benchmarks, models, and metrics
|
| 9 |
---
|
| 10 |
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|