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/catalog.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""Content-free discovery contracts and independently owned catalog fragments.
|
|
2
|
+
|
|
3
|
+
Discovery imports only descriptor fragments, never adapter constructors. Fragment
|
|
4
|
+
modules contain DATASETS and/or CANDIDATES tuples and import no optional model or
|
|
5
|
+
provider dependency. Readiness describes an approved release; caller-local files,
|
|
6
|
+
models, credentials and permission are still checked by execution preflight.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from importlib import import_module
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from .contracts import ContractError, _finite_number, _id, _text, _tuple_of_ids
|
|
15
|
+
from .integration_contracts import (
|
|
16
|
+
CONFIGURATION_SCHEMA, ScoreField, WireValue, _choice, _count,
|
|
17
|
+
_optional_digest, _schema, _sequence,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
DATASET_DESCRIPTOR_SCHEMA = "sphynx.dataset-descriptor.v1"
|
|
21
|
+
CANDIDATE_DESCRIPTOR_SCHEMA = "sphynx.candidate-descriptor.v1"
|
|
22
|
+
# Integration ownership is fixed here so workers add their own data-only module.
|
|
23
|
+
CATALOG_PROVIDER_MODULES = (
|
|
24
|
+
"sphynx.dataset_catalog_data", # I-02 dataset release descriptors
|
|
25
|
+
"sphynx.invocation_catalog_data", # I-03 routing candidates
|
|
26
|
+
"sphynx.model_catalog_data", # I-04 optional/recorded model candidates
|
|
27
|
+
"sphynx.learned_catalog_data", # I-05 learned candidates
|
|
28
|
+
"sphynx.statistical_catalog_data", # I-06 statistical/guarded candidates
|
|
29
|
+
"sphynx.retrieval_catalog_data", # I-07 retrieval candidates
|
|
30
|
+
"sphynx.workbench_catalog_data", # I-10/I-11/I-12 via integration owner
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _texts(value: Any, field: str) -> tuple[str, ...]:
|
|
35
|
+
values = _sequence(value, str, field)
|
|
36
|
+
for item in values:
|
|
37
|
+
_text(item, field)
|
|
38
|
+
if len(set(values)) != len(values):
|
|
39
|
+
raise ContractError(f"{field} must contain unique values")
|
|
40
|
+
return values
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class DatasetRelationship(WireValue):
|
|
45
|
+
dataset_id: str
|
|
46
|
+
kind: str
|
|
47
|
+
case_count: int | None = None
|
|
48
|
+
family_digest: str | None = None
|
|
49
|
+
|
|
50
|
+
def __post_init__(self) -> None:
|
|
51
|
+
_id(self.dataset_id, "related dataset_id")
|
|
52
|
+
_choice(self.kind, ("subset_of", "revised_from", "repeated_question", "shared_family", "shared_source", "supersedes"), "relationship kind")
|
|
53
|
+
if self.case_count is not None:
|
|
54
|
+
_count(self.case_count, "relationship case_count")
|
|
55
|
+
_optional_digest(self.family_digest, "relationship family_digest")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class DatasetDescriptor(WireValue):
|
|
60
|
+
dataset_id: str
|
|
61
|
+
pack_id: str
|
|
62
|
+
release: str
|
|
63
|
+
track: str
|
|
64
|
+
dataset_schema_version: str
|
|
65
|
+
source_digest: str | None
|
|
66
|
+
reference_digest: str | None
|
|
67
|
+
population: str
|
|
68
|
+
split: str
|
|
69
|
+
case_count: int | None
|
|
70
|
+
relevance_threshold: float | None
|
|
71
|
+
review_status: str
|
|
72
|
+
state: str
|
|
73
|
+
reason: str | None = None
|
|
74
|
+
source_case_count: int | None = None
|
|
75
|
+
candidate_depth: int | None = None
|
|
76
|
+
first_stage: str | None = None
|
|
77
|
+
family_status: str = "unknown"
|
|
78
|
+
family_digest: str | None = None
|
|
79
|
+
repeated_question_status: str = "unknown"
|
|
80
|
+
relationships: tuple[DatasetRelationship, ...] = ()
|
|
81
|
+
authorization_requirements: tuple[str, ...] = ()
|
|
82
|
+
local_prerequisites: tuple[str, ...] = ()
|
|
83
|
+
implementation_status: str = "pending"
|
|
84
|
+
previously_exercised: bool | None = None
|
|
85
|
+
superseded_by: tuple[str, ...] = ()
|
|
86
|
+
schema_version: str = DATASET_DESCRIPTOR_SCHEMA
|
|
87
|
+
|
|
88
|
+
def __post_init__(self) -> None:
|
|
89
|
+
_schema(self.schema_version, DATASET_DESCRIPTOR_SCHEMA)
|
|
90
|
+
for name in ("dataset_id", "pack_id", "release", "track", "dataset_schema_version", "population", "split", "review_status"):
|
|
91
|
+
_id(getattr(self, name), name)
|
|
92
|
+
for name in ("source_digest", "reference_digest", "family_digest"):
|
|
93
|
+
_optional_digest(getattr(self, name), name)
|
|
94
|
+
for name in ("case_count", "source_case_count", "candidate_depth"):
|
|
95
|
+
if getattr(self, name) is not None:
|
|
96
|
+
_count(getattr(self, name), name, 1 if name == "candidate_depth" else 0)
|
|
97
|
+
if self.source_case_count is not None and self.case_count is not None and self.source_case_count < self.case_count:
|
|
98
|
+
raise ContractError("source_case_count must include the selected cohort")
|
|
99
|
+
if self.relevance_threshold is not None:
|
|
100
|
+
object.__setattr__(self, "relevance_threshold", _finite_number(self.relevance_threshold, "relevance_threshold"))
|
|
101
|
+
if self.first_stage is not None:
|
|
102
|
+
_id(self.first_stage, "first_stage")
|
|
103
|
+
_choice(self.state, ("ready", "blocked", "superseded"), "dataset state")
|
|
104
|
+
_choice(self.implementation_status, ("available", "pending", "external_import_required"), "implementation_status")
|
|
105
|
+
_choice(self.family_status, ("known", "unknown", "not_applicable"), "family_status")
|
|
106
|
+
_choice(self.repeated_question_status, ("known", "unknown", "not_applicable"), "repeated_question_status")
|
|
107
|
+
if self.state == "ready" and (self.source_digest is None or self.reference_digest is None or self.implementation_status != "available"):
|
|
108
|
+
raise ContractError("ready requires exact source/reference identity and an available loader")
|
|
109
|
+
if self.state != "ready" and self.reason is None:
|
|
110
|
+
raise ContractError("blocked or superseded datasets require an exact reason")
|
|
111
|
+
if self.reason is not None:
|
|
112
|
+
_text(self.reason, "reason")
|
|
113
|
+
if self.family_status == "known" and self.family_digest is None:
|
|
114
|
+
raise ContractError("known family relationships require a family digest")
|
|
115
|
+
relationships = _sequence(self.relationships, DatasetRelationship, "relationships")
|
|
116
|
+
if any(item.dataset_id == self.dataset_id for item in relationships):
|
|
117
|
+
raise ContractError("dataset relationships must not reference themselves")
|
|
118
|
+
keys = [(item.dataset_id, item.kind) for item in relationships]
|
|
119
|
+
if len(set(keys)) != len(keys):
|
|
120
|
+
raise ContractError("dataset relationships must be unique")
|
|
121
|
+
object.__setattr__(self, "relationships", relationships)
|
|
122
|
+
for name in ("authorization_requirements", "local_prerequisites"):
|
|
123
|
+
object.__setattr__(self, name, _texts(getattr(self, name), name))
|
|
124
|
+
object.__setattr__(self, "superseded_by", _tuple_of_ids(self.superseded_by, "superseded_by"))
|
|
125
|
+
if self.state == "superseded" and not self.superseded_by:
|
|
126
|
+
raise ContractError("superseded state requires distinct replacement dataset IDs")
|
|
127
|
+
if self.dataset_id in self.superseded_by or (self.state != "superseded" and self.superseded_by):
|
|
128
|
+
raise ContractError("invalid superseded_by identity")
|
|
129
|
+
if self.previously_exercised is not None and type(self.previously_exercised) is not bool:
|
|
130
|
+
raise ContractError("previously_exercised must be boolean or unknown/null")
|
|
131
|
+
|
|
132
|
+
@classmethod
|
|
133
|
+
def from_dict(cls, value):
|
|
134
|
+
from collections.abc import Mapping
|
|
135
|
+
checked = cls._checked_fields(value)
|
|
136
|
+
if isinstance(checked, dict) and "relationships" in checked:
|
|
137
|
+
checked["relationships"] = tuple(DatasetRelationship.from_dict(item) for item in _sequence(checked["relationships"], Mapping, "relationships"))
|
|
138
|
+
return super().from_dict(checked)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass(frozen=True)
|
|
142
|
+
class CandidateDescriptor(WireValue):
|
|
143
|
+
candidate_id: str
|
|
144
|
+
track: str
|
|
145
|
+
interface_version: str
|
|
146
|
+
source_digest: str | None
|
|
147
|
+
supported_dataset_ids: tuple[str, ...]
|
|
148
|
+
execution_modes: tuple[str, ...]
|
|
149
|
+
implementation_status: str
|
|
150
|
+
reason: str | None = None
|
|
151
|
+
local_prerequisites: tuple[str, ...] = ()
|
|
152
|
+
dependencies: tuple[str, ...] = ()
|
|
153
|
+
score_fields: tuple[ScoreField, ...] = ()
|
|
154
|
+
model_identity_required: bool = False
|
|
155
|
+
supported_python: tuple[str, ...] = ()
|
|
156
|
+
configuration_schema_version: str = CONFIGURATION_SCHEMA
|
|
157
|
+
schema_version: str = CANDIDATE_DESCRIPTOR_SCHEMA
|
|
158
|
+
|
|
159
|
+
def __post_init__(self) -> None:
|
|
160
|
+
_schema(self.schema_version, CANDIDATE_DESCRIPTOR_SCHEMA)
|
|
161
|
+
_schema(self.configuration_schema_version, CONFIGURATION_SCHEMA)
|
|
162
|
+
for name in ("candidate_id", "track", "interface_version"):
|
|
163
|
+
_id(getattr(self, name), name)
|
|
164
|
+
_optional_digest(self.source_digest, "source_digest")
|
|
165
|
+
_choice(self.implementation_status, ("available", "pending", "blocked"), "implementation_status")
|
|
166
|
+
if self.implementation_status == "available" and self.source_digest is None:
|
|
167
|
+
raise ContractError("available candidates require a source identity")
|
|
168
|
+
if self.implementation_status != "available" and self.reason is None:
|
|
169
|
+
raise ContractError("pending/blocked candidates require a reason")
|
|
170
|
+
if self.reason is not None:
|
|
171
|
+
_text(self.reason, "reason")
|
|
172
|
+
object.__setattr__(self, "supported_dataset_ids", _tuple_of_ids(self.supported_dataset_ids, "supported_dataset_ids"))
|
|
173
|
+
modes = _texts(self.execution_modes, "execution_modes")
|
|
174
|
+
if not modes:
|
|
175
|
+
raise ContractError("execution_modes must be nonempty")
|
|
176
|
+
for mode in modes:
|
|
177
|
+
_choice(mode, ("recorded", "trusted_local", "external_command", "live_provider"), "execution mode")
|
|
178
|
+
object.__setattr__(self, "execution_modes", modes)
|
|
179
|
+
for name in ("local_prerequisites", "dependencies", "supported_python"):
|
|
180
|
+
object.__setattr__(self, name, _texts(getattr(self, name), name))
|
|
181
|
+
scores = _sequence(self.score_fields, ScoreField, "score_fields")
|
|
182
|
+
if len({item.field_id for item in scores}) != len(scores):
|
|
183
|
+
raise ContractError("candidate score field identities must be unique")
|
|
184
|
+
object.__setattr__(self, "score_fields", scores)
|
|
185
|
+
if type(self.model_identity_required) is not bool:
|
|
186
|
+
raise ContractError("model_identity_required must be a boolean")
|
|
187
|
+
|
|
188
|
+
@classmethod
|
|
189
|
+
def from_dict(cls, value):
|
|
190
|
+
from collections.abc import Mapping
|
|
191
|
+
checked = cls._checked_fields(value)
|
|
192
|
+
if isinstance(checked, dict) and "score_fields" in checked:
|
|
193
|
+
checked["score_fields"] = tuple(ScoreField.from_dict(item) for item in _sequence(checked["score_fields"], Mapping, "score_fields"))
|
|
194
|
+
return super().from_dict(checked)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def merge_catalog_descriptors(*groups):
|
|
198
|
+
"""Merge data-only fragments; no ID may overwrite a previous release."""
|
|
199
|
+
result = []
|
|
200
|
+
seen = set()
|
|
201
|
+
for group in groups:
|
|
202
|
+
if not isinstance(group, (tuple, list)):
|
|
203
|
+
raise ContractError("catalog fragments must be descriptor sequences")
|
|
204
|
+
for item in group:
|
|
205
|
+
if isinstance(item, DatasetDescriptor):
|
|
206
|
+
key = ("dataset", item.dataset_id)
|
|
207
|
+
elif isinstance(item, CandidateDescriptor):
|
|
208
|
+
key = ("candidate", item.candidate_id)
|
|
209
|
+
else:
|
|
210
|
+
raise ContractError("catalog fragments must contain validated descriptors")
|
|
211
|
+
if key in seen:
|
|
212
|
+
raise ContractError(f"catalog identity {key[1]!r} is duplicated or substitutes a release")
|
|
213
|
+
seen.add(key)
|
|
214
|
+
result.append(item)
|
|
215
|
+
return tuple(result)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@dataclass(frozen=True)
|
|
219
|
+
class Catalog:
|
|
220
|
+
datasets: tuple[DatasetDescriptor, ...] = ()
|
|
221
|
+
candidates: tuple[CandidateDescriptor, ...] = ()
|
|
222
|
+
|
|
223
|
+
def __post_init__(self) -> None:
|
|
224
|
+
datasets = _sequence(self.datasets, DatasetDescriptor, "datasets")
|
|
225
|
+
candidates = _sequence(self.candidates, CandidateDescriptor, "candidates")
|
|
226
|
+
merge_catalog_descriptors(datasets, candidates)
|
|
227
|
+
object.__setattr__(self, "datasets", datasets)
|
|
228
|
+
object.__setattr__(self, "candidates", candidates)
|
|
229
|
+
|
|
230
|
+
def list_datasets(self, track: str | None = None) -> tuple[DatasetDescriptor, ...]:
|
|
231
|
+
return tuple(item for item in self.datasets if track is None or item.track == track)
|
|
232
|
+
|
|
233
|
+
def get_dataset(self, dataset_id: str) -> DatasetDescriptor:
|
|
234
|
+
_id(dataset_id, "dataset_id")
|
|
235
|
+
for item in self.datasets:
|
|
236
|
+
if item.dataset_id == dataset_id:
|
|
237
|
+
return item
|
|
238
|
+
raise ValueError(f"Unknown dataset {dataset_id!r}; call list_datasets() for installed release IDs")
|
|
239
|
+
|
|
240
|
+
def list_candidates(self, track: str | None = None) -> tuple[CandidateDescriptor, ...]:
|
|
241
|
+
return tuple(item for item in self.candidates if track is None or item.track == track)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _installed_catalog() -> Catalog:
|
|
245
|
+
groups = []
|
|
246
|
+
for name in CATALOG_PROVIDER_MODULES:
|
|
247
|
+
try:
|
|
248
|
+
module = import_module(name)
|
|
249
|
+
except ModuleNotFoundError as exc:
|
|
250
|
+
if exc.name != name:
|
|
251
|
+
raise # An installed provider with a missing dependency must fail visibly.
|
|
252
|
+
continue
|
|
253
|
+
groups.append(getattr(module, "DATASETS", ()))
|
|
254
|
+
groups.append(getattr(module, "CANDIDATES", ()))
|
|
255
|
+
descriptors = merge_catalog_descriptors(*groups)
|
|
256
|
+
return Catalog(
|
|
257
|
+
tuple(item for item in descriptors if isinstance(item, DatasetDescriptor)),
|
|
258
|
+
tuple(item for item in descriptors if isinstance(item, CandidateDescriptor)),
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def list_datasets(track: str | None = None) -> tuple[DatasetDescriptor, ...]:
|
|
263
|
+
"""List installed content-free releases, without data/model/provider execution."""
|
|
264
|
+
return _installed_catalog().list_datasets(track)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def get_dataset(dataset_id: str) -> DatasetDescriptor:
|
|
268
|
+
return _installed_catalog().get_dataset(dataset_id)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def list_candidates(track: str | None = None) -> tuple[CandidateDescriptor, ...]:
|
|
272
|
+
return _installed_catalog().list_candidates(track)
|
sphynx/chunking.py
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"""Sphynx-owned port of the source contracts; no sibling runtime dependency."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import Any
|
|
4
|
+
JsonValue = Any
|
|
5
|
+
|
|
6
|
+
import hashlib, math, re
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from collections.abc import Mapping, Sequence
|
|
9
|
+
from typing import cast
|
|
10
|
+
DEPTHS = (5, 10, 15, 30)
|
|
11
|
+
class Level1Error(ValueError):
|
|
12
|
+
"""Raised when a Level 1 evaluation is not causally comparable."""
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class Chunk:
|
|
16
|
+
chunk_id: str
|
|
17
|
+
document_id: str
|
|
18
|
+
start: int
|
|
19
|
+
end: int
|
|
20
|
+
text: str
|
|
21
|
+
|
|
22
|
+
def _percentile(values: Sequence[float], fraction: float) -> float | None:
|
|
23
|
+
if not values:
|
|
24
|
+
return None
|
|
25
|
+
ordered = sorted(values)
|
|
26
|
+
position = (len(ordered) - 1) * fraction
|
|
27
|
+
lower = math.floor(position)
|
|
28
|
+
upper = math.ceil(position)
|
|
29
|
+
if lower == upper:
|
|
30
|
+
return ordered[lower]
|
|
31
|
+
return ordered[lower] + (ordered[upper] - ordered[lower]) * (position - lower)
|
|
32
|
+
|
|
33
|
+
def _distribution(values: Sequence[float]) -> dict[str, float | int | None]:
|
|
34
|
+
return {'count': len(values), 'min': min(values) if values else None, 'mean': sum(values) / len(values) if values else None, 'p50': _percentile(values, 0.5), 'p95': _percentile(values, 0.95), 'max': max(values) if values else None}
|
|
35
|
+
|
|
36
|
+
def _token_spans(text: str) -> tuple[tuple[int, int], ...]:
|
|
37
|
+
return tuple(((match.start(), match.end()) for match in re.finditer('\\S+', text)))
|
|
38
|
+
|
|
39
|
+
def evaluate_chunking(documents: Mapping[str, str], source_spans: Sequence[Mapping[str, object]], evidence_groups: Sequence[Mapping[str, object]], chunks: Sequence[Chunk], *, expected_document_sha256: Mapping[str, str]) -> dict[str, JsonValue]:
|
|
40
|
+
"""Score a versioned chunk set against immutable source spans."""
|
|
41
|
+
for document_id, content in documents.items():
|
|
42
|
+
digest = hashlib.sha256(content.encode()).hexdigest()
|
|
43
|
+
if expected_document_sha256.get(document_id) != digest:
|
|
44
|
+
raise Level1Error(f'document {document_id} does not match its immutable hash')
|
|
45
|
+
by_document: dict[str, list[Chunk]] = {document_id: [] for document_id in documents}
|
|
46
|
+
for chunk in chunks:
|
|
47
|
+
document_content = documents.get(chunk.document_id)
|
|
48
|
+
if document_content is None or chunk.start < 0 or chunk.end <= chunk.start or (chunk.end > len(document_content)):
|
|
49
|
+
raise Level1Error(f'chunk {chunk.chunk_id} has invalid document bounds')
|
|
50
|
+
if chunk.text != document_content[chunk.start:chunk.end]:
|
|
51
|
+
raise Level1Error(f'chunk {chunk.chunk_id} text does not bind its source document')
|
|
52
|
+
by_document[chunk.document_id].append(chunk)
|
|
53
|
+
contained = 0
|
|
54
|
+
intersections: list[int] = []
|
|
55
|
+
gold_token_total = 0
|
|
56
|
+
covered_gold_tokens = 0
|
|
57
|
+
emitted_token_total = 0
|
|
58
|
+
relevant_emitted_tokens = 0
|
|
59
|
+
duplicate_positions = 0
|
|
60
|
+
relevant_chunk_ids: set[str] = set()
|
|
61
|
+
span_to_chunks: dict[str, set[str]] = {}
|
|
62
|
+
for span in source_spans:
|
|
63
|
+
document_id = cast('str', span['document_id'])
|
|
64
|
+
start, end = (cast('int', span['start']), cast('int', span['end']))
|
|
65
|
+
span_id = cast('str', span['source_span_id'])
|
|
66
|
+
overlapping = {chunk.chunk_id for chunk in by_document.get(document_id, []) if max(start, chunk.start) < min(end, chunk.end)}
|
|
67
|
+
intersections.append(len(overlapping))
|
|
68
|
+
span_to_chunks[span_id] = overlapping
|
|
69
|
+
relevant_chunk_ids.update(overlapping)
|
|
70
|
+
contained += int(any((chunk.start <= start and chunk.end >= end for chunk in by_document.get(document_id, []))))
|
|
71
|
+
tokens = _token_spans(documents[document_id])
|
|
72
|
+
gold_positions = {(token_start, token_end) for token_start, token_end in tokens if token_start < end and token_end > start}
|
|
73
|
+
gold_token_total += len(gold_positions)
|
|
74
|
+
coverage: dict[tuple[int, int], int] = {position: 0 for position in gold_positions}
|
|
75
|
+
for chunk in by_document.get(document_id, []):
|
|
76
|
+
for position in gold_positions:
|
|
77
|
+
if position[0] < chunk.end and position[1] > chunk.start:
|
|
78
|
+
coverage[position] += 1
|
|
79
|
+
covered_gold_tokens += sum((count > 0 for count in coverage.values()))
|
|
80
|
+
for document_id, document_chunks in by_document.items():
|
|
81
|
+
positions: dict[tuple[int, int], int] = {}
|
|
82
|
+
relevant_ranges = [(cast('int', span['start']), cast('int', span['end'])) for span in source_spans if span['document_id'] == document_id]
|
|
83
|
+
for chunk in document_chunks:
|
|
84
|
+
for position in _token_spans(documents[document_id]):
|
|
85
|
+
if position[0] < chunk.end and position[1] > chunk.start:
|
|
86
|
+
emitted_token_total += 1
|
|
87
|
+
positions[position] = positions.get(position, 0) + 1
|
|
88
|
+
relevant_emitted_tokens += int(any((position[0] < end and position[1] > start for start, end in relevant_ranges)))
|
|
89
|
+
duplicate_positions += sum((max(count - 1, 0) for count in positions.values()))
|
|
90
|
+
required_groups = [group for group in evidence_groups if group['requirement'] == 'required']
|
|
91
|
+
oracle_order = sorted(chunks, key=lambda chunk: (-sum((len(span_to_chunks.get(cast('str', span_id), set()) & {chunk.chunk_id}) * cast('float', group['weight']) for group in evidence_groups for span_id in cast('list[object]', group['source_span_ids']))), chunk.chunk_id))
|
|
92
|
+
group_recall: dict[str, float | None] = {}
|
|
93
|
+
for depth in DEPTHS:
|
|
94
|
+
selected = {chunk.chunk_id for chunk in oracle_order[:depth]}
|
|
95
|
+
covered = sum((any((selected & span_to_chunks.get(cast('str', span_id), set()) for span_id in cast('list[object]', group['source_span_ids']))) for group in required_groups))
|
|
96
|
+
group_recall[str(depth)] = None if not required_groups else covered / len(required_groups)
|
|
97
|
+
chunk_chars = [float(chunk.end - chunk.start) for chunk in chunks]
|
|
98
|
+
chunk_tokens = [float(len(_token_spans(chunk.text))) for chunk in chunks]
|
|
99
|
+
return cast('dict[str, JsonValue]', {'evidence_span_containment': None if not source_spans else contained / len(source_spans), 'boundary_fragmentation': _distribution([float(value) for value in intersections]), 'gold_token_precision': None if emitted_token_total == 0 else relevant_emitted_tokens / emitted_token_total, 'gold_token_recall': None if gold_token_total == 0 else covered_gold_tokens / gold_token_total, 'evidence_density': None if not chunks else len(relevant_chunk_ids) / len(chunks), 'duplicate_token_rate': None if emitted_token_total == 0 else duplicate_positions / emitted_token_total, 'chunk_character_distribution': _distribution(chunk_chars), 'chunk_token_distribution': _distribution(chunk_tokens), 'oracle_evidence_group_recall': group_recall})
|
|
100
|
+
|
|
101
|
+
# Standalone Sphynx execution surface. Only captured responses enter grading.
|
|
102
|
+
from dataclasses import asdict, replace
|
|
103
|
+
from time import perf_counter
|
|
104
|
+
from .contracts import ContractError
|
|
105
|
+
from .integration_contracts import CandidateConfiguration, ComparisonIdentity, TrackCaseCapture, value_digest, require_comparable
|
|
106
|
+
from .dataset_v3 import load_dataset_v3, assess_readiness, intake_validate_pack_manifest, scan_privacy
|
|
107
|
+
from ._track_capture import (create_run, write_artifact, finish_run, read_artifact, verify_track_run,
|
|
108
|
+
write_evaluation, verify_track_evaluation, open_track_run, open_track_evaluation, byte_digest, strict_json)
|
|
109
|
+
from .packs import PackDescriptor, ExecutionPolicy
|
|
110
|
+
from pathlib import Path
|
|
111
|
+
|
|
112
|
+
PACK = PackDescriptor('rag.chunk.v1', 'v1', 'Source-bound chunking evaluation', 'rag_chunk',
|
|
113
|
+
'sphynx.chunk.v1', 'sable.rag.dataset.v3', 'sphynx.chunk.request.v1', 'sphynx.chunk.response.v1',
|
|
114
|
+
'runnable', 'Span and token recovery on frozen source documents.',
|
|
115
|
+
ExecutionPolicy('sphynx.chunk.policy.v1', 1, 30.0, 'trusted-local'),
|
|
116
|
+
metric_release='sphynx.chunk.metrics.v1', required_inputs=('Pinned DatasetV3',), run_api='run_chunking')
|
|
117
|
+
|
|
118
|
+
_source_evaluate_chunking = evaluate_chunking
|
|
119
|
+
|
|
120
|
+
def evaluate_chunking(documents, source_spans, evidence_groups, chunks, *, expected_document_sha256):
|
|
121
|
+
"""Source-parity metrics with strict span, revision and chunk identity validation."""
|
|
122
|
+
if set(documents) != set(expected_document_sha256):
|
|
123
|
+
raise Level1Error('document hash inventory differs')
|
|
124
|
+
ids = [chunk.chunk_id for chunk in chunks]
|
|
125
|
+
if len(ids) != len(set(ids)) or any(not isinstance(i, str) or not i for i in ids):
|
|
126
|
+
raise Level1Error('chunk IDs must be unique nonempty strings')
|
|
127
|
+
for chunk in chunks:
|
|
128
|
+
if type(chunk.start) is not int or type(chunk.end) is not int:
|
|
129
|
+
raise Level1Error('chunk bounds must be integers')
|
|
130
|
+
span_ids = set()
|
|
131
|
+
for span in source_spans:
|
|
132
|
+
sid, did = span['source_span_id'], span['document_id']
|
|
133
|
+
if sid in span_ids or did not in documents:
|
|
134
|
+
raise Level1Error('duplicate span or unknown document')
|
|
135
|
+
span_ids.add(sid)
|
|
136
|
+
start, end = span['start'], span['end']
|
|
137
|
+
if type(start) is not int or type(end) is not int or not 0 <= start < end <= len(documents[did]):
|
|
138
|
+
raise Level1Error('invalid source span bounds')
|
|
139
|
+
if span.get('content', documents[did]) != documents[did] or span.get('exact_text', documents[did][start:end]) != documents[did][start:end]:
|
|
140
|
+
raise Level1Error('source span content/revision drift')
|
|
141
|
+
group_ids = set()
|
|
142
|
+
for group in evidence_groups:
|
|
143
|
+
gid = group['evidence_group_id']
|
|
144
|
+
if gid in group_ids or not set(group['source_span_ids']) <= span_ids:
|
|
145
|
+
raise Level1Error('duplicate group or unknown source reference')
|
|
146
|
+
group_ids.add(gid)
|
|
147
|
+
if group['requirement'] not in ('required','optional') or type(group['weight']) not in (int,float) or not math.isfinite(group['weight']) or group['weight'] <= 0:
|
|
148
|
+
raise Level1Error('invalid evidence group')
|
|
149
|
+
return _source_evaluate_chunking(documents, source_spans, evidence_groups, chunks,
|
|
150
|
+
expected_document_sha256=expected_document_sha256)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _documents(task):
|
|
154
|
+
docs = {}
|
|
155
|
+
for span in task['source_spans']:
|
|
156
|
+
doc = {'document_id': span['document_id'], 'text': span['content'],
|
|
157
|
+
'content_sha256': span['content_sha256'],
|
|
158
|
+
'document_revision_family_id': span['document_revision_family_id']}
|
|
159
|
+
if doc['document_id'] in docs and docs[doc['document_id']] != doc:
|
|
160
|
+
raise ContractError('document revision drift')
|
|
161
|
+
docs[doc['document_id']] = doc
|
|
162
|
+
return list(docs.values())
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _request(task, policy):
|
|
166
|
+
return {'schema_version': 'sphynx.chunk.request.v1', 'case_id': task['task_id'],
|
|
167
|
+
'documents': _documents(task), 'policy': policy}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _parse_response(response, task):
|
|
171
|
+
if not isinstance(response, dict) or set(response) != {'schema_version','case_id','chunks'}:
|
|
172
|
+
raise ContractError('chunk response fields differ')
|
|
173
|
+
if response['schema_version'] != 'sphynx.chunk.response.v1' or response['case_id'] != task['task_id']:
|
|
174
|
+
raise ContractError('chunk response identity differs')
|
|
175
|
+
if not isinstance(response['chunks'], list):
|
|
176
|
+
raise ContractError('chunks must be an array')
|
|
177
|
+
chunks = []
|
|
178
|
+
for row in response['chunks']:
|
|
179
|
+
if not isinstance(row, dict) or set(row) != {'chunk_id','document_id','start','end','text'}:
|
|
180
|
+
raise ContractError('chunk fields differ')
|
|
181
|
+
chunks.append(Chunk(**row))
|
|
182
|
+
documents = {d['document_id']: d['text'] for d in _documents(task)}
|
|
183
|
+
hashes = {d['document_id']: d['content_sha256'] for d in _documents(task)}
|
|
184
|
+
return evaluate_chunking(documents, task['source_spans'], task['evidence_groups'], chunks,
|
|
185
|
+
expected_document_sha256=hashes)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _policy(dataset, *, synthetic, owner_authorized, pack_manifest_path, trials, attempts, boundary_digest):
|
|
189
|
+
for count in (trials, attempts):
|
|
190
|
+
if type(count) is not int or count < 1:
|
|
191
|
+
raise ContractError('trials and attempts must be positive integers')
|
|
192
|
+
if type(synthetic) is not bool:
|
|
193
|
+
raise ContractError('synthetic must be explicit boolean')
|
|
194
|
+
approval = None
|
|
195
|
+
if not synthetic:
|
|
196
|
+
if owner_authorized is not True or pack_manifest_path is None:
|
|
197
|
+
raise ContractError('owner authorization and an approved pack manifest are required')
|
|
198
|
+
data = Path(pack_manifest_path).read_bytes()
|
|
199
|
+
pack = intake_validate_pack_manifest(strict_json(data))
|
|
200
|
+
if pack['dataset'] != {'dataset_id': dataset.dataset_id, 'version': dataset.version,
|
|
201
|
+
'sha256': dataset._source_digest.removeprefix('sha256:')}:
|
|
202
|
+
raise ContractError('approved pack does not bind dataset bytes')
|
|
203
|
+
if any(t.customer_id != pack['customer_id'] for t in dataset.tasks):
|
|
204
|
+
raise ContractError('pack customer identity differs')
|
|
205
|
+
if scan_privacy(dataset.value):
|
|
206
|
+
raise ContractError('pack privacy scan failed')
|
|
207
|
+
if not assess_readiness(dataset, pack).quality_metrics_allowed:
|
|
208
|
+
raise ContractError('approved dataset remains blocked for quality claims')
|
|
209
|
+
approval = byte_digest(data)
|
|
210
|
+
return {'version':'sphynx.chunk.policy.v1', 'synthetic':synthetic,
|
|
211
|
+
'approved_manifest_digest':approval, 'trials':trials, 'attempts':attempts,
|
|
212
|
+
'selection':'first_success_per_trial', 'boundary_digest':boundary_digest}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _identity(dataset, source_digest, tasks, policy):
|
|
216
|
+
return ComparisonIdentity('rag_chunk', PACK.pack_id, PACK.pack_release, 'sable.rag.dataset.v3',
|
|
217
|
+
source_digest, value_digest([t['task_id'] for t in tasks]),
|
|
218
|
+
value_digest([{'source_spans': t['source_spans'], 'evidence_groups': t['evidence_groups']} for t in tasks]),
|
|
219
|
+
value_digest(policy), PACK.metric_release, 'sphynx.chunk.grader.v1',
|
|
220
|
+
policy['boundary_digest'] or value_digest({'boundary':'chunker', 'dataset':source_digest}))
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def run_chunking(candidate, adapter, dataset_path, output_dir, *, expected_sha256, synthetic=False,
|
|
224
|
+
owner_authorized=False, pack_manifest_path=None, trials=1, attempts=1,
|
|
225
|
+
case_ids=None, boundary_digest=None):
|
|
226
|
+
dataset = load_dataset_v3(dataset_path, expected_sha256=expected_sha256)
|
|
227
|
+
# A separate byte commitment is supplied to approval checks; no data identity is rewritten.
|
|
228
|
+
policy = _policy_for_source(dataset, expected_sha256, synthetic=synthetic,
|
|
229
|
+
owner_authorized=owner_authorized, pack_manifest_path=pack_manifest_path,
|
|
230
|
+
trials=trials, attempts=attempts, boundary_digest=boundary_digest)
|
|
231
|
+
tasks = _select_tasks(dataset, case_ids)
|
|
232
|
+
if not isinstance(candidate, CandidateConfiguration) or candidate.interface_version != PACK.interface_version:
|
|
233
|
+
raise ContractError('candidate must declare the chunk interface')
|
|
234
|
+
if candidate.execution_mode not in ('trusted_local', 'recorded'):
|
|
235
|
+
raise ContractError('chunk runner requires an explicit trusted local or recorded adapter')
|
|
236
|
+
if candidate.execution_mode == 'recorded':
|
|
237
|
+
if not isinstance(adapter, Mapping) or candidate.parameters.get('recordings_digest') != value_digest(adapter):
|
|
238
|
+
raise ContractError('recorded outputs need their complete pinned recording digest')
|
|
239
|
+
adapter = strict_json(__import__('json').dumps(dict(adapter),allow_nan=False))
|
|
240
|
+
elif not callable(adapter):
|
|
241
|
+
raise ContractError('trusted local adapter must be callable')
|
|
242
|
+
approval_source = None if synthetic else Path(pack_manifest_path).read_bytes().decode('utf-8')
|
|
243
|
+
observed = {'approved_manifest_source':approval_source}
|
|
244
|
+
_verify_approval(dataset, expected_sha256, policy, observed)
|
|
245
|
+
identity = _identity(dataset, expected_sha256, tasks, policy)
|
|
246
|
+
ordered = [t['task_id'] for t in tasks]
|
|
247
|
+
path = create_run(output_dir, identity, candidate, ordered)
|
|
248
|
+
captures=[]
|
|
249
|
+
for index, task in enumerate(tasks):
|
|
250
|
+
for trial in range(1,trials+1):
|
|
251
|
+
for attempt in range(1,attempts+1):
|
|
252
|
+
request = _request(task, policy)
|
|
253
|
+
prefix = f'cases/{index}/{trial}/{attempt}/'
|
|
254
|
+
request_binding = write_artifact(path,prefix+'request.json',request)
|
|
255
|
+
response_binding = None
|
|
256
|
+
failure=None
|
|
257
|
+
begin=perf_counter()
|
|
258
|
+
try:
|
|
259
|
+
response = adapter[value_digest(request)] if candidate.execution_mode == 'recorded' else adapter(strict_json(__import__('json').dumps(request)))
|
|
260
|
+
response_binding = write_artifact(path,prefix+'response.json',response)
|
|
261
|
+
_parse_response(response,task)
|
|
262
|
+
except Exception:
|
|
263
|
+
failure='invalid_response' if response_binding is not None else 'candidate_failure'
|
|
264
|
+
observed_binding=write_artifact(path,prefix+'observed.json',observed)
|
|
265
|
+
captures.append(TrackCaseCapture(task['task_id'],trial,attempt,request_binding,response_binding,observed_binding,failure,
|
|
266
|
+
(perf_counter()-begin)*1000))
|
|
267
|
+
return finish_run(path,identity,candidate,ordered,captures)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _policy_for_source(dataset, source_digest, **kwargs):
|
|
271
|
+
# Pass immutable dataset and raw-file commitment together without altering DatasetV3.
|
|
272
|
+
class BoundDataset:
|
|
273
|
+
_source_digest=source_digest
|
|
274
|
+
dataset_id=dataset.dataset_id
|
|
275
|
+
version=dataset.version
|
|
276
|
+
tasks=dataset.tasks
|
|
277
|
+
value=dataset.value
|
|
278
|
+
return _policy(BoundDataset(), **kwargs)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _select_tasks(dataset, case_ids):
|
|
282
|
+
tasks=dataset.value['tasks']
|
|
283
|
+
if case_ids is None: return tasks
|
|
284
|
+
case_ids=tuple(case_ids)
|
|
285
|
+
known={t['task_id']:t for t in tasks}
|
|
286
|
+
if not case_ids or len(set(case_ids))!=len(case_ids) or not set(case_ids)<=set(known):
|
|
287
|
+
raise ContractError('invalid selected case identities')
|
|
288
|
+
return [known[c] for c in case_ids]
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def grade_chunking(run, dataset_path, *, expected_sha256):
|
|
292
|
+
dataset=load_dataset_v3(dataset_path,expected_sha256=expected_sha256)
|
|
293
|
+
manifest=verify_track_run(run)
|
|
294
|
+
tasks=_select_tasks(dataset,manifest.case_ids)
|
|
295
|
+
policy=read_artifact(run.path,manifest.captures[0].request).get('policy')
|
|
296
|
+
if not isinstance(policy,dict) or set(policy)!={'version','synthetic','approved_manifest_digest','trials','attempts','selection','boundary_digest'}:
|
|
297
|
+
raise ContractError('capture policy fields differ')
|
|
298
|
+
if policy['version']!='sphynx.chunk.policy.v1' or policy['selection']!='first_success_per_trial' or type(policy['synthetic']) is not bool:
|
|
299
|
+
raise ContractError('unsupported capture policy')
|
|
300
|
+
require_comparable(_identity(dataset,expected_sha256,tasks,policy),manifest.identity)
|
|
301
|
+
manifest=verify_track_run(run,expected_case_ids=[t['task_id'] for t in tasks],
|
|
302
|
+
expected_requests={t['task_id']:_request(t,policy) for t in tasks},trials=policy['trials'],attempts=policy['attempts'])
|
|
303
|
+
for capture in manifest.captures:
|
|
304
|
+
if capture.observed is None:
|
|
305
|
+
raise ContractError('capture lacks its approval observation')
|
|
306
|
+
_verify_approval(dataset,expected_sha256,policy,read_artifact(run.path,capture.observed))
|
|
307
|
+
results=[]
|
|
308
|
+
for task in tasks:
|
|
309
|
+
for trial in range(1,policy['trials']+1):
|
|
310
|
+
selected=None
|
|
311
|
+
for capture in sorted(manifest.captures,key=lambda c:c.attempt_index):
|
|
312
|
+
if capture.case_id!=task['task_id'] or capture.trial_index!=trial: continue
|
|
313
|
+
if capture.failure is None and capture.response is not None:
|
|
314
|
+
score=_parse_response(read_artifact(run.path,capture.response),task)
|
|
315
|
+
if selected is None: selected=score
|
|
316
|
+
results.append({'case_id':task['task_id'],'trial_index':trial,'metrics':selected})
|
|
317
|
+
good=[r['metrics'] for r in results if r['metrics'] is not None]
|
|
318
|
+
summary={key: (sum(m[key] for m in good if m[key] is not None)/sum(m[key] is not None for m in good)
|
|
319
|
+
if any(m[key] is not None for m in good) else None)
|
|
320
|
+
for key in ('evidence_span_containment','gold_token_precision','gold_token_recall','evidence_density','duplicate_token_rate')}
|
|
321
|
+
summary.update(coverage=len(good)/len(results),successful_trials=len(good),expected_trials=len(results))
|
|
322
|
+
return write_evaluation(run,manifest.identity,{'summary':summary,'cases':results,
|
|
323
|
+
'quality_scope':'synthetic_fixture' if policy['synthetic'] else 'owner_approved',
|
|
324
|
+
'complete':len(good)==len(results)})
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def report_chunking(evaluation, dataset_path, *, expected_sha256):
|
|
328
|
+
# Revalidate the complete capture/request boundary without invoking any candidate.
|
|
329
|
+
_verify_evaluation_dataset(evaluation,dataset_path,expected_sha256)
|
|
330
|
+
return verify_track_evaluation(evaluation).to_dict()
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _verify_evaluation_dataset(evaluation, dataset_path, expected_sha256):
|
|
334
|
+
dataset=load_dataset_v3(dataset_path,expected_sha256=expected_sha256)
|
|
335
|
+
manifest=verify_track_run(evaluation.run)
|
|
336
|
+
policy=read_artifact(evaluation.run.path,manifest.captures[0].request)['policy']
|
|
337
|
+
tasks=_select_tasks(dataset,manifest.case_ids)
|
|
338
|
+
require_comparable(_identity(dataset,expected_sha256,tasks,policy),manifest.identity)
|
|
339
|
+
for capture in manifest.captures:
|
|
340
|
+
if capture.observed is None: raise ContractError('missing approval observation')
|
|
341
|
+
_verify_approval(dataset,expected_sha256,policy,read_artifact(evaluation.run.path,capture.observed))
|
|
342
|
+
verify_track_run(evaluation.run,expected_case_ids=[t['task_id'] for t in tasks],
|
|
343
|
+
expected_requests={t['task_id']:_request(t,policy) for t in tasks},trials=policy['trials'],attempts=policy['attempts'])
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def compare_chunking(baseline,candidate,dataset_path,*,expected_sha256):
|
|
347
|
+
left=report_chunking(baseline,dataset_path,expected_sha256=expected_sha256)
|
|
348
|
+
right=report_chunking(candidate,dataset_path,expected_sha256=expected_sha256)
|
|
349
|
+
require_comparable(ComparisonIdentity.from_dict(left['identity']),ComparisonIdentity.from_dict(right['identity']))
|
|
350
|
+
return {'baseline':left['evaluation_id'],'candidate':right['evaluation_id'],
|
|
351
|
+
'delta':{key: None if value is None or right['metrics']['summary'][key] is None
|
|
352
|
+
else right['metrics']['summary'][key]-value
|
|
353
|
+
for key,value in left['metrics']['summary'].items()}}
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _verify_approval(dataset, source_digest, policy, observed):
|
|
357
|
+
if not isinstance(observed,dict) or set(observed)!={'approved_manifest_source'}:
|
|
358
|
+
raise ContractError('approval observation fields differ')
|
|
359
|
+
source=observed['approved_manifest_source']
|
|
360
|
+
if policy['synthetic']:
|
|
361
|
+
if source is not None or policy['approved_manifest_digest'] is not None:
|
|
362
|
+
raise ContractError('synthetic captures cannot claim owner approval')
|
|
363
|
+
return
|
|
364
|
+
if not isinstance(source,str) or byte_digest(source.encode('utf-8'))!=policy['approved_manifest_digest']:
|
|
365
|
+
raise ContractError('approval evidence digest mismatch')
|
|
366
|
+
pack=intake_validate_pack_manifest(strict_json(source))
|
|
367
|
+
if pack['dataset']!={'dataset_id':dataset.dataset_id,'version':dataset.version,'sha256':source_digest.removeprefix('sha256:')}:
|
|
368
|
+
raise ContractError('approval evidence dataset mismatch')
|
|
369
|
+
if any(t.customer_id!=pack['customer_id'] for t in dataset.tasks):
|
|
370
|
+
raise ContractError('approval evidence customer mismatch')
|
|
371
|
+
if not assess_readiness(dataset,pack).quality_metrics_allowed:
|
|
372
|
+
raise ContractError('retained approval evidence does not permit quality claims')
|