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
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
"""Operator-owned extension registration for the shared workflow.
|
|
2
|
+
|
|
3
|
+
Registration and discovery inspect declared metadata only. They do not import
|
|
4
|
+
extensions, read assets, validate payloads, execute candidates, or run evaluator
|
|
5
|
+
and policy callbacks. The operator supplies objects explicitly. Their declared
|
|
6
|
+
implementation labels identify a binding; they do not authenticate source bytes.
|
|
7
|
+
|
|
8
|
+
Each role has an independent ID space. Native catalogs remain authoritative for
|
|
9
|
+
native records; this registry contains only explicitly enabled common extensions.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from collections.abc import Callable, Collection, Mapping
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from .workflow_contracts import (
|
|
19
|
+
BackendDescriptor, CaseProvider, Criterion, Dataset, DatasetDefinition,
|
|
20
|
+
EvaluatorDefinition, MetricSpec, PayloadSchema, SuiteDefinition,
|
|
21
|
+
WorkflowBackend, WorkflowContractError, _object, _text, _thaw,
|
|
22
|
+
contract_digest,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
_IDENTIFIER = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:/@+\-]*\Z")
|
|
26
|
+
_BACKEND_METHODS = (
|
|
27
|
+
"describe", "cases", "resolve", "admit", "recheck", "execute",
|
|
28
|
+
"accepts_reference", "reference", "assess", "report", "assessments", "compare",
|
|
29
|
+
)
|
|
30
|
+
_POLICY_KINDS = frozenset({"aggregation", "comparison", "report", "decision"})
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class WorkflowRegistryError(WorkflowContractError):
|
|
34
|
+
"""An extension is missing, incompatible, duplicated, or has drifted."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class CallbackDefinition:
|
|
39
|
+
"""Declared policy/projection identity; lookup never invokes its callback."""
|
|
40
|
+
|
|
41
|
+
callback_id: str
|
|
42
|
+
version: str
|
|
43
|
+
implementation_id: str
|
|
44
|
+
kind: str
|
|
45
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
46
|
+
|
|
47
|
+
def __post_init__(self) -> None:
|
|
48
|
+
for name in ("callback_id", "version", "implementation_id", "kind"):
|
|
49
|
+
_text(getattr(self, name), name)
|
|
50
|
+
object.__setattr__(self, "metadata", _object(self.metadata, "metadata"))
|
|
51
|
+
|
|
52
|
+
def to_dict(self) -> dict[str, Any]:
|
|
53
|
+
return {"callback_id": self.callback_id, "version": self.version,
|
|
54
|
+
"implementation_id": self.implementation_id, "kind": self.kind,
|
|
55
|
+
"metadata": _thaw(self.metadata)}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class _ProviderSchemaBinding:
|
|
60
|
+
role: str
|
|
61
|
+
digest: str
|
|
62
|
+
validator: Callable[..., Any] = field(repr=False, compare=False)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class _Registration:
|
|
67
|
+
value: Any
|
|
68
|
+
definition: Any
|
|
69
|
+
digest: str
|
|
70
|
+
callback: Callable[..., Any] | None = None
|
|
71
|
+
schema_bindings: tuple[_ProviderSchemaBinding, ...] | None = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class WorkflowRegistry:
|
|
75
|
+
"""Explicit, mutable operator registry with pinned registration metadata.
|
|
76
|
+
|
|
77
|
+
IDs are namespaced by default. ``legacy_ids`` permits an operator's exact,
|
|
78
|
+
known opaque native IDs without relaxing the convention for new extensions.
|
|
79
|
+
Duplicate IDs within a role are rejected, even for another version. Use a
|
|
80
|
+
separate registry to select a different binding. Criterion and metric IDs
|
|
81
|
+
may have the same spelling without implying a one-to-one relationship.
|
|
82
|
+
|
|
83
|
+
Backend ``descriptor``/``describe`` and provider ``definition`` must be pure
|
|
84
|
+
metadata access. Registration/discovery do not call asset or runtime methods.
|
|
85
|
+
``resolve_suite`` checks declared cross-references and descriptor drift; it
|
|
86
|
+
is not execution admission and does not grant reference access.
|
|
87
|
+
|
|
88
|
+
Register a concrete Dataset's input, reference and selection schemas first.
|
|
89
|
+
Registration and provider lookup require their exact registered metadata
|
|
90
|
+
and validator bindings. Custom CaseProvider implementations remain subject
|
|
91
|
+
to their backend's validation contract; the registry does not inspect their
|
|
92
|
+
private schema implementation or invoke provider methods to discover it.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(self, *, legacy_ids: Collection[str] = ()) -> None:
|
|
96
|
+
if isinstance(legacy_ids, (str, bytes)) or not isinstance(legacy_ids, Collection):
|
|
97
|
+
raise WorkflowRegistryError("legacy_ids must be an explicit collection of native IDs")
|
|
98
|
+
self._legacy_ids = frozenset(_text(value, "legacy ID") for value in legacy_ids)
|
|
99
|
+
self._stores: dict[str, dict[str, _Registration]] = {
|
|
100
|
+
role: {} for role in ("backend", "schema", "dataset", "provider", "criterion",
|
|
101
|
+
"evaluator", "metric", "suite", "policy", "projection")
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
def _id(self, value: str, role: str) -> str:
|
|
105
|
+
_text(value, role + " ID")
|
|
106
|
+
if not _IDENTIFIER.fullmatch(value):
|
|
107
|
+
raise WorkflowRegistryError(f"{role} ID must be a stable identifier")
|
|
108
|
+
if "." not in value and ":" not in value and value not in self._legacy_ids:
|
|
109
|
+
raise WorkflowRegistryError(f"{role} ID must be namespaced or an explicit legacy ID")
|
|
110
|
+
return value
|
|
111
|
+
|
|
112
|
+
def fork(self) -> WorkflowRegistry:
|
|
113
|
+
"""Stage trusted registrations without changing this registry's stores.
|
|
114
|
+
|
|
115
|
+
Bindings keep their exact validator, callback and implementation objects.
|
|
116
|
+
This isolates registration membership on factory failure. It does not
|
|
117
|
+
sandbox trusted code or copy mutable state inside an implementation.
|
|
118
|
+
Lookup still checks the original registration pins for drift.
|
|
119
|
+
"""
|
|
120
|
+
for identifier in self._stores["backend"]:
|
|
121
|
+
self.get_backend(identifier)
|
|
122
|
+
for identifier in self._stores["schema"]:
|
|
123
|
+
self.get_schema(identifier)
|
|
124
|
+
for identifier in self._stores["provider"]:
|
|
125
|
+
self.get_provider(identifier)
|
|
126
|
+
selected = WorkflowRegistry(legacy_ids=self._legacy_ids)
|
|
127
|
+
selected._stores = {role: dict(rows) for role, rows in self._stores.items()}
|
|
128
|
+
return selected
|
|
129
|
+
|
|
130
|
+
def _register(self, role: str, identifier: str, value: Any, definition: Any,
|
|
131
|
+
callback: Callable[..., Any] | None = None, *,
|
|
132
|
+
schema_bindings: tuple[_ProviderSchemaBinding, ...] | None = None) -> None:
|
|
133
|
+
identifier = self._id(identifier, role)
|
|
134
|
+
if identifier in self._stores[role]:
|
|
135
|
+
raise WorkflowRegistryError(f"duplicate {role} ID: {identifier}")
|
|
136
|
+
self._stores[role][identifier] = _Registration(
|
|
137
|
+
value, definition, contract_digest(definition.to_dict()), callback, schema_bindings,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def _get(self, role: str, identifier: str) -> _Registration:
|
|
141
|
+
self._id(identifier, role)
|
|
142
|
+
try:
|
|
143
|
+
return self._stores[role][identifier]
|
|
144
|
+
except KeyError:
|
|
145
|
+
raise WorkflowRegistryError(f"unregistered {role}: {identifier}") from None
|
|
146
|
+
|
|
147
|
+
def _definitions(self, role: str) -> tuple[Any, ...]:
|
|
148
|
+
# These are frozen snapshots captured during registration, not live
|
|
149
|
+
# backend/provider properties or runtime callbacks.
|
|
150
|
+
return tuple(self._stores[role][key].definition for key in sorted(self._stores[role]))
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def _unchanged(registration: _Registration, definition: Any, role: str) -> None:
|
|
154
|
+
if contract_digest(definition.to_dict()) != registration.digest:
|
|
155
|
+
raise WorkflowRegistryError(f"registered {role} metadata has changed")
|
|
156
|
+
|
|
157
|
+
def register_backend(self, backend: WorkflowBackend) -> BackendDescriptor:
|
|
158
|
+
descriptor = getattr(backend, "descriptor", None)
|
|
159
|
+
if not isinstance(descriptor, BackendDescriptor):
|
|
160
|
+
raise WorkflowRegistryError("backend requires a BackendDescriptor")
|
|
161
|
+
if any(not callable(getattr(backend, name, None)) for name in _BACKEND_METHODS):
|
|
162
|
+
raise WorkflowRegistryError("backend does not implement the workflow protocol")
|
|
163
|
+
from .execution_capabilities import execution_paths
|
|
164
|
+
try:
|
|
165
|
+
execution_paths(descriptor)
|
|
166
|
+
except WorkflowContractError as exc:
|
|
167
|
+
raise WorkflowRegistryError(str(exc)) from None
|
|
168
|
+
for suite_id in descriptor.suite_ids:
|
|
169
|
+
self._id(suite_id, "suite")
|
|
170
|
+
self._register("backend", descriptor.backend_id, backend, descriptor)
|
|
171
|
+
return descriptor
|
|
172
|
+
|
|
173
|
+
def get_backend(self, backend_id: str) -> WorkflowBackend:
|
|
174
|
+
registration = self._get("backend", backend_id)
|
|
175
|
+
descriptor = getattr(registration.value, "descriptor", None)
|
|
176
|
+
if not isinstance(descriptor, BackendDescriptor):
|
|
177
|
+
raise WorkflowRegistryError("registered backend descriptor is invalid")
|
|
178
|
+
self._unchanged(registration, descriptor, "backend")
|
|
179
|
+
return registration.value
|
|
180
|
+
|
|
181
|
+
def list_backends(self) -> tuple[BackendDescriptor, ...]:
|
|
182
|
+
return self._definitions("backend")
|
|
183
|
+
|
|
184
|
+
def register_schema(self, schema: PayloadSchema) -> PayloadSchema:
|
|
185
|
+
if not isinstance(schema, PayloadSchema):
|
|
186
|
+
raise WorkflowRegistryError("schema must be PayloadSchema")
|
|
187
|
+
self._register("schema", schema.schema_id, schema, schema, schema.validator)
|
|
188
|
+
return schema
|
|
189
|
+
|
|
190
|
+
def get_schema(self, schema_id: str) -> PayloadSchema:
|
|
191
|
+
registration = self._get("schema", schema_id)
|
|
192
|
+
self._unchanged(registration, registration.value, "schema")
|
|
193
|
+
if registration.value.validator is not registration.callback:
|
|
194
|
+
raise WorkflowRegistryError("registered schema validator has changed")
|
|
195
|
+
return registration.value
|
|
196
|
+
|
|
197
|
+
def list_schemas(self) -> tuple[PayloadSchema, ...]:
|
|
198
|
+
return self._definitions("schema")
|
|
199
|
+
|
|
200
|
+
def validate_payload(self, schema_id: str, value: Any) -> Any:
|
|
201
|
+
"""Explicitly execute the registered validator on a detached snapshot."""
|
|
202
|
+
return self.get_schema(schema_id).validate(value)
|
|
203
|
+
|
|
204
|
+
def register_dataset(self, definition: DatasetDefinition) -> DatasetDefinition:
|
|
205
|
+
if not isinstance(definition, DatasetDefinition):
|
|
206
|
+
raise WorkflowRegistryError("dataset must be DatasetDefinition")
|
|
207
|
+
for role, identifier in (("provider", definition.provider_id),
|
|
208
|
+
("schema", definition.input_schema_id)):
|
|
209
|
+
self._id(identifier, role)
|
|
210
|
+
if definition.reference_schema_id is not None:
|
|
211
|
+
self._id(definition.reference_schema_id, "schema")
|
|
212
|
+
self._register("dataset", definition.dataset_id, definition, definition)
|
|
213
|
+
return definition
|
|
214
|
+
|
|
215
|
+
def get_dataset(self, dataset_id: str) -> DatasetDefinition:
|
|
216
|
+
registration = self._get("dataset", dataset_id)
|
|
217
|
+
self._unchanged(registration, registration.value, "dataset")
|
|
218
|
+
return registration.value
|
|
219
|
+
|
|
220
|
+
def list_datasets(self) -> tuple[DatasetDefinition, ...]:
|
|
221
|
+
return self._definitions("dataset")
|
|
222
|
+
|
|
223
|
+
def _provider_schema_bindings(self, provider: CaseProvider,
|
|
224
|
+
definition: DatasetDefinition) -> tuple[_ProviderSchemaBinding, ...] | None:
|
|
225
|
+
if not isinstance(provider, Dataset):
|
|
226
|
+
return None
|
|
227
|
+
bindings = []
|
|
228
|
+
for role, schema, expected_id in (
|
|
229
|
+
("input", provider.input_schema, definition.input_schema_id),
|
|
230
|
+
("reference", provider.reference_schema, definition.reference_schema_id),
|
|
231
|
+
("selection", provider.selection_schema, None),
|
|
232
|
+
):
|
|
233
|
+
if schema is None and role != "input":
|
|
234
|
+
continue
|
|
235
|
+
if not isinstance(schema, PayloadSchema):
|
|
236
|
+
raise WorkflowRegistryError(f"concrete dataset {role} schema is invalid")
|
|
237
|
+
if role != "selection" and schema.schema_id != expected_id:
|
|
238
|
+
raise WorkflowRegistryError(f"concrete dataset {role} schema differs from its definition")
|
|
239
|
+
registered = self.get_schema(schema.schema_id)
|
|
240
|
+
digest = contract_digest(schema.to_dict())
|
|
241
|
+
if (digest != contract_digest(registered.to_dict())
|
|
242
|
+
or schema.validator is not registered.validator):
|
|
243
|
+
raise WorkflowRegistryError(f"concrete dataset {role} schema differs from registered binding")
|
|
244
|
+
bindings.append(_ProviderSchemaBinding(role, digest, schema.validator))
|
|
245
|
+
return tuple(bindings)
|
|
246
|
+
|
|
247
|
+
def register_provider(self, provider: CaseProvider) -> DatasetDefinition:
|
|
248
|
+
definition = getattr(provider, "definition", None)
|
|
249
|
+
if not isinstance(definition, DatasetDefinition):
|
|
250
|
+
raise WorkflowRegistryError("provider requires a DatasetDefinition")
|
|
251
|
+
if not callable(getattr(provider, "cases", None)) or not callable(getattr(provider, "reference_for", None)):
|
|
252
|
+
raise WorkflowRegistryError("provider does not implement the case-provider protocol")
|
|
253
|
+
self._id(definition.provider_id, "provider")
|
|
254
|
+
if definition.provider_id in self._stores["provider"]:
|
|
255
|
+
raise WorkflowRegistryError(f"duplicate provider ID: {definition.provider_id}")
|
|
256
|
+
existing = self._stores["dataset"].get(definition.dataset_id)
|
|
257
|
+
if existing is not None and existing.digest != contract_digest(definition.to_dict()):
|
|
258
|
+
raise WorkflowRegistryError("provider definition differs from registered dataset")
|
|
259
|
+
bindings = self._provider_schema_bindings(provider, definition)
|
|
260
|
+
if existing is None:
|
|
261
|
+
self.register_dataset(definition)
|
|
262
|
+
self._register("provider", definition.provider_id, provider, definition,
|
|
263
|
+
schema_bindings=bindings)
|
|
264
|
+
return definition
|
|
265
|
+
|
|
266
|
+
def get_provider(self, provider_id: str) -> CaseProvider:
|
|
267
|
+
registration = self._get("provider", provider_id)
|
|
268
|
+
definition = getattr(registration.value, "definition", None)
|
|
269
|
+
if not isinstance(definition, DatasetDefinition):
|
|
270
|
+
raise WorkflowRegistryError("registered provider definition is invalid")
|
|
271
|
+
self._unchanged(registration, definition, "provider")
|
|
272
|
+
current = self._provider_schema_bindings(registration.value, definition)
|
|
273
|
+
expected = registration.schema_bindings
|
|
274
|
+
if ((current is None) != (expected is None)
|
|
275
|
+
or (current is not None and expected is not None
|
|
276
|
+
and (len(current) != len(expected)
|
|
277
|
+
or any(a.role != b.role or a.digest != b.digest or a.validator is not b.validator
|
|
278
|
+
for a, b in zip(current, expected))))):
|
|
279
|
+
raise WorkflowRegistryError("registered concrete dataset schema bindings have changed")
|
|
280
|
+
return registration.value
|
|
281
|
+
|
|
282
|
+
def list_providers(self) -> tuple[DatasetDefinition, ...]:
|
|
283
|
+
return self._definitions("provider")
|
|
284
|
+
|
|
285
|
+
def register_criterion(self, criterion: Criterion) -> Criterion:
|
|
286
|
+
if not isinstance(criterion, Criterion):
|
|
287
|
+
raise WorkflowRegistryError("criterion must be Criterion")
|
|
288
|
+
self._register("criterion", criterion.criterion_id, criterion, criterion)
|
|
289
|
+
return criterion
|
|
290
|
+
|
|
291
|
+
def get_criterion(self, criterion_id: str) -> Criterion:
|
|
292
|
+
registration = self._get("criterion", criterion_id)
|
|
293
|
+
self._unchanged(registration, registration.value, "criterion")
|
|
294
|
+
return registration.value
|
|
295
|
+
|
|
296
|
+
def list_criteria(self) -> tuple[Criterion, ...]:
|
|
297
|
+
return self._definitions("criterion")
|
|
298
|
+
|
|
299
|
+
def register_evaluator(self, definition: EvaluatorDefinition,
|
|
300
|
+
evaluator: Callable[..., Any] | None = None) -> EvaluatorDefinition:
|
|
301
|
+
if not isinstance(definition, EvaluatorDefinition):
|
|
302
|
+
raise WorkflowRegistryError("evaluator must be EvaluatorDefinition")
|
|
303
|
+
if evaluator is not None and not callable(evaluator):
|
|
304
|
+
raise WorkflowRegistryError("evaluator callback must be callable")
|
|
305
|
+
for role, identifiers in (("criterion", definition.criterion_ids),
|
|
306
|
+
("schema", definition.assessment_schema_ids),
|
|
307
|
+
("metric", definition.metric_ids)):
|
|
308
|
+
for identifier in identifiers:
|
|
309
|
+
self._id(identifier, role)
|
|
310
|
+
self._register("evaluator", definition.evaluator_id, definition, definition, evaluator)
|
|
311
|
+
return definition
|
|
312
|
+
|
|
313
|
+
def get_evaluator(self, evaluator_id: str) -> EvaluatorDefinition:
|
|
314
|
+
registration = self._get("evaluator", evaluator_id)
|
|
315
|
+
self._unchanged(registration, registration.value, "evaluator")
|
|
316
|
+
return registration.value
|
|
317
|
+
|
|
318
|
+
def get_evaluator_callable(self, evaluator_id: str) -> Callable[..., Any]:
|
|
319
|
+
self.get_evaluator(evaluator_id)
|
|
320
|
+
callback = self._get("evaluator", evaluator_id).callback
|
|
321
|
+
if callback is None:
|
|
322
|
+
raise WorkflowRegistryError("evaluator execution is backend-owned; no callback is registered")
|
|
323
|
+
return callback
|
|
324
|
+
|
|
325
|
+
def list_evaluators(self) -> tuple[EvaluatorDefinition, ...]:
|
|
326
|
+
return self._definitions("evaluator")
|
|
327
|
+
|
|
328
|
+
def register_metric(self, metric: MetricSpec) -> MetricSpec:
|
|
329
|
+
if not isinstance(metric, MetricSpec):
|
|
330
|
+
raise WorkflowRegistryError("metric must be MetricSpec")
|
|
331
|
+
for policy_id in (metric.aggregation_policy, metric.comparison_policy):
|
|
332
|
+
if policy_id is not None:
|
|
333
|
+
self._id(policy_id, "policy")
|
|
334
|
+
self._register("metric", metric.metric_id, metric, metric)
|
|
335
|
+
return metric
|
|
336
|
+
|
|
337
|
+
def get_metric(self, metric_id: str) -> MetricSpec:
|
|
338
|
+
registration = self._get("metric", metric_id)
|
|
339
|
+
self._unchanged(registration, registration.value, "metric")
|
|
340
|
+
return registration.value
|
|
341
|
+
|
|
342
|
+
def list_metrics(self) -> tuple[MetricSpec, ...]:
|
|
343
|
+
return self._definitions("metric")
|
|
344
|
+
|
|
345
|
+
def register_suite(self, suite: SuiteDefinition) -> SuiteDefinition:
|
|
346
|
+
if not isinstance(suite, SuiteDefinition):
|
|
347
|
+
raise WorkflowRegistryError("suite must be SuiteDefinition")
|
|
348
|
+
self._id(suite.backend_id, "backend")
|
|
349
|
+
for role, identifiers in (("criterion", suite.criterion_ids),
|
|
350
|
+
("evaluator", suite.evaluator_ids), ("metric", suite.metric_ids)):
|
|
351
|
+
for identifier in identifiers:
|
|
352
|
+
self._id(identifier, role)
|
|
353
|
+
for dataset in suite.datasets:
|
|
354
|
+
self._id(dataset.dataset_id, "dataset")
|
|
355
|
+
self._register("suite", suite.suite_id, suite, suite)
|
|
356
|
+
return suite
|
|
357
|
+
|
|
358
|
+
def get_suite(self, suite_id: str) -> SuiteDefinition:
|
|
359
|
+
registration = self._get("suite", suite_id)
|
|
360
|
+
self._unchanged(registration, registration.value, "suite")
|
|
361
|
+
return registration.value
|
|
362
|
+
|
|
363
|
+
def list_suites(self) -> tuple[SuiteDefinition, ...]:
|
|
364
|
+
return self._definitions("suite")
|
|
365
|
+
|
|
366
|
+
def register_policy(self, policy_id: str, *, version: str, implementation_id: str,
|
|
367
|
+
kind: str, callback: Callable[..., Any] | None = None,
|
|
368
|
+
metadata: Mapping[str, Any] | None = None) -> CallbackDefinition:
|
|
369
|
+
if type(kind) is not str or kind not in _POLICY_KINDS:
|
|
370
|
+
raise WorkflowRegistryError("policy kind must be aggregation, comparison, report, or decision")
|
|
371
|
+
if callback is not None and not callable(callback):
|
|
372
|
+
raise WorkflowRegistryError("policy callback must be callable")
|
|
373
|
+
definition = CallbackDefinition(policy_id, version, implementation_id, kind,
|
|
374
|
+
metadata if metadata is not None else {})
|
|
375
|
+
self._register("policy", policy_id, definition, definition, callback)
|
|
376
|
+
return definition
|
|
377
|
+
|
|
378
|
+
def get_policy(self, policy_id: str, *, kind: str | None = None) -> CallbackDefinition:
|
|
379
|
+
registration = self._get("policy", policy_id)
|
|
380
|
+
self._unchanged(registration, registration.value, "policy")
|
|
381
|
+
if kind is not None and registration.definition.kind != kind:
|
|
382
|
+
raise WorkflowRegistryError(f"policy {policy_id} has incompatible kind")
|
|
383
|
+
return registration.value
|
|
384
|
+
|
|
385
|
+
def get_policy_callable(self, policy_id: str, *, kind: str | None = None) -> Callable[..., Any]:
|
|
386
|
+
self.get_policy(policy_id, kind=kind)
|
|
387
|
+
callback = self._get("policy", policy_id).callback
|
|
388
|
+
if callback is None:
|
|
389
|
+
raise WorkflowRegistryError("policy execution is backend-owned; no callback is registered")
|
|
390
|
+
return callback
|
|
391
|
+
|
|
392
|
+
def list_policies(self) -> tuple[CallbackDefinition, ...]:
|
|
393
|
+
return self._definitions("policy")
|
|
394
|
+
|
|
395
|
+
def register_projection(self, schema_id: str, projector: Callable[..., Any], *,
|
|
396
|
+
version: str, implementation_id: str,
|
|
397
|
+
metadata: Mapping[str, Any] | None = None) -> CallbackDefinition:
|
|
398
|
+
self.get_schema(schema_id)
|
|
399
|
+
if not callable(projector):
|
|
400
|
+
raise WorkflowRegistryError("projection callback must be callable")
|
|
401
|
+
definition = CallbackDefinition(schema_id, version, implementation_id, "projection",
|
|
402
|
+
metadata if metadata is not None else {})
|
|
403
|
+
self._register("projection", schema_id, definition, definition, projector)
|
|
404
|
+
return definition
|
|
405
|
+
|
|
406
|
+
def get_projection(self, schema_id: str) -> CallbackDefinition:
|
|
407
|
+
registration = self._get("projection", schema_id)
|
|
408
|
+
self._unchanged(registration, registration.value, "projection")
|
|
409
|
+
self.get_schema(schema_id)
|
|
410
|
+
return registration.value
|
|
411
|
+
|
|
412
|
+
def get_projection_callable(self, schema_id: str) -> Callable[..., Any]:
|
|
413
|
+
self.get_projection(schema_id)
|
|
414
|
+
callback = self._get("projection", schema_id).callback
|
|
415
|
+
assert callback is not None
|
|
416
|
+
return callback
|
|
417
|
+
|
|
418
|
+
def list_projections(self) -> tuple[CallbackDefinition, ...]:
|
|
419
|
+
return self._definitions("projection")
|
|
420
|
+
|
|
421
|
+
def resolve_suite(self, suite_id: str, *, scope: str | None = None,
|
|
422
|
+
mode: str | None = None) -> tuple[SuiteDefinition, WorkflowBackend]:
|
|
423
|
+
"""Bind pure metadata and check capabilities; do not admit execution."""
|
|
424
|
+
suite = self.get_suite(suite_id)
|
|
425
|
+
backend = self.get_backend(suite.backend_id)
|
|
426
|
+
descriptor = self._get("backend", suite.backend_id).definition
|
|
427
|
+
all_required_capabilities = set(suite.required_capabilities)
|
|
428
|
+
if descriptor.suite_ids and suite.suite_id not in descriptor.suite_ids:
|
|
429
|
+
raise WorkflowRegistryError("backend does not declare the selected suite")
|
|
430
|
+
if scope is not None and scope not in descriptor.supported_scopes:
|
|
431
|
+
raise WorkflowRegistryError(f"backend does not support scope {scope!r}")
|
|
432
|
+
if mode is not None and mode not in descriptor.supported_modes:
|
|
433
|
+
raise WorkflowRegistryError(f"backend does not support mode {mode!r}")
|
|
434
|
+
if not set(suite.required_capabilities).issubset(descriptor.capabilities):
|
|
435
|
+
raise WorkflowRegistryError("backend lacks required suite capabilities")
|
|
436
|
+
from .execution_capabilities import validate_execution_requirement
|
|
437
|
+
try:
|
|
438
|
+
validate_execution_requirement(descriptor, scope=scope, mode=mode,
|
|
439
|
+
capabilities=suite.required_capabilities)
|
|
440
|
+
if "evaluation_protocol" in suite.metadata:
|
|
441
|
+
from .evaluation_protocols import EvaluationProtocol, PROTOCOL_SCHEMA
|
|
442
|
+
protocol = EvaluationProtocol.from_dict(suite.metadata["evaluation_protocol"])
|
|
443
|
+
all_required_capabilities.add(PROTOCOL_SCHEMA)
|
|
444
|
+
validate_execution_requirement(descriptor, scope=scope, mode=mode,
|
|
445
|
+
capabilities=(PROTOCOL_SCHEMA,))
|
|
446
|
+
self.get_schema(protocol.input_schema_id)
|
|
447
|
+
self.get_schema(protocol.output_schema_id)
|
|
448
|
+
if any(item.input_schema_id != protocol.input_schema_id for item in suite.datasets):
|
|
449
|
+
raise WorkflowRegistryError("protocol input schema differs from suite datasets")
|
|
450
|
+
except WorkflowContractError as exc:
|
|
451
|
+
raise WorkflowRegistryError(str(exc)) from None
|
|
452
|
+
for criterion_id in suite.criterion_ids:
|
|
453
|
+
self.get_criterion(criterion_id)
|
|
454
|
+
for metric_id in suite.metric_ids:
|
|
455
|
+
metric = self.get_metric(metric_id)
|
|
456
|
+
if metric.aggregation_policy is not None:
|
|
457
|
+
self.get_policy(metric.aggregation_policy, kind="aggregation")
|
|
458
|
+
if metric.comparison_policy is not None:
|
|
459
|
+
self.get_policy(metric.comparison_policy, kind="comparison")
|
|
460
|
+
for evaluator_id in suite.evaluator_ids:
|
|
461
|
+
evaluator = self.get_evaluator(evaluator_id)
|
|
462
|
+
all_required_capabilities.update(evaluator.required_capabilities)
|
|
463
|
+
if not set(evaluator.required_capabilities).issubset(descriptor.capabilities):
|
|
464
|
+
raise WorkflowRegistryError("backend lacks required evaluator capabilities")
|
|
465
|
+
try:
|
|
466
|
+
validate_execution_requirement(descriptor, scope=scope, mode=mode,
|
|
467
|
+
capabilities=evaluator.required_capabilities)
|
|
468
|
+
except WorkflowContractError as exc:
|
|
469
|
+
raise WorkflowRegistryError(str(exc)) from None
|
|
470
|
+
if not set(evaluator.criterion_ids).issubset(suite.criterion_ids):
|
|
471
|
+
raise WorkflowRegistryError("evaluator refers to criteria outside the suite")
|
|
472
|
+
if not set(evaluator.metric_ids).issubset(suite.metric_ids):
|
|
473
|
+
raise WorkflowRegistryError("evaluator refers to metrics outside the suite")
|
|
474
|
+
for schema_id in evaluator.assessment_schema_ids:
|
|
475
|
+
self.get_schema(schema_id)
|
|
476
|
+
for dataset in suite.datasets:
|
|
477
|
+
registered = self.get_dataset(dataset.dataset_id)
|
|
478
|
+
if contract_digest(registered.to_dict()) != contract_digest(dataset.to_dict()):
|
|
479
|
+
raise WorkflowRegistryError("suite dataset differs from registered definition")
|
|
480
|
+
provider = self.get_provider(dataset.provider_id)
|
|
481
|
+
if contract_digest(provider.definition.to_dict()) != contract_digest(dataset.to_dict()):
|
|
482
|
+
raise WorkflowRegistryError("case provider differs from suite dataset")
|
|
483
|
+
self.get_schema(dataset.input_schema_id)
|
|
484
|
+
if dataset.reference_schema_id is not None:
|
|
485
|
+
self.get_schema(dataset.reference_schema_id)
|
|
486
|
+
try:
|
|
487
|
+
validate_execution_requirement(descriptor, scope=scope, mode=mode,
|
|
488
|
+
capabilities=tuple(sorted(all_required_capabilities)))
|
|
489
|
+
except WorkflowContractError as exc:
|
|
490
|
+
raise WorkflowRegistryError(str(exc)) from None
|
|
491
|
+
described = backend.describe(suite_id)
|
|
492
|
+
if not isinstance(described, SuiteDefinition) or described.definition_digest != suite.definition_digest:
|
|
493
|
+
raise WorkflowRegistryError("backend suite description differs from registered definition")
|
|
494
|
+
self.get_backend(suite.backend_id)
|
|
495
|
+
return suite, backend
|
|
496
|
+
|
|
497
|
+
def verify_suite(self, suite: str | SuiteDefinition, *, scope: str | None = None,
|
|
498
|
+
mode: str | None = None) -> tuple[SuiteDefinition, WorkflowBackend]:
|
|
499
|
+
"""Check a registered suite or require an exact supplied metadata match."""
|
|
500
|
+
if isinstance(suite, SuiteDefinition):
|
|
501
|
+
registered = self.get_suite(suite.suite_id)
|
|
502
|
+
if registered.definition_digest != suite.definition_digest:
|
|
503
|
+
raise WorkflowRegistryError("supplied suite differs from registered definition")
|
|
504
|
+
suite_id = suite.suite_id
|
|
505
|
+
else:
|
|
506
|
+
suite_id = suite
|
|
507
|
+
return self.resolve_suite(suite_id, scope=scope, mode=mode)
|
|
508
|
+
|
|
509
|
+
def to_dict(self) -> dict[str, Any]:
|
|
510
|
+
"""Return registration metadata only; do not call backend/provider code."""
|
|
511
|
+
return {"schema_version": "sphynx.workflow-registry.v1",
|
|
512
|
+
"roles": {role: [item.to_dict() for item in self._definitions(role)]
|
|
513
|
+
for role in self._stores}}
|