evalsi 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- evalsi/__init__.py +77 -0
- evalsi/__main__.py +7 -0
- evalsi/_version.py +1 -0
- evalsi/analytics.py +325 -0
- evalsi/auth.py +458 -0
- evalsi/cli.py +745 -0
- evalsi/cli_analytics.py +105 -0
- evalsi/cli_annotate.py +346 -0
- evalsi/cli_auth.py +422 -0
- evalsi/cli_credentials.py +53 -0
- evalsi/cli_flywheel.py +125 -0
- evalsi/cli_guardrails.py +143 -0
- evalsi/cli_mcp.py +64 -0
- evalsi/cli_plugins.py +141 -0
- evalsi/cli_rewards.py +131 -0
- evalsi/cli_sources.py +225 -0
- evalsi/cli_training.py +197 -0
- evalsi/client.py +719 -0
- evalsi/compare.py +100 -0
- evalsi/convert.py +476 -0
- evalsi/datasets.py +216 -0
- evalsi/evaluator.py +253 -0
- evalsi/harness/__init__.py +1 -0
- evalsi/harness/v1alpha1/__init__.py +1 -0
- evalsi/harness/v1alpha1/harness_pb2.py +69 -0
- evalsi/harness/v1alpha1/harness_pb2.pyi +366 -0
- evalsi/harness/v1alpha1/harness_pb2_grpc.py +285 -0
- evalsi/harness/v1alpha1/harness_pb2_grpc.pyi +115 -0
- evalsi/judges/__init__.py +215 -0
- evalsi/judges/anthropic.py +85 -0
- evalsi/judges/cache.py +45 -0
- evalsi/judges/openai_compatible.py +101 -0
- evalsi/judges/structured.py +99 -0
- evalsi/logging.py +323 -0
- evalsi/mcp_server.py +731 -0
- evalsi/packs/__init__.py +1 -0
- evalsi/packs/agent.py +535 -0
- evalsi/packs/code.py +118 -0
- evalsi/packs/core.py +278 -0
- evalsi/packs/finetune.py +365 -0
- evalsi/packs/judge.py +143 -0
- evalsi/packs/ml_classic.py +522 -0
- evalsi/packs/ml_monitoring.py +467 -0
- evalsi/packs/rag.py +234 -0
- evalsi/packs/rl.py +741 -0
- evalsi/packs/safety.py +162 -0
- evalsi/packs/text.py +205 -0
- evalsi/plugin/__init__.py +1 -0
- evalsi/plugin/v1alpha1/__init__.py +1 -0
- evalsi/plugin/v1alpha1/evaluator_plugin_pb2.py +70 -0
- evalsi/plugin/v1alpha1/evaluator_plugin_pb2.pyi +358 -0
- evalsi/plugin/v1alpha1/evaluator_plugin_pb2_grpc.py +346 -0
- evalsi/plugin/v1alpha1/evaluator_plugin_pb2_grpc.pyi +160 -0
- evalsi/plugins.py +258 -0
- evalsi/py.typed +0 -0
- evalsi/registry.py +150 -0
- evalsi/report.css +21 -0
- evalsi/report.py +346 -0
- evalsi/results.py +331 -0
- evalsi/rewards/__init__.py +597 -0
- evalsi/rewards/openrlhf.py +114 -0
- evalsi/rewards/spec.py +254 -0
- evalsi/rewards/trl.py +76 -0
- evalsi/rewards/verl.py +74 -0
- evalsi/run.py +392 -0
- evalsi/runner.py +284 -0
- evalsi/runspec.py +226 -0
- evalsi/sandbox/__init__.py +211 -0
- evalsi/sandbox/client.py +411 -0
- evalsi/sandbox/v1alpha1/__init__.py +1 -0
- evalsi/sandbox/v1alpha1/sandbox_pb2.py +109 -0
- evalsi/sandbox/v1alpha1/sandbox_pb2.pyi +772 -0
- evalsi/sandbox/v1alpha1/sandbox_pb2_grpc.py +515 -0
- evalsi/sandbox/v1alpha1/sandbox_pb2_grpc.pyi +175 -0
- evalsi/stats.py +209 -0
- evalsi/targets.py +267 -0
- evalsi/testing.py +59 -0
- evalsi/training/__init__.py +39 -0
- evalsi/training/callback.py +205 -0
- evalsi/training/checkpoints.py +197 -0
- evalsi/training/loop.py +416 -0
- evalsi/training/serving.py +152 -0
- evalsi/types.py +453 -0
- evalsi/v1alpha1/__init__.py +1 -0
- evalsi/v1alpha1/agent_pb2.py +116 -0
- evalsi/v1alpha1/agent_pb2.pyi +983 -0
- evalsi/v1alpha1/annotation_service_pb2.py +112 -0
- evalsi/v1alpha1/annotation_service_pb2.pyi +868 -0
- evalsi/v1alpha1/annotation_service_pb2_grpc.py +457 -0
- evalsi/v1alpha1/annotation_service_pb2_grpc.pyi +154 -0
- evalsi/v1alpha1/auth_service_pb2.py +136 -0
- evalsi/v1alpha1/auth_service_pb2.pyi +1004 -0
- evalsi/v1alpha1/auth_service_pb2_grpc.py +719 -0
- evalsi/v1alpha1/auth_service_pb2_grpc.pyi +220 -0
- evalsi/v1alpha1/catalog_service_pb2.py +47 -0
- evalsi/v1alpha1/catalog_service_pb2.pyi +132 -0
- evalsi/v1alpha1/catalog_service_pb2_grpc.py +145 -0
- evalsi/v1alpha1/catalog_service_pb2_grpc.pyi +73 -0
- evalsi/v1alpha1/dataset_service_pb2.py +46 -0
- evalsi/v1alpha1/dataset_service_pb2.pyi +130 -0
- evalsi/v1alpha1/dataset_service_pb2_grpc.py +140 -0
- evalsi/v1alpha1/dataset_service_pb2_grpc.pyi +58 -0
- evalsi/v1alpha1/evaluation_service_pb2.py +55 -0
- evalsi/v1alpha1/evaluation_service_pb2.pyi +219 -0
- evalsi/v1alpha1/evaluation_service_pb2_grpc.py +149 -0
- evalsi/v1alpha1/evaluation_service_pb2_grpc.pyi +82 -0
- evalsi/v1alpha1/evaluator_pb2.py +50 -0
- evalsi/v1alpha1/evaluator_pb2.pyi +271 -0
- evalsi/v1alpha1/guardrail_service_pb2.py +89 -0
- evalsi/v1alpha1/guardrail_service_pb2.pyi +534 -0
- evalsi/v1alpha1/guardrail_service_pb2_grpc.py +282 -0
- evalsi/v1alpha1/guardrail_service_pb2_grpc.pyi +106 -0
- evalsi/v1alpha1/monitor_service_pb2.py +75 -0
- evalsi/v1alpha1/monitor_service_pb2.pyi +458 -0
- evalsi/v1alpha1/monitor_service_pb2_grpc.py +230 -0
- evalsi/v1alpha1/monitor_service_pb2_grpc.pyi +85 -0
- evalsi/v1alpha1/record_pb2.py +95 -0
- evalsi/v1alpha1/record_pb2.pyi +727 -0
- evalsi/v1alpha1/reward_service_pb2.py +60 -0
- evalsi/v1alpha1/reward_service_pb2.pyi +311 -0
- evalsi/v1alpha1/reward_service_pb2_grpc.py +112 -0
- evalsi/v1alpha1/reward_service_pb2_grpc.pyi +70 -0
- evalsi/v1alpha1/run_pb2.py +75 -0
- evalsi/v1alpha1/run_pb2.pyi +582 -0
- evalsi/v1alpha1/run_service_pb2.py +93 -0
- evalsi/v1alpha1/run_service_pb2.pyi +593 -0
- evalsi/v1alpha1/run_service_pb2_grpc.py +541 -0
- evalsi/v1alpha1/run_service_pb2_grpc.pyi +181 -0
- evalsi/v1alpha1/score_pb2.py +57 -0
- evalsi/v1alpha1/score_pb2.pyi +305 -0
- evalsi/v1alpha1/source_service_pb2.py +101 -0
- evalsi/v1alpha1/source_service_pb2.pyi +742 -0
- evalsi/v1alpha1/source_service_pb2_grpc.py +382 -0
- evalsi/v1alpha1/source_service_pb2_grpc.pyi +154 -0
- evalsi/v1alpha1/trace_service_pb2.py +62 -0
- evalsi/v1alpha1/trace_service_pb2.pyi +315 -0
- evalsi/v1alpha1/trace_service_pb2_grpc.py +192 -0
- evalsi/v1alpha1/trace_service_pb2_grpc.pyi +91 -0
- evalsi/v1alpha1/webhook_service_pb2.py +73 -0
- evalsi/v1alpha1/webhook_service_pb2.pyi +458 -0
- evalsi/v1alpha1/webhook_service_pb2_grpc.py +353 -0
- evalsi/v1alpha1/webhook_service_pb2_grpc.pyi +157 -0
- evalsi/wasm.py +256 -0
- evalsi/webhooks.py +55 -0
- evalsi/worker.py +335 -0
- evalsi-0.1.0.dist-info/METADATA +62 -0
- evalsi-0.1.0.dist-info/RECORD +149 -0
- evalsi-0.1.0.dist-info/WHEEL +4 -0
- evalsi-0.1.0.dist-info/entry_points.txt +15 -0
evalsi/__init__.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Evals.si: one entrypoint for evaluating ML models, LLMs and agents.
|
|
2
|
+
|
|
3
|
+
Quick start::
|
|
4
|
+
|
|
5
|
+
import evalsi
|
|
6
|
+
|
|
7
|
+
result = evalsi.evaluate("qa.jsonl", ["exact-match", "llm-judge"],
|
|
8
|
+
judge=evalsi.JudgeConfig(provider="anthropic",
|
|
9
|
+
model="claude-opus-5-5"))
|
|
10
|
+
print(result.table())
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from evalsi._version import __version__
|
|
14
|
+
from evalsi.evaluator import (
|
|
15
|
+
EvalContext,
|
|
16
|
+
EvaluatorConfigError,
|
|
17
|
+
MetricSpec,
|
|
18
|
+
Requirements,
|
|
19
|
+
Scope,
|
|
20
|
+
ScoreType,
|
|
21
|
+
SkipRecord,
|
|
22
|
+
evaluator,
|
|
23
|
+
)
|
|
24
|
+
from evalsi.judges import JudgeConfig
|
|
25
|
+
from evalsi.logging import Logger, configure_logging, flush, log
|
|
26
|
+
from evalsi.registry import Pack, Registry
|
|
27
|
+
from evalsi.results import EvalResult, MetricSummary
|
|
28
|
+
from evalsi.runner import aevaluate, evaluate
|
|
29
|
+
from evalsi.types import (
|
|
30
|
+
Content,
|
|
31
|
+
EvaluationResult,
|
|
32
|
+
Message,
|
|
33
|
+
Outcome,
|
|
34
|
+
Record,
|
|
35
|
+
Score,
|
|
36
|
+
Step,
|
|
37
|
+
TaskCheck,
|
|
38
|
+
ToolCall,
|
|
39
|
+
ToolUse,
|
|
40
|
+
Trajectory,
|
|
41
|
+
Usage,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"Content",
|
|
46
|
+
"EvalContext",
|
|
47
|
+
"EvalResult",
|
|
48
|
+
"EvaluationResult",
|
|
49
|
+
"EvaluatorConfigError",
|
|
50
|
+
"JudgeConfig",
|
|
51
|
+
"Logger",
|
|
52
|
+
"Message",
|
|
53
|
+
"MetricSpec",
|
|
54
|
+
"MetricSummary",
|
|
55
|
+
"Outcome",
|
|
56
|
+
"Pack",
|
|
57
|
+
"Record",
|
|
58
|
+
"Registry",
|
|
59
|
+
"Requirements",
|
|
60
|
+
"Scope",
|
|
61
|
+
"Score",
|
|
62
|
+
"ScoreType",
|
|
63
|
+
"SkipRecord",
|
|
64
|
+
"Step",
|
|
65
|
+
"TaskCheck",
|
|
66
|
+
"ToolCall",
|
|
67
|
+
"ToolUse",
|
|
68
|
+
"Trajectory",
|
|
69
|
+
"Usage",
|
|
70
|
+
"__version__",
|
|
71
|
+
"aevaluate",
|
|
72
|
+
"configure_logging",
|
|
73
|
+
"evaluate",
|
|
74
|
+
"evaluator",
|
|
75
|
+
"flush",
|
|
76
|
+
"log",
|
|
77
|
+
]
|
evalsi/__main__.py
ADDED
evalsi/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
evalsi/analytics.py
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""Cross-run analytics in DuckDB.
|
|
2
|
+
|
|
3
|
+
Runs, from results files (``evalsi run --output``) or from a server through
|
|
4
|
+
its API, are loaded into three tables:
|
|
5
|
+
|
|
6
|
+
``runs``
|
|
7
|
+
run_id, name, project, status, model, source, created_at, labels (JSON)
|
|
8
|
+
``scores``
|
|
9
|
+
run_id, record_id, trial, evaluator, metric, value, passed, label,
|
|
10
|
+
outcome, reason, explanation: one row per score, plus one per result
|
|
11
|
+
that was skipped or errored (value NULL)
|
|
12
|
+
``records``
|
|
13
|
+
run_id, record_id, trial, input, output, reference, metadata (JSON):
|
|
14
|
+
one row per record, or per record and trial when a run produced the
|
|
15
|
+
outputs (trial -1 is a dataset record, the same in every trial)
|
|
16
|
+
|
|
17
|
+
so results can be sliced by record metadata across many runs, in SQL or
|
|
18
|
+
with :meth:`Warehouse.slice`. Going through the API, a server's results are
|
|
19
|
+
read the same way whatever its backend (SQLite, PostgreSQL, ClickHouse),
|
|
20
|
+
and only the runs the caller may read.
|
|
21
|
+
|
|
22
|
+
Needs ``duckdb`` (``pip install 'evalsi[analytics]'``).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import math
|
|
29
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
from evalsi.results import record_trial, result_metric
|
|
34
|
+
|
|
35
|
+
_SCHEMA = """
|
|
36
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
37
|
+
run_id VARCHAR PRIMARY KEY, name VARCHAR, project VARCHAR, status VARCHAR,
|
|
38
|
+
model VARCHAR, source VARCHAR, created_at TIMESTAMPTZ, labels JSON
|
|
39
|
+
);
|
|
40
|
+
CREATE TABLE IF NOT EXISTS scores (
|
|
41
|
+
run_id VARCHAR, record_id VARCHAR, trial INTEGER, evaluator VARCHAR,
|
|
42
|
+
metric VARCHAR, value DOUBLE, passed BOOLEAN, label VARCHAR,
|
|
43
|
+
outcome VARCHAR, reason VARCHAR, explanation VARCHAR
|
|
44
|
+
);
|
|
45
|
+
CREATE TABLE IF NOT EXISTS records (
|
|
46
|
+
run_id VARCHAR, record_id VARCHAR, trial INTEGER, input VARCHAR, output VARCHAR,
|
|
47
|
+
reference VARCHAR, metadata JSON
|
|
48
|
+
);
|
|
49
|
+
"""
|
|
50
|
+
# Warehouse files from before records had a trial.
|
|
51
|
+
_MIGRATE = "ALTER TABLE records ADD COLUMN IF NOT EXISTS trial INTEGER DEFAULT -1"
|
|
52
|
+
_RECORD_COLUMNS = "run_id, record_id, trial, input, output, reference, metadata"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _duckdb() -> Any:
|
|
56
|
+
try:
|
|
57
|
+
import duckdb
|
|
58
|
+
except ImportError as exc: # pragma: no cover - depends on the install
|
|
59
|
+
raise RuntimeError(
|
|
60
|
+
"cross-run analytics needs duckdb: pip install 'evalsi[analytics]'"
|
|
61
|
+
) from exc
|
|
62
|
+
return duckdb
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _text(value: Any) -> str | None:
|
|
66
|
+
if value is None:
|
|
67
|
+
return None
|
|
68
|
+
if isinstance(value, Mapping):
|
|
69
|
+
if "text" in value:
|
|
70
|
+
return str(value["text"])
|
|
71
|
+
return json.dumps(value, ensure_ascii=False, default=str)
|
|
72
|
+
return str(value)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _score_rows(run_id: str, results: Iterable[Mapping[str, Any]]) -> list[tuple[Any, ...]]:
|
|
76
|
+
rows: list[tuple[Any, ...]] = []
|
|
77
|
+
for r in results:
|
|
78
|
+
rid = str(r.get("record_id", r.get("recordId", "")))
|
|
79
|
+
trial = int(r.get("trial", 0) or 0)
|
|
80
|
+
evaluator = str(r.get("evaluator", ""))
|
|
81
|
+
ref = str(r.get("evaluator_ref", r.get("evaluatorRef", "")))
|
|
82
|
+
outcome = str(r.get("outcome", "")).lower().removeprefix("outcome_")
|
|
83
|
+
reason = str(r.get("reason", "")) or None
|
|
84
|
+
scores = r.get("scores") or []
|
|
85
|
+
if outcome != "scored" or not scores:
|
|
86
|
+
empty = (None, None, None, outcome, reason, None)
|
|
87
|
+
rows.append((run_id, rid, trial, evaluator, evaluator, *empty))
|
|
88
|
+
continue
|
|
89
|
+
for s in scores:
|
|
90
|
+
value: float | None = None
|
|
91
|
+
passed = s.get("passed")
|
|
92
|
+
if "number" in s and s["number"] is not None:
|
|
93
|
+
value = float(s["number"])
|
|
94
|
+
elif passed is not None:
|
|
95
|
+
value = 1.0 if passed else 0.0
|
|
96
|
+
label = s.get("label")
|
|
97
|
+
rows.append(
|
|
98
|
+
(
|
|
99
|
+
run_id,
|
|
100
|
+
rid,
|
|
101
|
+
trial,
|
|
102
|
+
evaluator,
|
|
103
|
+
result_metric(evaluator, ref, str(s.get("name", ""))),
|
|
104
|
+
value,
|
|
105
|
+
None if passed is None else bool(passed),
|
|
106
|
+
None if label is None else str(label),
|
|
107
|
+
outcome,
|
|
108
|
+
reason,
|
|
109
|
+
str(s.get("explanation", "")) or None,
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
return rows
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _record_rows(run_id: str, records: Iterable[Mapping[str, Any]]) -> list[tuple[Any, ...]]:
|
|
116
|
+
# One row per record and trial: result pages repeat a record for each
|
|
117
|
+
# page its results fall on.
|
|
118
|
+
rows: dict[tuple[str, int], tuple[Any, ...]] = {}
|
|
119
|
+
for r in records:
|
|
120
|
+
rid = str(r.get("id", ""))
|
|
121
|
+
trial = record_trial(r)
|
|
122
|
+
key = (rid, -1 if trial is None else trial)
|
|
123
|
+
rows[key] = (
|
|
124
|
+
run_id,
|
|
125
|
+
rid,
|
|
126
|
+
key[1],
|
|
127
|
+
_text(r.get("input")),
|
|
128
|
+
_text(r.get("output")),
|
|
129
|
+
_text(r.get("reference")),
|
|
130
|
+
json.dumps(r.get("metadata") or {}, ensure_ascii=False, default=str),
|
|
131
|
+
)
|
|
132
|
+
return list(rows.values())
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class Warehouse:
|
|
136
|
+
"""Runs loaded into DuckDB, in memory or in a file (``path``)."""
|
|
137
|
+
|
|
138
|
+
def __init__(self, path: str = ":memory:") -> None:
|
|
139
|
+
self.db = _duckdb().connect(path)
|
|
140
|
+
self.db.execute(_SCHEMA)
|
|
141
|
+
self.db.execute(_MIGRATE)
|
|
142
|
+
|
|
143
|
+
def close(self) -> None:
|
|
144
|
+
self.db.close()
|
|
145
|
+
|
|
146
|
+
def __enter__(self) -> Warehouse:
|
|
147
|
+
return self
|
|
148
|
+
|
|
149
|
+
def __exit__(self, *exc: object) -> None:
|
|
150
|
+
self.close()
|
|
151
|
+
|
|
152
|
+
def _replace(
|
|
153
|
+
self,
|
|
154
|
+
run: tuple[Any, ...],
|
|
155
|
+
results: Iterable[Mapping[str, Any]],
|
|
156
|
+
records: Iterable[Mapping[str, Any]],
|
|
157
|
+
) -> None:
|
|
158
|
+
run_id = run[0]
|
|
159
|
+
self.db.execute("BEGIN")
|
|
160
|
+
try:
|
|
161
|
+
for table in ("runs", "scores", "records"):
|
|
162
|
+
self.db.execute(f"DELETE FROM {table} WHERE run_id = ?", [run_id])
|
|
163
|
+
self.db.execute("INSERT INTO runs VALUES (?, ?, ?, ?, ?, ?, ?, ?)", list(run))
|
|
164
|
+
scores = _score_rows(run_id, results)
|
|
165
|
+
if scores:
|
|
166
|
+
self.db.executemany(
|
|
167
|
+
"INSERT INTO scores VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", scores
|
|
168
|
+
)
|
|
169
|
+
recs = _record_rows(run_id, records)
|
|
170
|
+
if recs:
|
|
171
|
+
self.db.executemany(
|
|
172
|
+
f"INSERT INTO records ({_RECORD_COLUMNS}) VALUES (?, ?, ?, ?, ?, ?, ?)", recs
|
|
173
|
+
)
|
|
174
|
+
self.db.execute("COMMIT")
|
|
175
|
+
except Exception:
|
|
176
|
+
self.db.execute("ROLLBACK")
|
|
177
|
+
raise
|
|
178
|
+
|
|
179
|
+
def add_results(self, data: Mapping[str, Any], *, run_id: str = "") -> str:
|
|
180
|
+
"""A results file's content (``evalsi run --output``). The run is
|
|
181
|
+
named by ``run_id``, else the manifest's run id or name and start."""
|
|
182
|
+
manifest = data.get("manifest") or {}
|
|
183
|
+
target = manifest.get("target") or {}
|
|
184
|
+
rid = (
|
|
185
|
+
run_id
|
|
186
|
+
or str(manifest.get("run_id") or "")
|
|
187
|
+
or f"{manifest.get('name') or 'run'}@{manifest.get('started_at') or ''}"
|
|
188
|
+
)
|
|
189
|
+
labels = (manifest.get("spec") or {}).get("labels") or manifest.get("labels") or {}
|
|
190
|
+
run = (
|
|
191
|
+
rid,
|
|
192
|
+
str(manifest.get("name") or ""),
|
|
193
|
+
str(manifest.get("project") or ""),
|
|
194
|
+
"finished",
|
|
195
|
+
str(target.get("model") or "") if isinstance(target, Mapping) else "",
|
|
196
|
+
"file",
|
|
197
|
+
manifest.get("started_at"),
|
|
198
|
+
json.dumps(labels, default=str),
|
|
199
|
+
)
|
|
200
|
+
self._replace(run, data.get("results") or [], data.get("records") or [])
|
|
201
|
+
return rid
|
|
202
|
+
|
|
203
|
+
def add_results_file(self, path: str | Path) -> str:
|
|
204
|
+
p = Path(path)
|
|
205
|
+
return self.add_results(json.loads(p.read_text(encoding="utf-8")), run_id="")
|
|
206
|
+
|
|
207
|
+
def add_server_run(self, client: Any, run: Mapping[str, Any]) -> str:
|
|
208
|
+
"""A server run (as ``GetRun``/``ListRuns`` return it), with its results."""
|
|
209
|
+
run_id = str(run.get("id", ""))
|
|
210
|
+
results: list[dict[str, Any]] = []
|
|
211
|
+
records: list[dict[str, Any]] = []
|
|
212
|
+
token = ""
|
|
213
|
+
while True:
|
|
214
|
+
page = client.list_run_results(run_id, page_token=token)
|
|
215
|
+
results += page.get("results", [])
|
|
216
|
+
records += page.get("records", [])
|
|
217
|
+
token = page.get("nextPageToken", "")
|
|
218
|
+
if not token:
|
|
219
|
+
break
|
|
220
|
+
target = (run.get("spec") or {}).get("target") or {}
|
|
221
|
+
row = (
|
|
222
|
+
run_id,
|
|
223
|
+
str(run.get("name") or ""),
|
|
224
|
+
str(run.get("project") or ""),
|
|
225
|
+
str(run.get("status", "")).removeprefix("RUN_STATUS_").lower(),
|
|
226
|
+
str(target.get("model") or ""),
|
|
227
|
+
"server",
|
|
228
|
+
run.get("createdAt"),
|
|
229
|
+
json.dumps(run.get("labels") or {}),
|
|
230
|
+
)
|
|
231
|
+
self._replace(row, results, records)
|
|
232
|
+
return run_id
|
|
233
|
+
|
|
234
|
+
def add_server_runs(
|
|
235
|
+
self,
|
|
236
|
+
client: Any,
|
|
237
|
+
*,
|
|
238
|
+
project: str = "",
|
|
239
|
+
labels: Mapping[str, str] | None = None,
|
|
240
|
+
run_ids: Sequence[str] = (),
|
|
241
|
+
limit: int = 50,
|
|
242
|
+
) -> list[str]:
|
|
243
|
+
"""The newest ``limit`` runs of a project (with these labels), or the given runs."""
|
|
244
|
+
if run_ids:
|
|
245
|
+
return [self.add_server_run(client, client.get_run(r)) for r in run_ids]
|
|
246
|
+
loaded: list[str] = []
|
|
247
|
+
token = ""
|
|
248
|
+
while len(loaded) < limit:
|
|
249
|
+
page = client.list_runs(project=project, page_token=token)
|
|
250
|
+
for run in page.get("runs", []):
|
|
251
|
+
have = run.get("labels") or {}
|
|
252
|
+
if labels and any(have.get(k) != v for k, v in labels.items()):
|
|
253
|
+
continue
|
|
254
|
+
# ListRuns leaves the spec out (the target, the model); the
|
|
255
|
+
# run itself has it.
|
|
256
|
+
full = run if "spec" in run else client.get_run(str(run.get("id", "")))
|
|
257
|
+
loaded.append(self.add_server_run(client, full))
|
|
258
|
+
if len(loaded) >= limit:
|
|
259
|
+
break
|
|
260
|
+
token = page.get("nextPageToken", "")
|
|
261
|
+
if not token:
|
|
262
|
+
break
|
|
263
|
+
return loaded
|
|
264
|
+
|
|
265
|
+
def query(
|
|
266
|
+
self, sql: str, params: Sequence[Any] = ()
|
|
267
|
+
) -> tuple[list[str], list[tuple[Any, ...]]]:
|
|
268
|
+
cur = self.db.execute(sql, list(params))
|
|
269
|
+
columns = [d[0] for d in cur.description or []]
|
|
270
|
+
return columns, cur.fetchall()
|
|
271
|
+
|
|
272
|
+
def slice(
|
|
273
|
+
self, metric: str, by: str, *, level: float = 0.95
|
|
274
|
+
) -> tuple[list[str], list[tuple[Any, ...]]]:
|
|
275
|
+
"""``metric`` per run and per value of the record metadata field
|
|
276
|
+
``by`` (a dotted path), with a normal-approximation interval."""
|
|
277
|
+
z = {0.9: 1.6449, 0.95: 1.96, 0.99: 2.5758}.get(level)
|
|
278
|
+
if z is None:
|
|
279
|
+
raise ValueError("level must be 0.9, 0.95 or 0.99")
|
|
280
|
+
path = "$." + ".".join(json.dumps(p) for p in by.split("."))
|
|
281
|
+
alias = by.replace('"', '""')
|
|
282
|
+
_, rows = self.query(
|
|
283
|
+
f"""
|
|
284
|
+
SELECT s.run_id, any_value(r.name) AS run,
|
|
285
|
+
coalesce(
|
|
286
|
+
json_extract_string(coalesce(out.metadata, rec.metadata), ?), '(none)'
|
|
287
|
+
) AS "{alias}",
|
|
288
|
+
count(s.value) AS n, avg(s.value) AS mean, stddev_samp(s.value) AS sd
|
|
289
|
+
FROM scores s
|
|
290
|
+
JOIN runs r USING (run_id)
|
|
291
|
+
-- The output of the score's own trial, else the dataset record:
|
|
292
|
+
-- at most one row each, so a score is counted once.
|
|
293
|
+
LEFT JOIN records out
|
|
294
|
+
ON out.run_id = s.run_id AND out.record_id = s.record_id AND out.trial = s.trial
|
|
295
|
+
LEFT JOIN records rec
|
|
296
|
+
ON rec.run_id = s.run_id AND rec.record_id = s.record_id AND rec.trial = -1
|
|
297
|
+
WHERE s.metric = ? AND s.value IS NOT NULL
|
|
298
|
+
GROUP BY ALL
|
|
299
|
+
ORDER BY 3, any_value(r.created_at), 1
|
|
300
|
+
""",
|
|
301
|
+
[path, metric],
|
|
302
|
+
)
|
|
303
|
+
out = []
|
|
304
|
+
for run_id, name, key, n, mean, sd in rows:
|
|
305
|
+
half = z * sd / math.sqrt(n) if n > 1 and sd is not None else None
|
|
306
|
+
low = None if half is None else mean - half
|
|
307
|
+
high = None if half is None else mean + half
|
|
308
|
+
out.append((run_id, name, key, n, mean, low, high))
|
|
309
|
+
return ["run_id", "run", by, "n", "mean", "low", "high"], out
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def format_table(columns: Sequence[str], rows: Sequence[Sequence[Any]]) -> str:
|
|
313
|
+
def cell(v: Any) -> str:
|
|
314
|
+
if v is None:
|
|
315
|
+
return ""
|
|
316
|
+
if isinstance(v, float):
|
|
317
|
+
return f"{v:.3f}"
|
|
318
|
+
return str(v)
|
|
319
|
+
|
|
320
|
+
body = [[cell(v) for v in row] for row in rows]
|
|
321
|
+
widths = [max([len(c), *(len(r[i]) for r in body)]) for i, c in enumerate(columns)]
|
|
322
|
+
lines = [" ".join(c.ljust(w) for c, w in zip(columns, widths, strict=True)).rstrip()]
|
|
323
|
+
for r in body:
|
|
324
|
+
lines.append(" ".join(v.ljust(w) for v, w in zip(r, widths, strict=True)).rstrip())
|
|
325
|
+
return "\n".join(lines)
|