sable-sphynx 0.7.0.dev0__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.
- sable_sphynx-0.7.0.dev0.dist-info/METADATA +152 -0
- sable_sphynx-0.7.0.dev0.dist-info/RECORD +174 -0
- sable_sphynx-0.7.0.dev0.dist-info/WHEEL +5 -0
- sable_sphynx-0.7.0.dev0.dist-info/top_level.txt +1 -0
- sphynx/__init__.py +469 -0
- sphynx/__init__.pyi +376 -0
- sphynx/_analytical_client.py +77 -0
- sphynx/_analytical_values.py +252 -0
- sphynx/_audit_client.py +173 -0
- sphynx/_comparison_adapters.py +113 -0
- sphynx/_componentized_full_rag.py +34 -0
- sphynx/_composed_pipeline.py +109 -0
- sphynx/_full_rag_grading.py +218 -0
- sphynx/_full_rag_trace.py +221 -0
- sphynx/_local_eval.py +268 -0
- sphynx/_pipeline_core.py +243 -0
- sphynx/_pr_client.py +237 -0
- sphynx/_rag_diagnostics_calibration.py +285 -0
- sphynx/_rag_diagnostics_catalog.py +39 -0
- sphynx/_rag_diagnostics_common.py +26 -0
- sphynx/_rag_diagnostics_contracts.py +186 -0
- sphynx/_rag_diagnostics_engine.py +243 -0
- sphynx/_rag_diagnostics_importers.py +242 -0
- sphynx/_rag_diagnostics_ranking.py +81 -0
- sphynx/_rag_diagnostics_semantic.py +325 -0
- sphynx/_rag_ranking.py +149 -0
- sphynx/_speech_benchmark_scoring.py +140 -0
- sphynx/_speech_benchmark_sources.py +275 -0
- sphynx/_speech_complete_scoring.py +218 -0
- sphynx/_speech_complete_source.py +118 -0
- sphynx/_speech_kido.py +294 -0
- sphynx/_speech_scoring.py +77 -0
- sphynx/_track_capture.py +213 -0
- sphynx/analytical_views.py +515 -0
- sphynx/assessment_contracts.py +378 -0
- sphynx/assessment_selection.py +57 -0
- sphynx/audit_access.py +245 -0
- sphynx/audit_contracts.py +204 -0
- sphynx/audit_requests.py +180 -0
- sphynx/authoring.py +188 -0
- sphynx/benchmarks.py +568 -0
- sphynx/candidates.py +62 -0
- sphynx/catalog.py +272 -0
- sphynx/chunking.py +372 -0
- sphynx/clickhouse_reader.py +169 -0
- sphynx/client.py +486 -0
- sphynx/comparison_core.py +149 -0
- sphynx/comparison_views.py +223 -0
- sphynx/components.py +1661 -0
- sphynx/contracts.py +514 -0
- sphynx/customer_rerank.py +299 -0
- sphynx/customer_retrieval.py +146 -0
- sphynx/data/benchmark-releases.v1.json +164 -0
- sphynx/data/public-benchmark-release.v1.json +354 -0
- sphynx/data/rag-diagnostics-synthetic.v1.json +6982 -0
- sphynx/data/rag-paper-references.v1.json +176 -0
- sphynx/data/rag-paper-schemas.v1.json +1069 -0
- sphynx/data/rag-rerank-v1.json +507 -0
- sphynx/dataset_catalog_data.py +1214 -0
- sphynx/dataset_readiness.py +138 -0
- sphynx/dataset_v3.py +999 -0
- sphynx/demo.py +130 -0
- sphynx/demo_candidates/__init__.py +1 -0
- sphynx/demo_candidates/dense.py +11 -0
- sphynx/demo_candidates/lexical.py +21 -0
- sphynx/diagnostics.py +207 -0
- sphynx/documentation.py +737 -0
- sphynx/documentation_candidate.py +939 -0
- sphynx/evals.py +538 -0
- sphynx/evaluation_protocols.py +124 -0
- sphynx/evaluator_bindings.py +370 -0
- sphynx/evidence.py +352 -0
- sphynx/execution_capabilities.py +109 -0
- sphynx/fast_search.py +285 -0
- sphynx/full_rag.py +341 -0
- sphynx/hosted/__init__.py +18 -0
- sphynx/hosted/benchmark_intake.py +218 -0
- sphynx/hosted/contracts.py +361 -0
- sphynx/hosted/model_admission.py +130 -0
- sphynx/hosted/model_artifacts.py +298 -0
- sphynx/hosted/model_contracts.py +63 -0
- sphynx/hosted/model_gcs.py +181 -0
- sphynx/hosted/model_history.py +84 -0
- sphynx/hosted/model_session.py +233 -0
- sphynx/hosted/public_suite.py +74 -0
- sphynx/hosted/registry.py +97 -0
- sphynx/hosted/revisions.py +126 -0
- sphynx/hosted/sandbox.py +105 -0
- sphynx/hosted/submission.py +287 -0
- sphynx/hosted_workflow.py +451 -0
- sphynx/http_transport.py +86 -0
- sphynx/integrated_rag.py +352 -0
- sphynx/integrated_workflow.py +552 -0
- sphynx/integration_contracts.py +474 -0
- sphynx/invocation.py +791 -0
- sphynx/invocation_candidates.py +392 -0
- sphynx/invocation_catalog_data.py +40 -0
- sphynx/learned_catalog_data.py +30 -0
- sphynx/learned_rerank.py +267 -0
- sphynx/legacy_full_rag.py +67 -0
- sphynx/lifecycle.py +885 -0
- sphynx/lifecycle_workflow.py +490 -0
- sphynx/metric_views.py +474 -0
- sphynx/metrics.py +282 -0
- sphynx/model_adapters.py +438 -0
- sphynx/model_catalog_data.py +30 -0
- sphynx/official_benchmarks.py +385 -0
- sphynx/packs.py +258 -0
- sphynx/pipeline.py +313 -0
- sphynx/pipeline_workflow.py +551 -0
- sphynx/pr_evaluations.py +560 -0
- sphynx/progress.py +89 -0
- sphynx/public_bundle.py +173 -0
- sphynx/public_eval.py +62 -0
- sphynx/public_rerank.py +355 -0
- sphynx/py.typed +0 -0
- sphynx/pytest_plugin.py +62 -0
- sphynx/question_robustness.py +437 -0
- sphynx/rag.py +329 -0
- sphynx/rag_diagnostics.py +309 -0
- sphynx/rag_papers/__init__.py +29 -0
- sphynx/rag_papers/__main__.py +45 -0
- sphynx/rag_papers/candidates.py +19 -0
- sphynx/rag_papers/contracts.py +282 -0
- sphynx/rag_papers/datasets.py +291 -0
- sphynx/rag_papers/experiments.py +116 -0
- sphynx/rag_papers/judges.py +308 -0
- sphynx/rag_papers/registry.py +33 -0
- sphynx/rag_papers/runner.py +176 -0
- sphynx/rag_papers/sable.py +135 -0
- sphynx/rag_papers/schemas.py +12 -0
- sphynx/rag_papers/scoring.py +249 -0
- sphynx/rag_papers/semantic.py +288 -0
- sphynx/rag_papers/trec.py +80 -0
- sphynx/recorded_scores.py +352 -0
- sphynx/records.py +124 -0
- sphynx/reflex_traces.py +193 -0
- sphynx/report_rendering.py +71 -0
- sphynx/rerank_family_contracts.py +150 -0
- sphynx/rerank_workflow.py +424 -0
- sphynx/research_candidate.py +190 -0
- sphynx/results.py +852 -0
- sphynx/retrieval.py +750 -0
- sphynx/retrieval_candidates.py +295 -0
- sphynx/retrieval_catalog_data.py +19 -0
- sphynx/retrieval_recordings.py +125 -0
- sphynx/reviewer_audit.py +266 -0
- sphynx/routing_policies.py +375 -0
- sphynx/runner.py +544 -0
- sphynx/sable_rerank.py +489 -0
- sphynx/score_reliability.py +393 -0
- sphynx/speech_benchmarks.py +204 -0
- sphynx/speech_campaigns.py +378 -0
- sphynx/speech_complete.py +236 -0
- sphynx/speech_contracts.py +225 -0
- sphynx/speech_evidence.py +429 -0
- sphynx/speech_listening.py +115 -0
- sphynx/speech_multilingual.py +71 -0
- sphynx/speech_tts.py +479 -0
- sphynx/sql/001_sync_v1.sql +30 -0
- sphynx/stage_contracts.py +199 -0
- sphynx/statistical_catalog_data.py +53 -0
- sphynx/statistical_rerank.py +232 -0
- sphynx/sync.py +797 -0
- sphynx/workbench.py +378 -0
- sphynx/workbench_catalog_data.py +13 -0
- sphynx/workbench_tracks.py +78 -0
- sphynx/workflow_async.py +197 -0
- sphynx/workflow_contracts.py +594 -0
- sphynx/workflow_decisions.py +211 -0
- sphynx/workflow_defaults.py +16 -0
- sphynx/workflow_extensions.py +212 -0
- sphynx/workflow_registry.py +513 -0
- sphynx/workflows.py +792 -0
sphynx/metrics.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""Versioned illustrative frozen-pool RAG ranking metrics.
|
|
2
|
+
|
|
3
|
+
The gain, discount, relevance threshold, and required-group rules follow the
|
|
4
|
+
local ``rag-evals/src/metrics-v2.ts`` source pinned by T-001. This narrower
|
|
5
|
+
Sphynx release deliberately publishes only four ranking metrics and runner
|
|
6
|
+
wall latency. No answer-quality or abstention grade is inferred.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import math
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Sequence
|
|
14
|
+
|
|
15
|
+
from .contracts import CaseReferences, ContractError, FrozenPoolCase, _finite_number
|
|
16
|
+
from .rag import RagRerankPack, load_rag_rerank_pack
|
|
17
|
+
|
|
18
|
+
METRIC_RELEASE = "rag.rerank.metric.v1"
|
|
19
|
+
GRADER_RELEASE = "rag.rerank.grader.v1"
|
|
20
|
+
_QUALITY_METRICS = ("ndcg_at_5", "hit_at_5", "mrr_at_5", "evidence_group_recall_at_5")
|
|
21
|
+
_LATENCY_METRIC = "runner_latency_ms"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class RankingMetricDefinition:
|
|
26
|
+
metric_id: str
|
|
27
|
+
release: str
|
|
28
|
+
unit: str
|
|
29
|
+
direction: str
|
|
30
|
+
primary: bool
|
|
31
|
+
eligible_population: str
|
|
32
|
+
aggregation: str
|
|
33
|
+
undefined_policy: str
|
|
34
|
+
missing_policy: str
|
|
35
|
+
|
|
36
|
+
def to_dict(self) -> dict[str, str | bool]:
|
|
37
|
+
return vars(self).copy()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
METRIC_DEFINITIONS = (
|
|
41
|
+
RankingMetricDefinition(
|
|
42
|
+
"ndcg_at_5", METRIC_RELEASE, "ratio", "higher", True,
|
|
43
|
+
"answerable cases in the fixed pack", "arithmetic mean across eligible cases",
|
|
44
|
+
"null for unsupported cases or zero ideal DCG", "null and incomplete aggregate when an eligible case fails or lacks a valid ranking",
|
|
45
|
+
),
|
|
46
|
+
RankingMetricDefinition(
|
|
47
|
+
"hit_at_5", METRIC_RELEASE, "ratio", "higher", False,
|
|
48
|
+
"answerable cases in the fixed pack", "arithmetic mean across eligible cases",
|
|
49
|
+
"null for unsupported cases", "null and incomplete aggregate when an eligible case fails or lacks a valid ranking",
|
|
50
|
+
),
|
|
51
|
+
RankingMetricDefinition(
|
|
52
|
+
"mrr_at_5", METRIC_RELEASE, "ratio", "higher", False,
|
|
53
|
+
"answerable cases in the fixed pack", "arithmetic mean across eligible cases",
|
|
54
|
+
"null for unsupported cases", "null and incomplete aggregate when an eligible case fails or lacks a valid ranking",
|
|
55
|
+
),
|
|
56
|
+
RankingMetricDefinition(
|
|
57
|
+
"evidence_group_recall_at_5", METRIC_RELEASE, "ratio", "higher", False,
|
|
58
|
+
"answerable cases with required evidence groups", "arithmetic mean across eligible cases",
|
|
59
|
+
"null for unsupported cases or no required groups", "null and incomplete aggregate when an eligible case fails or lacks a valid ranking",
|
|
60
|
+
),
|
|
61
|
+
RankingMetricDefinition(
|
|
62
|
+
_LATENCY_METRIC, METRIC_RELEASE, "ms", "lower", False,
|
|
63
|
+
"all attempted cases with a runner monotonic span", "per-case span; aggregate p50 and p95",
|
|
64
|
+
"null when no runner span exists", "null and incomplete aggregate when an attempted case lacks a runner span",
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def metric_definitions() -> tuple[RankingMetricDefinition, ...]:
|
|
70
|
+
return METRIC_DEFINITIONS
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class CaseMetricResult:
|
|
75
|
+
case_id: str
|
|
76
|
+
pool_digest: str
|
|
77
|
+
terminal_status: str
|
|
78
|
+
answerable: bool
|
|
79
|
+
values: tuple[tuple[str, float | None], ...]
|
|
80
|
+
undefined_reasons: tuple[tuple[str, str], ...]
|
|
81
|
+
metric_release: str = METRIC_RELEASE
|
|
82
|
+
grader_release: str = GRADER_RELEASE
|
|
83
|
+
|
|
84
|
+
def value_for(self, metric_id: str) -> float | None:
|
|
85
|
+
return dict(self.values)[metric_id]
|
|
86
|
+
|
|
87
|
+
def to_dict(self) -> dict[str, object]:
|
|
88
|
+
return {
|
|
89
|
+
"case_id": self.case_id,
|
|
90
|
+
"pool_digest": self.pool_digest,
|
|
91
|
+
"terminal_status": self.terminal_status,
|
|
92
|
+
"answerable": self.answerable,
|
|
93
|
+
"metric_release": self.metric_release,
|
|
94
|
+
"grader_release": self.grader_release,
|
|
95
|
+
"values": dict(self.values),
|
|
96
|
+
"undefined_reasons": dict(self.undefined_reasons),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True)
|
|
101
|
+
class MetricAggregate:
|
|
102
|
+
metric_id: str
|
|
103
|
+
release: str
|
|
104
|
+
unit: str
|
|
105
|
+
direction: str
|
|
106
|
+
value: float | None
|
|
107
|
+
p95: float | None
|
|
108
|
+
eligible_count: int
|
|
109
|
+
observed_count: int
|
|
110
|
+
missing_count: int
|
|
111
|
+
undefined_count: int
|
|
112
|
+
complete: bool
|
|
113
|
+
|
|
114
|
+
def to_dict(self) -> dict[str, str | float | int | bool | None]:
|
|
115
|
+
return vars(self).copy()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _dcg(grades: Sequence[int], cutoff: int = 5) -> float:
|
|
119
|
+
return sum((2 ** grade - 1) / math.log2(index + 2) for index, grade in enumerate(grades[:cutoff]))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _percentile(values: Sequence[float], fraction: float) -> float:
|
|
123
|
+
ordered = sorted(values)
|
|
124
|
+
position = (len(ordered) - 1) * fraction
|
|
125
|
+
lower = math.floor(position)
|
|
126
|
+
upper = math.ceil(position)
|
|
127
|
+
return ordered[lower] + (ordered[upper] - ordered[lower]) * (position - lower)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def grade_case(
|
|
131
|
+
case: FrozenPoolCase,
|
|
132
|
+
references: CaseReferences,
|
|
133
|
+
ranked_ids: Sequence[str] | None,
|
|
134
|
+
latency_ms: float | None,
|
|
135
|
+
*,
|
|
136
|
+
terminal_status: str = "ok",
|
|
137
|
+
relevance_threshold: int = 2,
|
|
138
|
+
pack: RagRerankPack | None = None,
|
|
139
|
+
) -> CaseMetricResult:
|
|
140
|
+
"""Grade one attempt against the pinned pack, retaining explicit absence.
|
|
141
|
+
|
|
142
|
+
``ranked_ids`` must cover the entire frozen pool exactly once for an ``ok``
|
|
143
|
+
attempt. Use :func:`sphynx.contracts.ranked_candidate_ids` to apply the
|
|
144
|
+
stable descending-score, original-pool-order tie rule first. Latency is
|
|
145
|
+
supplied from the runner's monotonic wall clock, not candidate output.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
pack = pack or load_rag_rerank_pack()
|
|
149
|
+
original = next((item for item in pack.cases if item.case_id == case.case_id), None)
|
|
150
|
+
if original is None or original.pool_digest != case.pool_digest:
|
|
151
|
+
raise ContractError(f"Case {case.case_id!r} does not match the frozen pack pool and corpus identity")
|
|
152
|
+
if not isinstance(references, CaseReferences) or references.case_id != case.case_id:
|
|
153
|
+
raise ContractError("references must match the frozen case identity")
|
|
154
|
+
if references != pack.reference_for(case.case_id):
|
|
155
|
+
raise ContractError("references differ from the selected frozen pack labels")
|
|
156
|
+
expected_ids = {candidate.candidate_id for candidate in case.candidates}
|
|
157
|
+
by_id = {candidate.candidate_id: candidate for candidate in references.candidates}
|
|
158
|
+
if set(by_id) != expected_ids:
|
|
159
|
+
raise ContractError("reference candidate IDs do not match the frozen pool")
|
|
160
|
+
pinned_threshold = pack.relevance_threshold
|
|
161
|
+
if type(relevance_threshold) is not int or relevance_threshold != pinned_threshold:
|
|
162
|
+
raise ContractError(
|
|
163
|
+
f"{METRIC_RELEASE} requires the pack's relevance_threshold={pinned_threshold}; "
|
|
164
|
+
"another threshold requires a new metric and grader release"
|
|
165
|
+
)
|
|
166
|
+
if type(terminal_status) is not str or not terminal_status:
|
|
167
|
+
raise ContractError("terminal_status must be a nonempty string")
|
|
168
|
+
if latency_ms is not None:
|
|
169
|
+
latency_ms = _finite_number(latency_ms, "runner latency_ms")
|
|
170
|
+
if latency_ms < 0:
|
|
171
|
+
raise ContractError("runner latency_ms cannot be negative")
|
|
172
|
+
if terminal_status == "ok":
|
|
173
|
+
if ranked_ids is None or isinstance(ranked_ids, (str, bytes)):
|
|
174
|
+
raise ContractError("ok attempt requires a full ranked ID sequence")
|
|
175
|
+
ranked_ids = tuple(ranked_ids)
|
|
176
|
+
if len(ranked_ids) != len(expected_ids) or set(ranked_ids) != expected_ids:
|
|
177
|
+
raise ContractError("ranked IDs must cover the exact frozen pool once")
|
|
178
|
+
elif ranked_ids is not None:
|
|
179
|
+
raise ContractError("failed attempt cannot carry a quality ranking")
|
|
180
|
+
required_groups = tuple(group for group in references.evidence_groups if group.required)
|
|
181
|
+
answerable = bool(required_groups)
|
|
182
|
+
reasons: dict[str, str] = {}
|
|
183
|
+
values: dict[str, float | None] = {metric: None for metric in _QUALITY_METRICS}
|
|
184
|
+
if not answerable:
|
|
185
|
+
reasons.update({metric: "unsupported_query" for metric in _QUALITY_METRICS})
|
|
186
|
+
elif terminal_status != "ok":
|
|
187
|
+
reasons.update({metric: f"candidate_{terminal_status}" for metric in _QUALITY_METRICS})
|
|
188
|
+
else:
|
|
189
|
+
assert ranked_ids is not None
|
|
190
|
+
grades = [by_id[candidate_id].relevance_grade for candidate_id in ranked_ids]
|
|
191
|
+
ideal = sorted((reference.relevance_grade for reference in references.candidates), reverse=True)
|
|
192
|
+
ideal_dcg = _dcg(ideal)
|
|
193
|
+
if ideal_dcg == 0:
|
|
194
|
+
reasons["ndcg_at_5"] = "zero_ideal_dcg"
|
|
195
|
+
else:
|
|
196
|
+
values["ndcg_at_5"] = _dcg(grades) / ideal_dcg
|
|
197
|
+
first_relevant = next((index for index, grade in enumerate(grades[:5], start=1) if grade >= relevance_threshold), None)
|
|
198
|
+
values["hit_at_5"] = 1.0 if first_relevant is not None else 0.0
|
|
199
|
+
values["mrr_at_5"] = 1.0 / first_relevant if first_relevant is not None else 0.0
|
|
200
|
+
top_ids = set(ranked_ids[:5])
|
|
201
|
+
covered = sum(bool(top_ids.intersection(group.acceptable_candidate_ids)) for group in required_groups)
|
|
202
|
+
values["evidence_group_recall_at_5"] = covered / len(required_groups)
|
|
203
|
+
values[_LATENCY_METRIC] = latency_ms
|
|
204
|
+
if latency_ms is None:
|
|
205
|
+
reasons[_LATENCY_METRIC] = "runner_span_not_captured"
|
|
206
|
+
return CaseMetricResult(
|
|
207
|
+
case.case_id, case.pool_digest, terminal_status, answerable,
|
|
208
|
+
tuple(values.items()), tuple(reasons.items()),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def aggregate_case_metrics(results: Sequence[CaseMetricResult], *, pack: RagRerankPack | None = None) -> dict[str, MetricAggregate]:
|
|
213
|
+
"""Aggregate a complete selected cohort without masking missing cases.
|
|
214
|
+
|
|
215
|
+
The caller must supply one result for every fixed pack case. If any eligible
|
|
216
|
+
case has no value, the metric's aggregate is ``None`` and ``complete`` is
|
|
217
|
+
false. Unsupported cases are counted as undefined, not as failures/zeros.
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
pack = pack or load_rag_rerank_pack()
|
|
221
|
+
if len(results) != len(pack.cases):
|
|
222
|
+
raise ContractError("aggregation requires one result for every fixed pack case")
|
|
223
|
+
if any(not isinstance(result, CaseMetricResult) for result in results):
|
|
224
|
+
raise ContractError("aggregation requires CaseMetricResult values")
|
|
225
|
+
by_case = {result.case_id: result for result in results}
|
|
226
|
+
if len(by_case) != len(results) or set(by_case) != {case.case_id for case in pack.cases}:
|
|
227
|
+
raise ContractError("aggregate case IDs must equal the fixed pack case IDs exactly once")
|
|
228
|
+
for case in pack.cases:
|
|
229
|
+
result = by_case[case.case_id]
|
|
230
|
+
if result.pool_digest != case.pool_digest:
|
|
231
|
+
raise ContractError(f"aggregate pool digest mismatch for {case.case_id}")
|
|
232
|
+
if result.metric_release != pack.metric_release or result.grader_release != GRADER_RELEASE:
|
|
233
|
+
raise ContractError(f"aggregate release mismatch for {case.case_id}")
|
|
234
|
+
expected_answerable = any(group.required for group in pack.reference_for(case.case_id).evidence_groups)
|
|
235
|
+
if result.answerable is not expected_answerable:
|
|
236
|
+
raise ContractError(f"aggregate answerability mismatch for {case.case_id}")
|
|
237
|
+
if type(result.terminal_status) is not str or not result.terminal_status:
|
|
238
|
+
raise ContractError(f"aggregate terminal status is invalid for {case.case_id}")
|
|
239
|
+
value_map = dict(result.values)
|
|
240
|
+
if len(result.values) != len(METRIC_DEFINITIONS) or set(value_map) != {rule.metric_id for rule in METRIC_DEFINITIONS}:
|
|
241
|
+
raise ContractError(f"aggregate metric fields mismatch for {case.case_id}")
|
|
242
|
+
for metric_id in _QUALITY_METRICS:
|
|
243
|
+
value = value_map[metric_id]
|
|
244
|
+
if value is not None and (type(value) not in (int, float) or not math.isfinite(value) or not 0 <= value <= 1):
|
|
245
|
+
raise ContractError(f"aggregate {metric_id} value is invalid for {case.case_id}")
|
|
246
|
+
if not expected_answerable or result.terminal_status != "ok":
|
|
247
|
+
if value is not None:
|
|
248
|
+
raise ContractError(f"aggregate {metric_id} must be undefined or missing for {case.case_id}")
|
|
249
|
+
elif value is None:
|
|
250
|
+
raise ContractError(f"aggregate {metric_id} is missing from successful answerable case {case.case_id}")
|
|
251
|
+
latency = value_map[_LATENCY_METRIC]
|
|
252
|
+
if latency is not None and (type(latency) not in (int, float) or not math.isfinite(latency) or latency < 0):
|
|
253
|
+
raise ContractError(f"aggregate runner latency is invalid for {case.case_id}")
|
|
254
|
+
aggregate: dict[str, MetricAggregate] = {}
|
|
255
|
+
for definition in METRIC_DEFINITIONS:
|
|
256
|
+
values: list[float] = []
|
|
257
|
+
eligible_count = missing_count = undefined_count = 0
|
|
258
|
+
for case in pack.cases:
|
|
259
|
+
result = by_case[case.case_id]
|
|
260
|
+
eligible = definition.metric_id == _LATENCY_METRIC or any(
|
|
261
|
+
group.required for group in pack.reference_for(case.case_id).evidence_groups
|
|
262
|
+
)
|
|
263
|
+
if not eligible:
|
|
264
|
+
undefined_count += 1
|
|
265
|
+
continue
|
|
266
|
+
eligible_count += 1
|
|
267
|
+
value = result.value_for(definition.metric_id)
|
|
268
|
+
if value is None:
|
|
269
|
+
missing_count += 1
|
|
270
|
+
else:
|
|
271
|
+
values.append(value)
|
|
272
|
+
complete = missing_count == 0 and eligible_count > 0
|
|
273
|
+
if complete:
|
|
274
|
+
value = _percentile(values, 0.5) if definition.metric_id == _LATENCY_METRIC else sum(values) / len(values)
|
|
275
|
+
p95 = _percentile(values, 0.95) if definition.metric_id == _LATENCY_METRIC else None
|
|
276
|
+
else:
|
|
277
|
+
value = p95 = None
|
|
278
|
+
aggregate[definition.metric_id] = MetricAggregate(
|
|
279
|
+
definition.metric_id, definition.release, definition.unit, definition.direction,
|
|
280
|
+
value, p95, eligible_count, len(values), missing_count, undefined_count, complete,
|
|
281
|
+
)
|
|
282
|
+
return aggregate
|