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.
Files changed (174) hide show
  1. sable_sphynx-0.7.0.dev0.dist-info/METADATA +152 -0
  2. sable_sphynx-0.7.0.dev0.dist-info/RECORD +174 -0
  3. sable_sphynx-0.7.0.dev0.dist-info/WHEEL +5 -0
  4. sable_sphynx-0.7.0.dev0.dist-info/top_level.txt +1 -0
  5. sphynx/__init__.py +469 -0
  6. sphynx/__init__.pyi +376 -0
  7. sphynx/_analytical_client.py +77 -0
  8. sphynx/_analytical_values.py +252 -0
  9. sphynx/_audit_client.py +173 -0
  10. sphynx/_comparison_adapters.py +113 -0
  11. sphynx/_componentized_full_rag.py +34 -0
  12. sphynx/_composed_pipeline.py +109 -0
  13. sphynx/_full_rag_grading.py +218 -0
  14. sphynx/_full_rag_trace.py +221 -0
  15. sphynx/_local_eval.py +268 -0
  16. sphynx/_pipeline_core.py +243 -0
  17. sphynx/_pr_client.py +237 -0
  18. sphynx/_rag_diagnostics_calibration.py +285 -0
  19. sphynx/_rag_diagnostics_catalog.py +39 -0
  20. sphynx/_rag_diagnostics_common.py +26 -0
  21. sphynx/_rag_diagnostics_contracts.py +186 -0
  22. sphynx/_rag_diagnostics_engine.py +243 -0
  23. sphynx/_rag_diagnostics_importers.py +242 -0
  24. sphynx/_rag_diagnostics_ranking.py +81 -0
  25. sphynx/_rag_diagnostics_semantic.py +325 -0
  26. sphynx/_rag_ranking.py +149 -0
  27. sphynx/_speech_benchmark_scoring.py +140 -0
  28. sphynx/_speech_benchmark_sources.py +275 -0
  29. sphynx/_speech_complete_scoring.py +218 -0
  30. sphynx/_speech_complete_source.py +118 -0
  31. sphynx/_speech_kido.py +294 -0
  32. sphynx/_speech_scoring.py +77 -0
  33. sphynx/_track_capture.py +213 -0
  34. sphynx/analytical_views.py +515 -0
  35. sphynx/assessment_contracts.py +378 -0
  36. sphynx/assessment_selection.py +57 -0
  37. sphynx/audit_access.py +245 -0
  38. sphynx/audit_contracts.py +204 -0
  39. sphynx/audit_requests.py +180 -0
  40. sphynx/authoring.py +188 -0
  41. sphynx/benchmarks.py +568 -0
  42. sphynx/candidates.py +62 -0
  43. sphynx/catalog.py +272 -0
  44. sphynx/chunking.py +372 -0
  45. sphynx/clickhouse_reader.py +169 -0
  46. sphynx/client.py +486 -0
  47. sphynx/comparison_core.py +149 -0
  48. sphynx/comparison_views.py +223 -0
  49. sphynx/components.py +1661 -0
  50. sphynx/contracts.py +514 -0
  51. sphynx/customer_rerank.py +299 -0
  52. sphynx/customer_retrieval.py +146 -0
  53. sphynx/data/benchmark-releases.v1.json +164 -0
  54. sphynx/data/public-benchmark-release.v1.json +354 -0
  55. sphynx/data/rag-diagnostics-synthetic.v1.json +6982 -0
  56. sphynx/data/rag-paper-references.v1.json +176 -0
  57. sphynx/data/rag-paper-schemas.v1.json +1069 -0
  58. sphynx/data/rag-rerank-v1.json +507 -0
  59. sphynx/dataset_catalog_data.py +1214 -0
  60. sphynx/dataset_readiness.py +138 -0
  61. sphynx/dataset_v3.py +999 -0
  62. sphynx/demo.py +130 -0
  63. sphynx/demo_candidates/__init__.py +1 -0
  64. sphynx/demo_candidates/dense.py +11 -0
  65. sphynx/demo_candidates/lexical.py +21 -0
  66. sphynx/diagnostics.py +207 -0
  67. sphynx/documentation.py +737 -0
  68. sphynx/documentation_candidate.py +939 -0
  69. sphynx/evals.py +538 -0
  70. sphynx/evaluation_protocols.py +124 -0
  71. sphynx/evaluator_bindings.py +370 -0
  72. sphynx/evidence.py +352 -0
  73. sphynx/execution_capabilities.py +109 -0
  74. sphynx/fast_search.py +285 -0
  75. sphynx/full_rag.py +341 -0
  76. sphynx/hosted/__init__.py +18 -0
  77. sphynx/hosted/benchmark_intake.py +218 -0
  78. sphynx/hosted/contracts.py +361 -0
  79. sphynx/hosted/model_admission.py +130 -0
  80. sphynx/hosted/model_artifacts.py +298 -0
  81. sphynx/hosted/model_contracts.py +63 -0
  82. sphynx/hosted/model_gcs.py +181 -0
  83. sphynx/hosted/model_history.py +84 -0
  84. sphynx/hosted/model_session.py +233 -0
  85. sphynx/hosted/public_suite.py +74 -0
  86. sphynx/hosted/registry.py +97 -0
  87. sphynx/hosted/revisions.py +126 -0
  88. sphynx/hosted/sandbox.py +105 -0
  89. sphynx/hosted/submission.py +287 -0
  90. sphynx/hosted_workflow.py +451 -0
  91. sphynx/http_transport.py +86 -0
  92. sphynx/integrated_rag.py +352 -0
  93. sphynx/integrated_workflow.py +552 -0
  94. sphynx/integration_contracts.py +474 -0
  95. sphynx/invocation.py +791 -0
  96. sphynx/invocation_candidates.py +392 -0
  97. sphynx/invocation_catalog_data.py +40 -0
  98. sphynx/learned_catalog_data.py +30 -0
  99. sphynx/learned_rerank.py +267 -0
  100. sphynx/legacy_full_rag.py +67 -0
  101. sphynx/lifecycle.py +885 -0
  102. sphynx/lifecycle_workflow.py +490 -0
  103. sphynx/metric_views.py +474 -0
  104. sphynx/metrics.py +282 -0
  105. sphynx/model_adapters.py +438 -0
  106. sphynx/model_catalog_data.py +30 -0
  107. sphynx/official_benchmarks.py +385 -0
  108. sphynx/packs.py +258 -0
  109. sphynx/pipeline.py +313 -0
  110. sphynx/pipeline_workflow.py +551 -0
  111. sphynx/pr_evaluations.py +560 -0
  112. sphynx/progress.py +89 -0
  113. sphynx/public_bundle.py +173 -0
  114. sphynx/public_eval.py +62 -0
  115. sphynx/public_rerank.py +355 -0
  116. sphynx/py.typed +0 -0
  117. sphynx/pytest_plugin.py +62 -0
  118. sphynx/question_robustness.py +437 -0
  119. sphynx/rag.py +329 -0
  120. sphynx/rag_diagnostics.py +309 -0
  121. sphynx/rag_papers/__init__.py +29 -0
  122. sphynx/rag_papers/__main__.py +45 -0
  123. sphynx/rag_papers/candidates.py +19 -0
  124. sphynx/rag_papers/contracts.py +282 -0
  125. sphynx/rag_papers/datasets.py +291 -0
  126. sphynx/rag_papers/experiments.py +116 -0
  127. sphynx/rag_papers/judges.py +308 -0
  128. sphynx/rag_papers/registry.py +33 -0
  129. sphynx/rag_papers/runner.py +176 -0
  130. sphynx/rag_papers/sable.py +135 -0
  131. sphynx/rag_papers/schemas.py +12 -0
  132. sphynx/rag_papers/scoring.py +249 -0
  133. sphynx/rag_papers/semantic.py +288 -0
  134. sphynx/rag_papers/trec.py +80 -0
  135. sphynx/recorded_scores.py +352 -0
  136. sphynx/records.py +124 -0
  137. sphynx/reflex_traces.py +193 -0
  138. sphynx/report_rendering.py +71 -0
  139. sphynx/rerank_family_contracts.py +150 -0
  140. sphynx/rerank_workflow.py +424 -0
  141. sphynx/research_candidate.py +190 -0
  142. sphynx/results.py +852 -0
  143. sphynx/retrieval.py +750 -0
  144. sphynx/retrieval_candidates.py +295 -0
  145. sphynx/retrieval_catalog_data.py +19 -0
  146. sphynx/retrieval_recordings.py +125 -0
  147. sphynx/reviewer_audit.py +266 -0
  148. sphynx/routing_policies.py +375 -0
  149. sphynx/runner.py +544 -0
  150. sphynx/sable_rerank.py +489 -0
  151. sphynx/score_reliability.py +393 -0
  152. sphynx/speech_benchmarks.py +204 -0
  153. sphynx/speech_campaigns.py +378 -0
  154. sphynx/speech_complete.py +236 -0
  155. sphynx/speech_contracts.py +225 -0
  156. sphynx/speech_evidence.py +429 -0
  157. sphynx/speech_listening.py +115 -0
  158. sphynx/speech_multilingual.py +71 -0
  159. sphynx/speech_tts.py +479 -0
  160. sphynx/sql/001_sync_v1.sql +30 -0
  161. sphynx/stage_contracts.py +199 -0
  162. sphynx/statistical_catalog_data.py +53 -0
  163. sphynx/statistical_rerank.py +232 -0
  164. sphynx/sync.py +797 -0
  165. sphynx/workbench.py +378 -0
  166. sphynx/workbench_catalog_data.py +13 -0
  167. sphynx/workbench_tracks.py +78 -0
  168. sphynx/workflow_async.py +197 -0
  169. sphynx/workflow_contracts.py +594 -0
  170. sphynx/workflow_decisions.py +211 -0
  171. sphynx/workflow_defaults.py +16 -0
  172. sphynx/workflow_extensions.py +212 -0
  173. sphynx/workflow_registry.py +513 -0
  174. sphynx/workflows.py +792 -0
sphynx/workbench.py ADDED
@@ -0,0 +1,378 @@
1
+ """Pinned native RAG batches with validation and explicit execution.
2
+
3
+ The canonical batch calls retain the existing coordinator and record formats.
4
+ Native experiment names remain compatible entry points and wire identities.
5
+ """
6
+ from __future__ import annotations
7
+ import math
8
+ from dataclasses import dataclass, field
9
+ from collections.abc import Mapping
10
+ from types import MappingProxyType
11
+ from typing import Any, cast
12
+ from .components import *
13
+ from .dataset_v3 import DatasetV3, PARTITIONS, Partition, validate_dataset_v3
14
+ JsonValue = Any
15
+ CONTEXT_CONDITIONS = frozenset({'oracle', 'dense', 'a0', 'challenger', 'empty'})
16
+ WORKBENCH_WORKLOAD_SCHEMA_VERSION = 'sable.rag.workbench-workload.v1'
17
+ class WorkbenchError(ValueError):
18
+ """Raised when a native RAG batch cannot execute or support comparison."""
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class Level3ClaimTruth:
22
+ """An exact answer phrase and the frozen evidence IDs that support it."""
23
+ text: str
24
+ supported_by_evidence_ids: tuple[str, ...] = ()
25
+
26
+ @classmethod
27
+ def from_mapping(cls, raw: object, context: str) -> Level3ClaimTruth:
28
+ value = _mapping(raw, context)
29
+ if set(value) != {'text', 'supported_by_evidence_ids'}:
30
+ raise WorkbenchError(f'{context} keys must be exactly text, supported_by_evidence_ids')
31
+ return cls(_text(value['text'], f'{context}.text'), _strings(value['supported_by_evidence_ids'], f'{context}.supported_by_evidence_ids'))
32
+
33
+ def __post_init__(self) -> None:
34
+ _text(self.text, 'Level 3 claim text')
35
+ evidence_ids = _sequence_values(self.supported_by_evidence_ids, 'claim evidence IDs')
36
+ object.__setattr__(self, 'supported_by_evidence_ids', tuple(_text(item, 'claim evidence ID') for item in evidence_ids))
37
+ if len(self.supported_by_evidence_ids) != len(set(self.supported_by_evidence_ids)):
38
+ raise WorkbenchError('Level 3 claim evidence IDs must be unique')
39
+
40
+ def to_mapping(self) -> dict[str, JsonValue]:
41
+ return {'text': self.text, 'supported_by_evidence_ids': list(self.supported_by_evidence_ids)}
42
+
43
+ @dataclass(frozen=True, slots=True)
44
+ class Level3TaskGrading:
45
+ """Sealed deterministic semantic truth for one full-RAG task."""
46
+ required_facts: Mapping[str, tuple[str, ...]] = field(default_factory=dict)
47
+ exact_values: tuple[str, ...] = ()
48
+ claims: tuple[Level3ClaimTruth, ...] = ()
49
+
50
+ def __post_init__(self) -> None:
51
+ _mapping(self.required_facts, 'Level 3 required facts')
52
+ _sequence_values(self.exact_values, 'Level 3 exact values')
53
+ _sequence_values(self.claims, 'Level 3 claims')
54
+ facts: dict[str, tuple[str, ...]] = {}
55
+ for raw_fact_id, raw_phrases in self.required_facts.items():
56
+ fact_id = _text(raw_fact_id, 'Level 3 required fact ID')
57
+ phrases = tuple((_text(phrase, f'Level 3 required fact {fact_id} phrase') for phrase in _sequence_values(raw_phrases, 'required fact phrases')))
58
+ if not phrases or len(phrases) != len(set(phrases)):
59
+ raise WorkbenchError(f'Level 3 required fact {fact_id} needs unique accepted phrases')
60
+ facts[fact_id] = phrases
61
+ exact_values = tuple((_text(value, 'Level 3 exact value') for value in self.exact_values))
62
+ if len(exact_values) != len(set(exact_values)):
63
+ raise WorkbenchError('Level 3 exact values must be unique')
64
+ if any((not isinstance(claim, Level3ClaimTruth) for claim in self.claims)):
65
+ raise WorkbenchError('Level 3 claims must be Level3ClaimTruth values')
66
+ claim_texts = [claim.text for claim in self.claims]
67
+ if len(claim_texts) != len(set(claim_texts)):
68
+ raise WorkbenchError('Level 3 claim texts must be unique')
69
+ object.__setattr__(self, 'required_facts', MappingProxyType(dict(sorted(facts.items()))))
70
+ object.__setattr__(self, 'exact_values', exact_values)
71
+ object.__setattr__(self, 'claims', tuple(self.claims))
72
+
73
+ @classmethod
74
+ def from_mapping(cls, raw: object, context: str) -> Level3TaskGrading:
75
+ value = _mapping(raw, context)
76
+ if set(value) != {'required_facts', 'exact_values', 'claims'}:
77
+ raise WorkbenchError(f'{context} keys must be exactly claims, exact_values, required_facts')
78
+ raw_facts = _mapping(value['required_facts'], f'{context}.required_facts')
79
+ facts = {fact_id: _strings(phrases, f'{context}.required_facts.{fact_id}') for fact_id, phrases in raw_facts.items()}
80
+ raw_claims = value['claims']
81
+ if not isinstance(raw_claims, list):
82
+ raise WorkbenchError(f'{context}.claims must be an array')
83
+ return cls(required_facts=facts, exact_values=_strings(value['exact_values'], f'{context}.exact_values'), claims=tuple((Level3ClaimTruth.from_mapping(claim, f'{context}.claims[{index}]') for index, claim in enumerate(raw_claims))))
84
+
85
+ def to_mapping(self) -> dict[str, JsonValue]:
86
+ return {'required_facts': {fact_id: list(phrases) for fact_id, phrases in sorted(self.required_facts.items())}, 'exact_values': list(self.exact_values), 'claims': [claim.to_mapping() for claim in self.claims]}
87
+
88
+ @dataclass(frozen=True, slots=True)
89
+ class WorkbenchWorkload:
90
+ """Validated data selection plus optional forced Level 3 contexts."""
91
+ dataset: DatasetV3
92
+ customer_ids: tuple[str, ...] = ()
93
+ partitions: tuple[Partition, ...] = ('development',)
94
+ level3_context_condition: str = 'dense'
95
+ level3_contexts: Mapping[str, tuple[str, ...]] = field(default_factory=dict)
96
+ level3_grading: Mapping[str, Level3TaskGrading] = field(default_factory=dict)
97
+ _dataset_sha256: str = field(init=False, repr=False)
98
+ _level3_grading_sha256: str = field(init=False, repr=False)
99
+
100
+ def __post_init__(self) -> None:
101
+ if type(self.dataset) is not DatasetV3:
102
+ raise WorkbenchError('workload requires a validated DatasetV3')
103
+ _sequence_values(self.customer_ids, 'customer IDs')
104
+ _sequence_values(self.partitions, 'partitions')
105
+ _mapping(self.level3_contexts, 'level3 contexts')
106
+ _mapping(self.level3_grading, 'level3 grading')
107
+ _text(self.level3_context_condition, 'level3 context condition')
108
+ object.__setattr__(self, 'customer_ids', tuple(sorted((_text(customer_id, 'workload customer ID') for customer_id in self.customer_ids))))
109
+ object.__setattr__(self, 'partitions', tuple(sorted(_text(item, 'partition') for item in self.partitions)))
110
+ contexts: dict[str, tuple[str, ...]] = {}
111
+ for raw_task_id, raw_candidate_ids in self.level3_contexts.items():
112
+ task_id = _text(raw_task_id, 'level3 context task ID')
113
+ contexts[task_id] = tuple((_text(candidate_id, f'level3 contexts for {task_id}') for candidate_id in _sequence_values(raw_candidate_ids, 'context candidate IDs')))
114
+ object.__setattr__(self, 'level3_contexts', MappingProxyType(contexts))
115
+ grading: dict[str, Level3TaskGrading] = {}
116
+ for raw_task_id, truth in self.level3_grading.items():
117
+ task_id = _text(raw_task_id, 'Level 3 grading task ID')
118
+ if not isinstance(truth, Level3TaskGrading):
119
+ raise WorkbenchError('Level 3 grading values must be Level3TaskGrading values')
120
+ grading[task_id] = truth
121
+ object.__setattr__(self, 'level3_grading', MappingProxyType(dict(sorted(grading.items()))))
122
+ object.__setattr__(self, '_dataset_sha256', json_sha256(self.dataset.value))
123
+ object.__setattr__(self, '_level3_grading_sha256', json_sha256({task_id: truth.to_mapping() for task_id, truth in sorted(grading.items())}))
124
+ if self.level3_context_condition not in CONTEXT_CONDITIONS:
125
+ raise WorkbenchError('Level 3 context condition is invalid')
126
+ if len(self.customer_ids) != len(set(self.customer_ids)):
127
+ raise WorkbenchError('workload customer IDs must be unique')
128
+ if not self.partitions or len(self.partitions) != len(set(self.partitions)):
129
+ raise WorkbenchError('workload partitions must be non-empty and unique')
130
+ if not set(self.partitions) <= PARTITIONS:
131
+ raise WorkbenchError('workload contains an invalid partition')
132
+ available_partitions = {identity.partition for identity in self.dataset.tasks}
133
+ missing_partitions = set(self.partitions) - available_partitions
134
+ if missing_partitions:
135
+ raise WorkbenchError(f'workload partitions are absent from the dataset: {sorted(missing_partitions)}')
136
+ known = {identity.customer_id for identity in self.dataset.tasks}
137
+ unknown = set(self.customer_ids) - known
138
+ if unknown:
139
+ raise WorkbenchError(f'workload references unknown customers: {sorted(unknown)}')
140
+ known_tasks = {identity.task_id for identity in self.dataset.tasks}
141
+ unknown_tasks = set(self.level3_contexts) - known_tasks
142
+ if unknown_tasks:
143
+ raise WorkbenchError(f'Level 3 contexts reference unknown tasks: {sorted(unknown_tasks)}')
144
+ for task_id, candidate_ids in self.level3_contexts.items():
145
+ if len(candidate_ids) != len(set(candidate_ids)):
146
+ raise WorkbenchError(f'Level 3 contexts for {task_id} contain duplicate IDs')
147
+ unknown_grading_tasks = set(self.level3_grading) - known_tasks
148
+ if unknown_grading_tasks:
149
+ raise WorkbenchError(f'Level 3 grading references unknown tasks: {sorted(unknown_grading_tasks)}')
150
+ raw_tasks = {_text(task.get('task_id'), 'dataset task ID'): task for task in _rows(_mapping(self.dataset.value, 'dataset').get('tasks'), 'dataset.tasks')}
151
+ for task_id, truth in self.level3_grading.items():
152
+ known_candidate_ids = {_text(candidate.get('candidate_id'), f'{task_id} candidate ID') for candidate in _rows(raw_tasks[task_id].get('candidates'), 'task.candidates')}
153
+ cited_ids = {evidence_id for claim in truth.claims for evidence_id in claim.supported_by_evidence_ids}
154
+ if not cited_ids <= known_candidate_ids:
155
+ raise WorkbenchError(f'Level 3 grading for {task_id} references unknown evidence IDs: {sorted(cited_ids - known_candidate_ids)}')
156
+
157
+ @property
158
+ def dataset_sha256(self) -> str:
159
+ return self._dataset_sha256
160
+
161
+ @property
162
+ def level3_grading_sha256(self) -> str:
163
+ return self._level3_grading_sha256
164
+
165
+ @classmethod
166
+ def from_mapping(cls, dataset: DatasetV3, raw: object) -> WorkbenchWorkload:
167
+ """Load the strict JSON control document used by CLI or service front ends."""
168
+ root = _mapping(raw, 'workbench workload')
169
+ required = {'schema_version', 'dataset', 'customer_ids', 'partitions', 'level3_context_condition', 'level3_contexts'}
170
+ allowed = required | {'level3_grading'}
171
+ if not required <= set(root) or not set(root) <= allowed:
172
+ raise WorkbenchError('workbench workload keys must be exactly the required control fields, with optional level3_grading')
173
+ if root['schema_version'] != WORKBENCH_WORKLOAD_SCHEMA_VERSION:
174
+ raise WorkbenchError('workbench workload schema_version is unsupported')
175
+ dataset_commitment = _mapping(root['dataset'], 'workbench workload.dataset')
176
+ if set(dataset_commitment) != {'dataset_id', 'version', 'sha256'}:
177
+ raise WorkbenchError('workbench workload.dataset fields differ')
178
+ expected_dataset = {'dataset_id': dataset.dataset_id, 'version': dataset.version, 'sha256': json_sha256(dataset.value)}
179
+ if dict(dataset_commitment) != expected_dataset:
180
+ raise WorkbenchError('workbench workload does not bind the supplied dataset')
181
+ contexts_raw = _mapping(root['level3_contexts'], 'level3_contexts')
182
+ contexts = {task_id: _strings(candidate_ids, f'level3_contexts.{task_id}') for task_id, candidate_ids in contexts_raw.items()}
183
+ grading_raw = _mapping(root.get('level3_grading', {}), 'level3_grading')
184
+ grading = {task_id: Level3TaskGrading.from_mapping(truth, f'level3_grading.{task_id}') for task_id, truth in grading_raw.items()}
185
+ partitions = _strings(root['partitions'], 'partitions')
186
+ return cls(dataset=dataset, customer_ids=_strings(root['customer_ids'], 'customer_ids'), partitions=cast('tuple[Partition, ...]', partitions), level3_context_condition=_text(root['level3_context_condition'], 'level3_context_condition'), level3_contexts=contexts, level3_grading=grading)
187
+
188
+ def to_mapping(self) -> dict[str, JsonValue]:
189
+ result: dict[str, JsonValue] = {'schema_version': WORKBENCH_WORKLOAD_SCHEMA_VERSION, 'dataset': {'dataset_id': self.dataset.dataset_id, 'version': self.dataset.version, 'sha256': self.dataset_sha256}, 'customer_ids': list(self.customer_ids), 'partitions': list(self.partitions), 'level3_context_condition': self.level3_context_condition, 'level3_contexts': {task_id: list(candidate_ids) for task_id, candidate_ids in sorted(self.level3_contexts.items())}}
190
+ if self.level3_grading:
191
+ result['level3_grading'] = {task_id: truth.to_mapping() for task_id, truth in sorted(self.level3_grading.items())}
192
+ return result
193
+
194
+ def _sequence_values(value, context):
195
+ if not isinstance(value, (list, tuple)):
196
+ raise WorkbenchError(f'{context} must be a list or tuple')
197
+ return tuple(value)
198
+
199
+
200
+ def _mapping(value: object, context: str) -> Mapping[str, object]:
201
+ if not isinstance(value, Mapping) or any((not isinstance(key, str) for key in value)):
202
+ raise WorkbenchError(f'{context} must be an object')
203
+ return cast('Mapping[str, object]', value)
204
+
205
+ def _json_value(value: object, context: str) -> JsonValue:
206
+ """Validate and detach an arbitrary value at the adapter/data boundary."""
207
+ if value is None or isinstance(value, bool | int | str):
208
+ return value
209
+ if isinstance(value, float):
210
+ if not math.isfinite(value):
211
+ raise WorkbenchError(f'{context} contains a non-finite number')
212
+ return value
213
+ if isinstance(value, list | tuple):
214
+ return [_json_value(item, f'{context}[]') for item in value]
215
+ if isinstance(value, Mapping):
216
+ result: dict[str, JsonValue] = {}
217
+ for key, item in value.items():
218
+ if not isinstance(key, str):
219
+ raise WorkbenchError(f'{context} contains a non-string object key')
220
+ result[key] = _json_value(item, f'{context}.{key}')
221
+ return result
222
+ raise WorkbenchError(f'{context} is not JSON-compatible')
223
+
224
+ def _rows(value: object, context: str) -> list[Mapping[str, object]]:
225
+ if not isinstance(value, list):
226
+ raise WorkbenchError(f'{context} must be an array')
227
+ return [_mapping(row, f'{context}[{index}]') for index, row in enumerate(value)]
228
+
229
+ def _strings(value: object, context: str) -> tuple[str, ...]:
230
+ if not isinstance(value, list):
231
+ raise WorkbenchError(f'{context} must be an array')
232
+ return tuple((_text(item, f'{context}[]') for item in value))
233
+
234
+ def _text(value: object, context: str) -> str:
235
+ if not isinstance(value, str) or not value:
236
+ raise WorkbenchError(f'{context} must be non-empty text')
237
+ return value
238
+
239
+ def _tasks(workload: WorkbenchWorkload) -> tuple[Mapping[str, object], ...]:
240
+ if json_sha256(workload.dataset.value) != workload.dataset_sha256:
241
+ raise WorkbenchError('workload dataset changed after it was frozen')
242
+ raw = _mapping(workload.dataset.value, 'dataset')
243
+ selected = set(workload.customer_ids)
244
+ tasks = tuple((task for task in _rows(raw.get('tasks'), 'dataset.tasks') if (not selected or task.get('customer_id') in selected) and task.get('partition') in workload.partitions))
245
+ if not tasks:
246
+ raise WorkbenchError('workload selection contains no tasks')
247
+ return tasks
248
+
249
+ from pathlib import Path
250
+ from .integration_contracts import CandidateConfiguration, value_digest
251
+ from .dataset_v3 import load_dataset_v3
252
+ from ._track_capture import strict_json
253
+
254
+ @dataclass(frozen=True)
255
+ class WorkbenchExecutionPolicy:
256
+ """Explicit caller permissions and already registered adapters; no dynamic imports."""
257
+ registry: ComponentRegistry
258
+ allowed_implementation_kinds: tuple[str, ...] = ('builtin', 'recorded')
259
+ synthetic: bool = False
260
+ owner_authorized: bool = False
261
+ pack_manifest_path: Path | None = None
262
+ expected_sha256: str | None = None
263
+ # Downstream tracks may supply an explicit ordinary Python runner.
264
+ track_runners: Mapping = field(default_factory=dict)
265
+
266
+
267
+ def _validated(components, experiment, dataset, workload):
268
+ if not isinstance(components, ComponentManifest):
269
+ components=validate_component_manifest(components)
270
+ else:
271
+ components=validate_component_manifest(components.value)
272
+ if isinstance(experiment, ExperimentManifest): experiment=experiment.value
273
+ experiment=validate_experiment_manifest(experiment,components)
274
+ if not isinstance(dataset,DatasetV3):
275
+ dataset=load_dataset_v3(dataset) if isinstance(dataset,(str,Path)) else validate_dataset_v3(dataset)
276
+ else:
277
+ dataset=validate_dataset_v3(dataset.value)
278
+ if isinstance(workload,WorkbenchWorkload): workload=workload.to_mapping()
279
+ workload=WorkbenchWorkload.from_mapping(dataset,workload)
280
+ tasks=_tasks(workload)
281
+ # Source workload validates duplicate IDs; also reject context references outside a case.
282
+ for task in tasks:
283
+ supplied=set(workload.level3_contexts.get(task['task_id'],()))
284
+ known={c['candidate_id'] for c in task['candidates']}
285
+ if not supplied<=known:
286
+ raise WorkbenchError('workload context references unknown evidence')
287
+ return components,experiment,dataset,workload,tasks
288
+
289
+
290
+ def validate_experiment(components, experiment, dataset, workload):
291
+ """Validate frozen controls without constructing, importing or executing adapters."""
292
+ components,experiment,dataset,workload,tasks=_validated(components,experiment,dataset,workload)
293
+ selected={r.component_id for cfg in (experiment.baseline,*(a.configuration for a in experiment.alternatives)) for r in cfg.components}
294
+ external=sorted(c.component_id for c in components.components if c.component_id in selected and c.implementation.kind=='command')
295
+ return {'valid':True,'dataset_digest':'sha256:'+workload.dataset_sha256,
296
+ 'workload_digest':value_digest(workload.to_mapping()),
297
+ 'component_manifest_digest':'sha256:'+components.sha256,
298
+ 'experiment_digest':'sha256:'+experiment.sha256,
299
+ 'case_ids':[t['task_id'] for t in tasks],
300
+ 'targets':[{'level':t.level,'track':t.track} for t in experiment.targets],
301
+ 'external_components':external,
302
+ 'execution_ready':False,
303
+ 'reason':'Execution needs explicit policy and manifest-bound adapter registrations.'}
304
+
305
+
306
+ def run_experiment(components, experiment, dataset, workload, output_dir, *, execution_policy):
307
+ """Execute the fixed baseline and declared alternatives under explicit permissions."""
308
+ from .chunking import run_chunking, grade_chunking
309
+ if not isinstance(execution_policy,WorkbenchExecutionPolicy):
310
+ raise WorkbenchError('explicit WorkbenchExecutionPolicy is required')
311
+ if not isinstance(dataset,(str,Path)) or execution_policy.expected_sha256 is None:
312
+ raise WorkbenchError('execution requires an external pinned dataset path')
313
+ dataset_path=Path(dataset)
314
+ data=load_dataset_v3(dataset_path,expected_sha256=execution_policy.expected_sha256)
315
+ components,experiment,data,workload,tasks=_validated(components,experiment,data,workload)
316
+ policy=execution_policy
317
+ if policy.registry.manifest.sha256!=components.sha256:
318
+ raise WorkbenchError('runtime registry differs from frozen catalog')
319
+ configurations=(experiment.baseline,*(a.configuration for a in experiment.alternatives))
320
+ for configuration in configurations:
321
+ for ref in configuration.components:
322
+ component=components.by_id[ref.component_id]
323
+ if component.implementation.kind not in policy.allowed_implementation_kinds:
324
+ raise WorkbenchError('component execution kind is not authorized')
325
+ adapter=policy.registry.resolve(ref.component_id,component.capabilities[0],OPERATIONS_BY_KIND[component.kind][0])
326
+ if adapter.descriptor!=AdapterDescriptor.from_component(component):
327
+ raise WorkbenchError('registered adapter identity drift')
328
+ for target in experiment.targets:
329
+ if target.track!='chunking' and target.track not in policy.track_runners:
330
+ raise WorkbenchError('target requires an explicit registered track runner: '+target.track)
331
+ # One native batch boundary; component changes are captured separately.
332
+ boundary=value_digest({'experiment':experiment.value,'workload':workload.to_mapping()})
333
+ results=[]
334
+ for configuration in configurations:
335
+ for target in experiment.targets:
336
+ if target.track=='chunking':
337
+ component=components.by_id[configuration.by_kind['chunker'].component_id]
338
+ registered=policy.registry.resolve(component.component_id,target,'chunk')
339
+ candidate=CandidateConfiguration(configuration.configuration_id,'sphynx.chunk.v1',
340
+ 'sha256:'+component.implementation.artifact_sha256,
341
+ {'component_id':component.component_id,'config':component.config,
342
+ 'workload_digest':value_digest(workload.to_mapping())},'trusted_local')
343
+ def adapter(request, registered=registered):
344
+ output=registered.invoke('chunk',{'documents':[
345
+ {'document_id':d['document_id'],'text':d['text']} for d in request['documents']]})
346
+ return {'schema_version':'sphynx.chunk.response.v1','case_id':request['case_id'],'chunks':[{key:row[key] for key in ('chunk_id','document_id','start','end','text')} for row in output['chunks']]}
347
+ run=run_chunking(candidate,adapter,dataset_path,output_dir,
348
+ expected_sha256=policy.expected_sha256,synthetic=policy.synthetic,
349
+ owner_authorized=policy.owner_authorized,pack_manifest_path=policy.pack_manifest_path,
350
+ case_ids=[t['task_id'] for t in tasks],boundary_digest=boundary)
351
+ evaluation=grade_chunking(run,dataset_path,expected_sha256=policy.expected_sha256)
352
+ results.append({'configuration_id':configuration.configuration_id,'track':target.track,
353
+ 'run':run,'evaluation':evaluation})
354
+ else:
355
+ result=policy.track_runners[target.track](components=components,configuration=configuration,
356
+ registry=policy.registry,dataset_path=dataset_path,workload=workload,
357
+ output_dir=output_dir,execution_policy=policy,boundary_digest=boundary)
358
+ results.append({'configuration_id':configuration.configuration_id,'track':target.track,'result':result})
359
+ return tuple(results)
360
+
361
+
362
+ def validate_batch(components, batch, dataset, workload):
363
+ """Validate a native RAG batch without executing adapters.
364
+
365
+ This is the canonical spelling of ``validate_experiment``. Returned native
366
+ field names, including ``experiment_digest``, retain their original meaning.
367
+ """
368
+ return validate_experiment(components, batch, dataset, workload)
369
+
370
+
371
+ def run_batch(components, batch, dataset, workload, output_dir, *, execution_policy):
372
+ """Run the native baseline and alternatives under the existing policy.
373
+
374
+ This call delegates to ``run_experiment``. It does not add common-workflow
375
+ pipeline substitution, new execution targets, or a new record schema.
376
+ """
377
+ return run_experiment(components, batch, dataset, workload, output_dir,
378
+ execution_policy=execution_policy)
@@ -0,0 +1,13 @@
1
+ """Metadata-only component candidates; owner data stays external."""
2
+ from .catalog import CandidateDescriptor
3
+ DATASETS = () # The eleven blocked customer rows already belong to I-02.
4
+ CANDIDATES = (
5
+ CandidateDescriptor('workbench.synthetic-chunker.v1', 'rag_chunk', 'sphynx.chunk.v1',
6
+ 'sha256:f38dfca7d06360fe5e024b0d03dd326da7f324ac069de15df4776865e5dcb47d', (), ('trusted_local',), 'available',
7
+ local_prerequisites=('Explicit synthetic workload and a pinned component manifest.',),
8
+ supported_python=('>=3.11',)),
9
+ CandidateDescriptor('workbench.recorded-component.v1', 'rag_workbench', 'sable.rag.component.v1',
10
+ 'sha256:f38dfca7d06360fe5e024b0d03dd326da7f324ac069de15df4776865e5dcb47d', (), ('recorded',), 'available',
11
+ local_prerequisites=('Complete request-bound recordings and a pinned component manifest.',),
12
+ supported_python=('>=3.11',)),
13
+ )
@@ -0,0 +1,78 @@
1
+ """Explicit Level 2/3 runners for the existing declarative workbench.
2
+
3
+ The seven-component pipeline begins with source documents. The componentized
4
+ Level 3 diagnostic begins with a frozen context and calls router, rewriter and
5
+ generator; it is not an integrated production retriever or opaque system.
6
+ """
7
+ from .components import AdapterDescriptor
8
+ from .integration_contracts import CandidateConfiguration,value_digest
9
+ from .pipeline import run_pipeline,grade_pipeline,INTERFACE as PIPELINE_INTERFACE
10
+ from .full_rag import run_full_rag,grade_full_rag,INTERFACE as FULL_INTERFACE
11
+ from ._composed_pipeline import compose_pipeline
12
+ from ._componentized_full_rag import compose_full_rag
13
+ from .workbench import WorkbenchError,_tasks
14
+
15
+
16
+ def _invoker(components,configuration,registry):
17
+ def invoke(kind,operation,payload):
18
+ ref=configuration.by_kind.get(kind)
19
+ if ref is None:raise WorkbenchError("configuration lacks a required component: "+kind)
20
+ component=components.by_id[ref.component_id]
21
+ adapter=registry.resolve(ref.component_id,component.capabilities[0],operation)
22
+ if adapter.descriptor!=AdapterDescriptor.from_component(component):
23
+ raise WorkbenchError("registered adapter identity differs from the frozen component")
24
+ return adapter.invoke(operation,payload)
25
+ return invoke
26
+
27
+
28
+ def _configuration(components,configuration):
29
+ records={row["component_id"]:row for row in components.value["components"]}
30
+ return {ref.kind:records[ref.component_id] for ref in configuration.components}
31
+
32
+
33
+ def _options(execution_policy,workload,boundary_digest):
34
+ return {"expected_sha256":execution_policy.expected_sha256,"synthetic":execution_policy.synthetic,
35
+ "owner_authorized":execution_policy.owner_authorized,"pack_manifest_path":execution_policy.pack_manifest_path,
36
+ "case_ids":[task["task_id"] for task in _tasks(workload)],"boundary_digest":boundary_digest}
37
+
38
+
39
+ def run_workbench_pipeline(*,components,configuration,registry,dataset_path,workload,output_dir,execution_policy,boundary_digest):
40
+ selected=_configuration(components,configuration)
41
+ if not {"chunker","embedder","retriever","reranker","fusion","dedup","gate"}<=set(selected):
42
+ raise WorkbenchError("pipeline requires all seven registered components")
43
+ invoke=_invoker(components,configuration,registry)
44
+ gate=components.by_id[configuration.by_kind["gate"].component_id]
45
+ candidate=CandidateConfiguration(configuration.configuration_id,PIPELINE_INTERFACE,value_digest(selected),
46
+ {"controlled_config":{"top_k":gate.config.get("top_k",5),"chunking":selected["chunker"],"embedding":selected["embedder"],
47
+ "retrieval":selected["retriever"],"reranker":selected["reranker"],"fusion":selected["fusion"],
48
+ "dedup":selected["dedup"],"gating":selected["gate"]}},"trusted_local")
49
+ run=run_pipeline(candidate,lambda request:compose_pipeline(request,invoke),dataset_path,output_dir,
50
+ boundary="composed_source_documents",**_options(execution_policy,workload,boundary_digest))
51
+ evaluation=grade_pipeline(run,dataset_path,expected_sha256=execution_policy.expected_sha256)
52
+ return {"run":run,"evaluation":evaluation,"boundary":"composed_source_documents"}
53
+
54
+
55
+ def run_workbench_full_rag(*,components,configuration,registry,dataset_path,workload,output_dir,execution_policy,boundary_digest):
56
+ selected=_configuration(components,configuration);invoke=_invoker(components,configuration,registry)
57
+ subject=components.by_id[configuration.by_kind["full_rag_subject"].component_id]
58
+ mode=subject.config.get("workbench_execution_mode","opaque")
59
+ if mode not in ("opaque","componentized"):raise WorkbenchError("full-RAG execution shape differs")
60
+ if mode=="componentized" and not {"router","rewriter","generator"}<=set(selected):
61
+ raise WorkbenchError("componentized full-RAG requires router, rewriter and generator")
62
+ generator=selected["generator"] if mode=="componentized" else selected["full_rag_subject"]
63
+ candidate=CandidateConfiguration(configuration.configuration_id,FULL_INTERFACE,value_digest(selected),
64
+ {"generator_digest":value_digest(generator),"prompt_digest":value_digest({"generator_config":generator["config"],"boundary":"fixed-context"}),
65
+ "components":selected},"trusted_local")
66
+ if mode=="componentized":adapter=lambda request:compose_full_rag(request,invoke)
67
+ else:adapter=lambda request:invoke("full_rag_subject","full_rag",request)
68
+ run=run_full_rag(candidate,adapter,dataset_path,output_dir,execution_shape=mode,
69
+ context_condition=workload.level3_context_condition,contexts=dict(workload.level3_contexts) or None,
70
+ **_options(execution_policy,workload,boundary_digest))
71
+ evaluation=grade_full_rag(run,dataset_path,expected_sha256=execution_policy.expected_sha256)
72
+ return {"run":run,"evaluation":evaluation,"execution_shape":mode,
73
+ "semantic_grading":"capture_bound_review_required","predeclared_workload_truth_digest":"sha256:"+workload.level3_grading_sha256}
74
+
75
+
76
+ def workbench_track_runners():
77
+ """Pass explicitly to WorkbenchExecutionPolicy(track_runners=...)."""
78
+ return {"pipeline":run_workbench_pipeline,"full_rag":run_workbench_full_rag}