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/pipeline.py
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""Capture and grade a frozen N11–N17 pipeline without exposing reference labels.
|
|
2
|
+
|
|
3
|
+
The mathematical core is an imports-only port of sablebench's Level 2 grader.
|
|
4
|
+
Execution is explicitly trusted local or pinned replay, never an OS sandbox.
|
|
5
|
+
Missing labels/failed trials remain unavailable; recorded timings are replay.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from copy import deepcopy
|
|
10
|
+
from collections.abc import Mapping
|
|
11
|
+
import math
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from time import perf_counter
|
|
14
|
+
|
|
15
|
+
from .contracts import ContractError, canonical_json
|
|
16
|
+
from .integration_contracts import CandidateConfiguration, ComparisonIdentity, TrackCaseCapture, value_digest, require_comparable
|
|
17
|
+
from .dataset_v3 import load_dataset_v3
|
|
18
|
+
from ._track_capture import (byte_digest, strict_json, create_run, write_artifact, finish_run,
|
|
19
|
+
read_artifact, verify_track_run, write_evaluation, verify_track_evaluation)
|
|
20
|
+
from .chunking import _policy_for_source, _select_tasks, _verify_approval
|
|
21
|
+
from ._pipeline_core import STAGES, evaluate_level2_run, validate_controlled_change
|
|
22
|
+
|
|
23
|
+
PACK_ID = "rag.pipeline.v1"
|
|
24
|
+
INTERFACE = "sphynx.rag.pipeline.v1"
|
|
25
|
+
METRIC_RELEASE = "rag.pipeline.source-level2.metric.v1"
|
|
26
|
+
GRADER_RELEASE = "rag.pipeline.source-level2.grader.v1"
|
|
27
|
+
POLICY_VERSION = "sphynx.rag.pipeline.policy.v1"
|
|
28
|
+
_CANDIDATE_FIELDS = {"candidate_id","dense_rank","dense_score","reranker_logit","reranker_rank",
|
|
29
|
+
"fusion_score","fusion_rank","dedup_decision","post_dedup_rank","gate_decision","gate_threshold","final_rank"}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _require(ok, message):
|
|
33
|
+
if not ok: raise ContractError(message)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _detach(value):
|
|
37
|
+
if isinstance(value,Mapping):
|
|
38
|
+
_require(all(type(key) is str for key in value),"JSON keys must be strings")
|
|
39
|
+
value={key:_detach(child) for key,child in value.items()}
|
|
40
|
+
elif isinstance(value,(list,tuple)):
|
|
41
|
+
value=[_detach(child) for child in value]
|
|
42
|
+
return strict_json(canonical_json(value))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _number(value):
|
|
46
|
+
try:return type(value) in (int,float) and math.isfinite(value)
|
|
47
|
+
except OverflowError:return False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def build_pipeline_request(task, policy):
|
|
51
|
+
"""The request contains input texts/identities, never qrels or evidence truth."""
|
|
52
|
+
pool = [{key:row[key] for key in ("candidate_id","input","input_sha256","dense_rank","dense_score")}
|
|
53
|
+
for row in task["candidates"]]
|
|
54
|
+
body = {"schema_version":"sphynx.rag.pipeline.request.v1", "case_id":task["task_id"],
|
|
55
|
+
"customer_id":task["customer_id"], "query":task["query"],
|
|
56
|
+
"conversation_context":task["conversation_context"], "candidates":pool, "policy":policy}
|
|
57
|
+
if policy.get("boundary")=="composed_source_documents":
|
|
58
|
+
from .chunking import _documents
|
|
59
|
+
body["documents"]=_documents(task)
|
|
60
|
+
body["input_digest"] = value_digest(body)
|
|
61
|
+
return _detach(body)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def validate_pipeline_response(response, request):
|
|
65
|
+
fields={"schema_version","case_id","input_digest","construction","embedding","candidates","latency_state","latency_ms"}
|
|
66
|
+
composed=request["policy"].get("boundary")=="composed_source_documents"
|
|
67
|
+
if composed:fields.add("stages")
|
|
68
|
+
_require(type(response) is dict and set(response)==fields, "pipeline response fields differ")
|
|
69
|
+
_require(response["schema_version"]==("sphynx.rag.pipeline.composed.response.v1" if composed else "sphynx.rag.pipeline.response.v1")
|
|
70
|
+
and response["case_id"]==request["case_id"] and response["input_digest"]==request["input_digest"],
|
|
71
|
+
"pipeline response does not bind its request")
|
|
72
|
+
_require(type(response["construction"]) is dict and type(response["embedding"]) is dict,
|
|
73
|
+
"pipeline construction/embedding traces are required")
|
|
74
|
+
candidates=response["candidates"]
|
|
75
|
+
_require(type(candidates) is list and all(type(row) is dict and set(row)==_CANDIDATE_FIELDS for row in candidates),
|
|
76
|
+
"pipeline must return the exact label-free candidate trace")
|
|
77
|
+
if composed:
|
|
78
|
+
from ._composed_pipeline import validate_chunks,compose_pipeline
|
|
79
|
+
expected=validate_chunks(response["construction"].get("chunks"),request["documents"])
|
|
80
|
+
_require(type(response["stages"]) is dict and set(response["stages"])=={"chunker","embedder","retriever","reranker","fusion","dedup","gate"},"composed stages are incomplete")
|
|
81
|
+
# Reconstruct only the retained transformation, never execute adapters.
|
|
82
|
+
derived=compose_pipeline(request,lambda kind,operation,payload:deepcopy(response["stages"][kind]))
|
|
83
|
+
derived["latency_state"]=response["latency_state"]
|
|
84
|
+
_require(_detach(derived)==response,"composed stage outputs differ from the retained final trace")
|
|
85
|
+
else:expected={row["candidate_id"]:row for row in request["candidates"]}
|
|
86
|
+
ids=[row["candidate_id"] for row in candidates]
|
|
87
|
+
_require(len(ids)==len(set(ids)) and set(ids)==set(expected),"pipeline candidate membership changed")
|
|
88
|
+
for row in candidates:
|
|
89
|
+
source=expected[row["candidate_id"]]
|
|
90
|
+
_require(composed or row["dense_rank"]==source["dense_rank"] and row["dense_score"]==source["dense_score"],
|
|
91
|
+
"pipeline N11 dense input changed")
|
|
92
|
+
for key in ("dense_score","reranker_logit","fusion_score","gate_threshold"):
|
|
93
|
+
_require(_number(row[key]),"pipeline score must be finite")
|
|
94
|
+
for key in ("dense_rank","reranker_rank","fusion_rank"):
|
|
95
|
+
_require(type(row[key]) is int and row[key]>=1,"pipeline ranks must be positive integers")
|
|
96
|
+
for key in ("post_dedup_rank","final_rank"):
|
|
97
|
+
_require(row[key] is None or type(row[key]) is int and row[key]>=1,"pipeline optional ranks differ")
|
|
98
|
+
_require(row["dedup_decision"] in ("keep","drop") and row["gate_decision"] in ("pass","reject"),
|
|
99
|
+
"pipeline decisions differ")
|
|
100
|
+
if row["dedup_decision"]=="drop":
|
|
101
|
+
_require(row["post_dedup_rank"] is None and row["gate_decision"]=="reject" and row["final_rank"] is None,
|
|
102
|
+
"deduplicated candidates cannot reenter later stages")
|
|
103
|
+
else:
|
|
104
|
+
_require(row["post_dedup_rank"] is not None,"kept candidates require post-dedup ranks")
|
|
105
|
+
_require(row["final_rank"] is None or row["gate_decision"]=="pass", "gated candidates cannot reenter N17")
|
|
106
|
+
_require(response["latency_state"] in ("cold","warm"),"pipeline latency state differs")
|
|
107
|
+
_require(type(response["latency_ms"]) is dict and set(response["latency_ms"])<=
|
|
108
|
+
{"retrieval","reranker","fusion","dedup","gate","total","chunking","embedding"},"pipeline timing fields differ")
|
|
109
|
+
_require(all(_number(value) and value>=0 for value in response["latency_ms"].values()),"pipeline timings must be finite/nonnegative")
|
|
110
|
+
# Structural core validation uses neutral labels only; real labels enter later.
|
|
111
|
+
neutral={**deepcopy(response),"query_id":request["case_id"],"answerable":False,
|
|
112
|
+
"input_contract":{"candidate_count":len(candidates),"identity_sha256":request["input_digest"]}}
|
|
113
|
+
neutral["candidates"]=[{**row,"qrel":0,"evidence_group_ids":[]} for row in candidates]
|
|
114
|
+
evaluate_level2_run({"controlled_config":request["policy"]["controlled_config"],"queries":[neutral]})
|
|
115
|
+
kept=sorted((row for row in candidates if row["dedup_decision"]=="keep"),key=lambda row:row["post_dedup_rank"])
|
|
116
|
+
_require([row["post_dedup_rank"] for row in kept]==list(range(1,len(kept)+1)),"post-dedup ranks are not contiguous")
|
|
117
|
+
return _detach(response)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _policy(dataset, source_digest, candidate, *, boundary="frozen_n11", **options):
|
|
121
|
+
_require(boundary in ("frozen_n11","composed_source_documents"),"pipeline execution boundary differs")
|
|
122
|
+
policy=_policy_for_source(dataset,source_digest,**options)
|
|
123
|
+
controlled=candidate.parameters.get("controlled_config")
|
|
124
|
+
_require(isinstance(controlled,Mapping) and type(controlled.get("top_k")) is int
|
|
125
|
+
and 1<=controlled["top_k"]<=50,"candidate needs a bounded controlled_config.top_k")
|
|
126
|
+
policy.update(version=POLICY_VERSION,controlled_config=_detach(controlled),
|
|
127
|
+
boundary=boundary,
|
|
128
|
+
execution_mode=candidate.execution_mode,timing_authority="component_reported_unattested",
|
|
129
|
+
missing_label_policy="unavailable_not_zero")
|
|
130
|
+
return policy
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _identity(dataset, digest, tasks, policy):
|
|
134
|
+
# Controlled component values are captured separately. The comparison gate
|
|
135
|
+
# explicitly verifies every changed path rather than hiding them in one key.
|
|
136
|
+
fixed={key:value for key,value in policy.items() if key!="controlled_config"}
|
|
137
|
+
return ComparisonIdentity("rag_pipeline",PACK_ID,"v1","sable.rag.dataset.v3",digest,
|
|
138
|
+
value_digest([task["task_id"] for task in tasks]),
|
|
139
|
+
value_digest([{key:task[key] for key in ("task_id","answerability","candidates","evidence_groups")} for task in tasks]),
|
|
140
|
+
value_digest(fixed),METRIC_RELEASE,GRADER_RELEASE,
|
|
141
|
+
policy["boundary_digest"] or value_digest({"boundary":"N11-N17", "inputs":digest}))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def run_pipeline(candidate, adapter, dataset_path, output_dir, *, expected_sha256,
|
|
145
|
+
synthetic=False, owner_authorized=False, pack_manifest_path=None,
|
|
146
|
+
trials=1, attempts=1, case_ids=None, boundary_digest=None,boundary="frozen_n11"):
|
|
147
|
+
_require(isinstance(candidate,CandidateConfiguration) and candidate.interface_version==INTERFACE,
|
|
148
|
+
"pipeline candidate interface differs")
|
|
149
|
+
_require(candidate.execution_mode in ("trusted_local","recorded"),"pipeline execution mode requires an explicit adapter")
|
|
150
|
+
dataset=load_dataset_v3(dataset_path,expected_sha256=expected_sha256)
|
|
151
|
+
policy=_policy(dataset,expected_sha256,candidate,synthetic=synthetic,owner_authorized=owner_authorized,
|
|
152
|
+
pack_manifest_path=pack_manifest_path,trials=trials,attempts=attempts,boundary_digest=boundary_digest,boundary=boundary)
|
|
153
|
+
tasks=_select_tasks(dataset,case_ids)
|
|
154
|
+
if candidate.execution_mode=="recorded":
|
|
155
|
+
_require(isinstance(adapter,Mapping) and candidate.parameters.get("recordings_digest")==value_digest(adapter),
|
|
156
|
+
"pipeline replay requires the complete pinned recording")
|
|
157
|
+
adapter=_detach(adapter)
|
|
158
|
+
else: _require(callable(adapter),"pipeline adapter must be callable")
|
|
159
|
+
observed={"approved_manifest_source":None if synthetic else Path(pack_manifest_path).read_text(encoding="utf-8")}
|
|
160
|
+
_verify_approval(dataset,expected_sha256,policy,observed)
|
|
161
|
+
identity=_identity(dataset,expected_sha256,tasks,policy)
|
|
162
|
+
ids=[task["task_id"] for task in tasks]
|
|
163
|
+
directory=create_run(output_dir,identity,candidate,ids)
|
|
164
|
+
captures=[]
|
|
165
|
+
for index,task in enumerate(tasks):
|
|
166
|
+
request=build_pipeline_request(task,policy)
|
|
167
|
+
for trial in range(1,trials+1):
|
|
168
|
+
for attempt in range(1,attempts+1):
|
|
169
|
+
prefix=f"cases/{index}/{trial}/{attempt}/"
|
|
170
|
+
req=write_artifact(directory,prefix+"request.json",request)
|
|
171
|
+
response=None;failure=None;begin=perf_counter()
|
|
172
|
+
try:
|
|
173
|
+
raw=adapter[value_digest(request)] if candidate.execution_mode=="recorded" else adapter(_detach(request))
|
|
174
|
+
response=write_artifact(directory,prefix+"response.json",raw)
|
|
175
|
+
validate_pipeline_response(raw,request)
|
|
176
|
+
except Exception:
|
|
177
|
+
failure="invalid_response" if response is not None else "candidate_failure"
|
|
178
|
+
observation=write_artifact(directory,prefix+"observed.json",observed)
|
|
179
|
+
captures.append(TrackCaseCapture(task["task_id"],trial,attempt,req,response,observation,failure,
|
|
180
|
+
(perf_counter()-begin)*1000))
|
|
181
|
+
return finish_run(directory,identity,candidate,ids,captures)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _verified(run,dataset_path,digest):
|
|
185
|
+
dataset=load_dataset_v3(dataset_path,expected_sha256=digest)
|
|
186
|
+
manifest=verify_track_run(run)
|
|
187
|
+
_require(manifest.candidate.interface_version==INTERFACE,"capture is not a pipeline experiment")
|
|
188
|
+
tasks=_select_tasks(dataset,manifest.case_ids)
|
|
189
|
+
policy=read_artifact(run.path,manifest.captures[0].request).get("policy")
|
|
190
|
+
_require(type(policy) is dict and set(policy)=={"version","synthetic","approved_manifest_digest","trials","attempts","selection","boundary_digest",
|
|
191
|
+
"controlled_config","execution_mode","timing_authority","missing_label_policy","boundary"} and policy.get("version")==POLICY_VERSION and
|
|
192
|
+
policy.get("execution_mode")==manifest.candidate.execution_mode,"captured pipeline policy differs")
|
|
193
|
+
_require(type(policy["synthetic"]) is bool and policy["selection"]=="first_success_per_trial"
|
|
194
|
+
and policy["timing_authority"]=="component_reported_unattested" and policy["missing_label_policy"]=="unavailable_not_zero"
|
|
195
|
+
and policy["controlled_config"]==_detach(manifest.candidate.parameters["controlled_config"])
|
|
196
|
+
and policy["boundary"] in ("frozen_n11","composed_source_documents"),"pipeline policy is not the frozen declared policy")
|
|
197
|
+
_require(all(type(policy[key]) is int and policy[key]>=1 for key in ("trials","attempts")),"pipeline trial/attempt policy differs")
|
|
198
|
+
expected=_policy(dataset,digest,manifest.candidate,synthetic=policy.get("synthetic"),
|
|
199
|
+
owner_authorized=True,pack_manifest_path=None,trials=policy.get("trials"),attempts=policy.get("attempts"),
|
|
200
|
+
boundary_digest=policy.get("boundary_digest"),boundary=policy["boundary"]) if policy.get("synthetic") else None
|
|
201
|
+
if expected is not None:_require(policy==expected,"pipeline policy shape or values changed")
|
|
202
|
+
require_comparable(_identity(dataset,digest,tasks,policy),manifest.identity)
|
|
203
|
+
requests={task["task_id"]:build_pipeline_request(task,policy) for task in tasks}
|
|
204
|
+
verify_track_run(run,expected_case_ids=manifest.case_ids,expected_requests=requests,
|
|
205
|
+
trials=policy["trials"],attempts=policy["attempts"])
|
|
206
|
+
for capture in manifest.captures:
|
|
207
|
+
_require(capture.observed is not None,"pipeline approval observation is missing")
|
|
208
|
+
_verify_approval(dataset,digest,policy,read_artifact(run.path,capture.observed))
|
|
209
|
+
return manifest,tasks,policy,requests
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _score(task,response,request):
|
|
213
|
+
response=validate_pipeline_response(response,request)
|
|
214
|
+
labels={row["candidate_id"]:row for row in task["candidates"]}
|
|
215
|
+
if request["policy"]["boundary"]=="composed_source_documents":
|
|
216
|
+
from ._composed_pipeline import validate_chunks,truth_for_chunks
|
|
217
|
+
chunks=validate_chunks(response["construction"]["chunks"],request["documents"])
|
|
218
|
+
labels=truth_for_chunks(task,chunks)
|
|
219
|
+
if labels is None:return None,len(task["candidates"])
|
|
220
|
+
missing=sum(row.get("qrel") is None or row.get("judgment_status")!="judged" for row in labels.values())
|
|
221
|
+
if missing:return None,missing
|
|
222
|
+
query={key:response[key] for key in ("construction","embedding","latency_state","latency_ms")}
|
|
223
|
+
query.update(query_id=task["task_id"],answerable=task["answerability"]=="answerable",
|
|
224
|
+
input_contract={"candidate_count":len(labels),"identity_sha256":request["input_digest"]},
|
|
225
|
+
candidates=[{**row,"qrel":labels[row["candidate_id"]]["qrel"],
|
|
226
|
+
"evidence_group_ids":labels[row["candidate_id"]]["evidence_group_ids"]} for row in response["candidates"]])
|
|
227
|
+
score=evaluate_level2_run({"controlled_config":request["policy"]["controlled_config"],"queries":[query]})
|
|
228
|
+
# Keep the canonical core's diagnostic output, but do not promote its legacy
|
|
229
|
+
# zero-on-empty aggregate into a defined ranking score for an unsupported pool.
|
|
230
|
+
metric_eligibility={"ndcg_at_5":any(row["qrel"]>0 for row in task["candidates"]),
|
|
231
|
+
"recall_at_5":any(row["qrel"]>=2 for row in task["candidates"]),
|
|
232
|
+
"mrr":any(row["qrel"]>=2 for row in task["candidates"])}
|
|
233
|
+
score["metric_eligibility"]=metric_eligibility
|
|
234
|
+
if not all(metric_eligibility.values()):
|
|
235
|
+
score["source_stage_metrics"]=_detach(score["stage_metrics"])
|
|
236
|
+
for stage in STAGES:
|
|
237
|
+
for key,eligible in metric_eligibility.items():
|
|
238
|
+
if not eligible:score["stage_metrics"][stage][key]=None
|
|
239
|
+
score["ranking_status"]="partially_eligible_no_strong_reference" if any(metric_eligibility.values()) else "undefined_no_positive_reference"
|
|
240
|
+
else:score["ranking_status"]="measured"
|
|
241
|
+
return score,0
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _grade_metrics(run,dataset_path,expected_sha256):
|
|
245
|
+
manifest,tasks,policy,requests=_verified(run,dataset_path,expected_sha256)
|
|
246
|
+
rows=[]
|
|
247
|
+
for task in tasks:
|
|
248
|
+
for trial in range(1,policy["trials"]+1):
|
|
249
|
+
score=None;selected=None;missing=0
|
|
250
|
+
for capture in sorted(manifest.captures,key=lambda row:row.attempt_index):
|
|
251
|
+
if capture.case_id!=task["task_id"] or capture.trial_index!=trial or capture.failure is not None:continue
|
|
252
|
+
_require(capture.response is not None,"successful pipeline capture has no response")
|
|
253
|
+
value,absent=_score(task,read_artifact(run.path,capture.response),requests[task["task_id"]])
|
|
254
|
+
if selected is None:score,missing,selected=value,absent,capture
|
|
255
|
+
rows.append({"case_id":task["task_id"],"trial_index":trial,"metrics":score,
|
|
256
|
+
"status":"failed" if selected is None else "labels_missing" if missing else "measured",
|
|
257
|
+
"missing_label_count":missing,"runner_wall_ms":None if selected is None else selected.runner_latency_ms})
|
|
258
|
+
good=[row["metrics"] for row in rows if row["metrics"] is not None]
|
|
259
|
+
def aggregate(stage,key):
|
|
260
|
+
values=[row["stage_metrics"][stage][key] for row in good if row["stage_metrics"][stage][key] is not None]
|
|
261
|
+
return None if len(good)!=len(rows) or not values else sum(values)/len(values)
|
|
262
|
+
stage={stage:{key:aggregate(stage,key) for key in ("candidate_count","recall_at_5","ndcg_at_5","mrr")} for stage in STAGES}
|
|
263
|
+
undefined=sum(row["metrics"] is not None and row["metrics"]["ranking_status"]=="undefined_no_positive_reference" for row in rows)
|
|
264
|
+
undefined_by_metric={key:sum(not row["metric_eligibility"][key] for row in good) for key in ("recall_at_5","ndcg_at_5","mrr")}
|
|
265
|
+
accounting={stage:{key:{"eligible_count":len(rows)-undefined_by_metric.get(key,0),
|
|
266
|
+
"observed_count":sum(row["stage_metrics"][stage][key] is not None for row in good),
|
|
267
|
+
"missing_count":len(rows)-len(good),"undefined_count":undefined_by_metric.get(key,0)}
|
|
268
|
+
for key in ("candidate_count","recall_at_5","ndcg_at_5","mrr")} for stage in STAGES}
|
|
269
|
+
return manifest,{"stage_metrics":stage,"cases":rows,
|
|
270
|
+
"stage_metric_accounting":accounting,"ranking_eligible_count":len(rows)-undefined,
|
|
271
|
+
"ranking_fully_eligible_count":sum(all(row["metric_eligibility"].values()) for row in good),
|
|
272
|
+
"ranking_undefined_no_positive_count":undefined,"quality_complete_meaning":"all_capture_and_reference_rows_available",
|
|
273
|
+
"coverage":sum(row["status"]!="failed" for row in rows)/len(rows),"quality_complete":len(good)==len(rows),
|
|
274
|
+
"quality_observed_count":len(good),"quality_missing_count":len(rows)-len(good),
|
|
275
|
+
"quality_scope":"synthetic_fixture" if policy["synthetic"] else "owner_approved",
|
|
276
|
+
"execution_mode":policy["execution_mode"],"timing_authority":"component_reported_unattested",
|
|
277
|
+
"runner_timing_scope":"replay_adapter_wall" if policy["execution_mode"]=="recorded" else "trusted_local_adapter_wall",
|
|
278
|
+
"controlled_config":policy["controlled_config"],"boundary":policy["boundary"]}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def grade_pipeline(run,dataset_path,*,expected_sha256):
|
|
282
|
+
manifest,metrics=_grade_metrics(run,dataset_path,expected_sha256)
|
|
283
|
+
return write_evaluation(run,manifest.identity,metrics)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def report_pipeline(evaluation,dataset_path,*,expected_sha256):
|
|
287
|
+
_,expected=_grade_metrics(evaluation.run,dataset_path,expected_sha256)
|
|
288
|
+
report=verify_track_evaluation(evaluation).to_dict()
|
|
289
|
+
_require(report["metrics"]==expected,"pipeline evaluation differs from its retained captures/references")
|
|
290
|
+
return report
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def compare_pipeline(baseline,candidate,dataset_path,*,expected_sha256,declared_paths=()):
|
|
294
|
+
left=report_pipeline(baseline,dataset_path,expected_sha256=expected_sha256)
|
|
295
|
+
right=report_pipeline(candidate,dataset_path,expected_sha256=expected_sha256)
|
|
296
|
+
return _compare_report_data(left,right,declared_paths=declared_paths)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _compare_report_data(left,right,*,declared_paths=()):
|
|
300
|
+
"""Native comparison arithmetic over already verified reports; no I/O/grant."""
|
|
301
|
+
require_comparable(ComparisonIdentity.from_dict(left["identity"]),ComparisonIdentity.from_dict(right["identity"]))
|
|
302
|
+
a,b=left["metrics"],right["metrics"]
|
|
303
|
+
changed=None
|
|
304
|
+
if a["controlled_config"]!=b["controlled_config"]:
|
|
305
|
+
changed=validate_controlled_change(a["controlled_config"],b["controlled_config"],declared_paths)
|
|
306
|
+
else:_require(not declared_paths,"declared pipeline change did not occur")
|
|
307
|
+
_require([(r["case_id"],r["trial_index"]) for r in a["cases"]]==[(r["case_id"],r["trial_index"]) for r in b["cases"]],"pipeline paired cohort differs")
|
|
308
|
+
delta={stage:{key:None if a["stage_metrics"][stage][key] is None or b["stage_metrics"][stage][key] is None
|
|
309
|
+
else b["stage_metrics"][stage][key]-a["stage_metrics"][stage][key] for key in a["stage_metrics"][stage]} for stage in STAGES}
|
|
310
|
+
return {"schema_version":"sphynx.rag.pipeline.comparison.v1","baseline":left["evaluation_id"],
|
|
311
|
+
"candidate":right["evaluation_id"],"changed_component":changed,"delta":delta,
|
|
312
|
+
"direction":"candidate_minus_baseline","timing_comparable":False,
|
|
313
|
+
"notice":"Component timings are caller-reported diagnostics; local execution is not attested."}
|