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
@@ -0,0 +1,392 @@
1
+ """Portable, cohort-bound invocation candidates and stdin protocol adapters.
2
+
3
+ Recordings are owner supplied and pinned, never discovered relative to source.
4
+ Credentials are resolved at execution and never persisted or placed in argv.
5
+ The protocol subprocess accepts a configuration artifact path and exact digest;
6
+ this is an SDK candidate protocol, not a user-facing Sphynx command line.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import json
12
+ import os
13
+ import re
14
+ import sys
15
+ import time
16
+ import urllib.request
17
+ from collections.abc import Mapping
18
+ from pathlib import Path
19
+ from types import MappingProxyType
20
+ from typing import Any, Callable
21
+
22
+ from .contracts import CandidateSpec, canonical_json
23
+ from .integration_contracts import CandidateConfiguration, require_digest, value_digest
24
+ from .invocation_catalog_data import POLICY_MANIFESTS, BANK_BINDINGS
25
+ from . import routing_policies as policies
26
+
27
+ INTERFACE = "sphynx.rag.invoke.v1"
28
+ RECORDING_SCHEMA = "sphynx.rag.invoke.recording.v1"
29
+ ADAPTER_SCHEMA = "sphynx.rag.invoke.adapter.v1"
30
+ JEV_ENDPOINT = "https://api.typesafe.ai/v1/systemone"
31
+ REVIEW_ENDPOINT = "https://api.openai.com/v1/responses"
32
+ _APPROVED_LIVE_INPUTS = {
33
+ "excerpt-free-200": "sha256:7316862d37a7185ed7c995278a5d5f9ae34ff0ae05afe5c4d281e2c00303d2bf",
34
+ "decision-boundary-80": "sha256:f1aea9ab88c55586ed01a6bf5aee4ac78661ae11727dfea12ecee14da09ea2b6",
35
+ "narrow-gate-20260925-r5": "sha256:c230ad79a9aba6cef17266b83345d273e93eba0871408ce23f87d59e27543194",
36
+ }
37
+ _BINDING_KEYS = {"bank_id", "bank_release", "input_digest", "cohort_digest", "case_ids", "request_digests"}
38
+ _PARAMETER_KEYS = _BINDING_KEYS | {"policy", "excerpt_policy"}
39
+
40
+
41
+ def _file_digest(raw: bytes) -> str:
42
+ return "sha256:" + hashlib.sha256(raw).hexdigest()
43
+
44
+
45
+ def _strict_json(raw: bytes | str) -> Any:
46
+ def unique(pairs):
47
+ obj = {}
48
+ for key, value in pairs:
49
+ if key in obj:
50
+ raise ValueError("duplicate JSON field")
51
+ obj[key] = value
52
+ return obj
53
+ def invalid(value):
54
+ raise ValueError("non-finite JSON value")
55
+ return json.loads(raw, object_pairs_hook=unique, parse_constant=invalid)
56
+
57
+
58
+ def bind_invocation_pack(pack) -> dict[str, Any]:
59
+ """Derive a content-only execution scope from a loaded InvocationPack."""
60
+ requests = [case.request() for case in pack.cases]
61
+ if not requests or len({r["case_id"] for r in requests}) != len(requests):
62
+ raise ValueError("scope must contain unique cases")
63
+ return {
64
+ "bank_id": pack.bank_id, "bank_release": pack.bank_release,
65
+ "input_digest": "sha256:" + pack.input_sha256,
66
+ "cohort_digest": "sha256:" + pack.cohort_sha256,
67
+ "case_ids": [r["case_id"] for r in requests],
68
+ "request_digests": {r["case_id"]: value_digest(r) for r in requests},
69
+ }
70
+
71
+
72
+ def recording_model_snapshot(rows: list[dict], model_id: str) -> str:
73
+ """Pin captured provider responses, not a claim about hosted model weights."""
74
+ return value_digest({"model_id": model_id, "provider_responses": [
75
+ {"case_id": row["case_id"], "response": row["response"],
76
+ "reviewer_response": row["reviewer_response"]} for row in rows]})
77
+
78
+
79
+ def _model_id(policy: str) -> str:
80
+ if policy == "jev-reviewed":
81
+ return "jev-1.13.0+gpt-6-sol"
82
+ return "jev-1.13.0" if policy.startswith("jev-") else "gpt-6-sol"
83
+
84
+
85
+ def _binding(parameters: Mapping) -> dict:
86
+ return {key: parameters[key] for key in _BINDING_KEYS}
87
+
88
+
89
+ def _plain(value):
90
+ if isinstance(value, Mapping):
91
+ return {key: _plain(item) for key, item in value.items()}
92
+ if isinstance(value, (list, tuple)):
93
+ return [_plain(item) for item in value]
94
+ return value
95
+
96
+
97
+ def _freeze(value):
98
+ """Retain validated JSON without mutable aliases to emitted evidence."""
99
+ if isinstance(value, Mapping):
100
+ return MappingProxyType({key: _freeze(item) for key, item in value.items()})
101
+ if isinstance(value, (list, tuple)):
102
+ return tuple(_freeze(item) for item in value)
103
+ return value
104
+
105
+
106
+ def _validate_configuration(configuration: CandidateConfiguration) -> str:
107
+ if not isinstance(configuration, CandidateConfiguration) or configuration.interface_version != INTERFACE:
108
+ raise ValueError("invocation configuration interface differs")
109
+ p = configuration.parameters
110
+ policy = p.get("policy")
111
+ if policy not in POLICY_MANIFESTS:
112
+ raise ValueError("routing policy is blocked or unknown")
113
+ manifest = POLICY_MANIFESTS[policy]
114
+ keys = _PARAMETER_KEYS | ({"recording_digest"} if configuration.execution_mode == "recorded" else set())
115
+ if set(p) != keys:
116
+ raise ValueError("routing parameters differ from exact schema")
117
+ if configuration.source_digest != manifest["source_digest"]:
118
+ raise ValueError("frozen policy source identity differs")
119
+ if configuration.execution_mode not in manifest["execution_modes"]:
120
+ raise ValueError("policy execution mode is unsupported")
121
+ if p["bank_id"] not in manifest["supported_banks"] or not isinstance(p["bank_release"], str) or not p["bank_release"]:
122
+ raise ValueError("routing bank is unsupported")
123
+ bank = BANK_BINDINGS[p["bank_id"]]
124
+ if p["bank_release"] != bank["release"] or p["input_digest"] != bank["input_digest"]:
125
+ raise ValueError("routing bank release or input identity differs")
126
+ if p["excerpt_policy"] != manifest["excerpt_policy"]:
127
+ raise ValueError("routing excerpt policy differs")
128
+ for field in ("input_digest", "cohort_digest"):
129
+ require_digest(p[field], field)
130
+ ids = p["case_ids"]
131
+ if not isinstance(ids, tuple) or not ids or any(not isinstance(i, str) or not i for i in ids) or len(set(ids)) != len(ids):
132
+ raise ValueError("routing coverage case IDs are invalid")
133
+ if not isinstance(p["request_digests"], Mapping) or set(p["request_digests"]) != set(ids):
134
+ raise ValueError("routing request coverage differs")
135
+ for digest in p["request_digests"].values():
136
+ require_digest(digest, "request_digest")
137
+ if configuration.execution_mode == "recorded":
138
+ require_digest(p["recording_digest"], "recording_digest")
139
+ if configuration.credential_refs or configuration.model_id != _model_id(policy) or configuration.model_revision != "recorded-provider-responses.v1":
140
+ raise ValueError("recording model/revision or credentials differ")
141
+ elif configuration.model_id is not None:
142
+ raise ValueError("deterministic/live candidates cannot claim captured model snapshots")
143
+ if configuration.execution_mode != "live_provider" and configuration.credential_refs:
144
+ raise ValueError("credentials are only accepted for explicit live execution")
145
+ return policy
146
+
147
+
148
+ def _request(configuration, request):
149
+ if not isinstance(request, dict) or set(request) != {"schema_version", "pack_id", "case_id", "company_id", "user_turn", "conversation_context"}:
150
+ raise ValueError("request fields differ from invocation schema")
151
+ if request["schema_version"] != "sphynx.rag.invoke.request.v1" or request["pack_id"] != "rag.invoke.v1":
152
+ raise ValueError("request interface differs")
153
+ if request["company_id"] not in policies.COMPANIES or not isinstance(request["user_turn"], str) or not request["user_turn"].strip():
154
+ raise ValueError("request company or user turn is invalid")
155
+ context = request["conversation_context"]
156
+ if not isinstance(context, list) or any(not isinstance(t, dict) or set(t) != {"role", "content"} or t["role"] not in ("user", "assistant") or not isinstance(t["content"], str) for t in context):
157
+ raise ValueError("request conversation is invalid")
158
+ if configuration.parameters["request_digests"].get(request["case_id"]) != value_digest(request):
159
+ raise ValueError("request is outside the bound cohort")
160
+ if configuration.parameters["excerpt_policy"] == "forbid" and "source excerpt" in canonical_json(request).casefold():
161
+ raise ValueError("source excerpts are outside approved routing scope")
162
+
163
+
164
+ def _default_credentials(reference: str) -> str:
165
+ if not reference.startswith("env:") or not re.fullmatch(r"SPHYNX_[A-Z0-9_]+", reference[4:]):
166
+ raise ValueError("subprocess requires an env:SPHYNX_ named credential reference")
167
+ return os.environ.get(reference[4:], "")
168
+
169
+
170
+ def _provider_transport(endpoint: str, body: bytes, credential: str) -> dict:
171
+ request = urllib.request.Request(endpoint, data=body, method="POST", headers={
172
+ "Authorization": "Bearer " + credential, "Content-Type": "application/json"})
173
+ try:
174
+ with urllib.request.urlopen(request, timeout=90) as response:
175
+ raw = response.read(16 * 1024 * 1024 + 1)
176
+ if len(raw) > 16 * 1024 * 1024:
177
+ raise ValueError("provider response too large")
178
+ return _strict_json(raw)
179
+ except Exception:
180
+ raise ValueError("provider transport failed; details withheld") from None
181
+
182
+
183
+ class RoutingAdapter:
184
+ """Direct typed adapter. Every provider boundary requires explicit approval."""
185
+ def __init__(self, configuration: CandidateConfiguration, *, recordings=None,
186
+ allow_live: bool = False, approved_scope: Mapping | None = None,
187
+ credential_resolver: Callable[[str], str] | None = None,
188
+ transport: Callable[[str, bytes, str], dict] | None = None):
189
+ self.configuration = configuration
190
+ self.policy = _validate_configuration(configuration)
191
+ self.last_events: tuple[dict, ...] = ()
192
+ self.last_intervention: dict | None = None
193
+ self._rows = MappingProxyType({})
194
+ self._credentials: tuple[str, ...] = ()
195
+ self._transport = transport or _provider_transport
196
+ if configuration.execution_mode == "live_provider":
197
+ if allow_live is not True or not isinstance(approved_scope, Mapping) or value_digest(approved_scope) != value_digest(_binding(configuration.parameters)):
198
+ raise ValueError("live execution requires explicit opt-in and exact approved scope")
199
+ p = configuration.parameters
200
+ if p["input_digest"] != _APPROVED_LIVE_INPUTS.get(p["bank_id"]) or p["excerpt_policy"] != "forbid":
201
+ raise ValueError("live bank/input scope is unapproved")
202
+ refs = configuration.credential_refs
203
+ if len(refs) != (2 if self.policy == "jev-reviewed" else 1):
204
+ raise ValueError("live credential references differ from selected stages")
205
+ resolver = credential_resolver or _default_credentials
206
+ try:
207
+ credentials = tuple(resolver(ref) for ref in refs)
208
+ except Exception:
209
+ raise ValueError("credential resolution failed; details withheld") from None
210
+ if any(not isinstance(key, str) or not key or any(c.isspace() for c in key) for key in credentials):
211
+ raise ValueError("live credentials are missing or malformed")
212
+ self._credentials = credentials
213
+ elif allow_live or approved_scope is not None or credential_resolver is not None or transport is not None:
214
+ raise ValueError("provider settings are only valid for live execution")
215
+ if configuration.execution_mode == "recorded":
216
+ if recordings is None:
217
+ raise ValueError("complete pinned recording is required")
218
+ self._load_recording(Path(recordings))
219
+ elif recordings is not None:
220
+ raise ValueError("recordings are only valid for recorded execution")
221
+
222
+ def _load_recording(self, path: Path):
223
+ raw = path.read_bytes()
224
+ if len(raw) > 64 * 1024 * 1024 or _file_digest(raw) != self.configuration.parameters["recording_digest"]:
225
+ raise ValueError("recording bytes differ from pinned capture")
226
+ recording = _strict_json(raw)
227
+ expected_keys = {"schema_version", "policy", "source_digest", "binding", "model_id", "model_revision", "model_snapshot_digest", "rows"}
228
+ if not isinstance(recording, dict) or set(recording) != expected_keys or recording["schema_version"] != RECORDING_SCHEMA:
229
+ raise ValueError("recording schema differs")
230
+ for field in ("model_id", "model_revision", "model_snapshot_digest", "source_digest"):
231
+ if recording[field] != getattr(self.configuration, field):
232
+ raise ValueError("recording " + field + " identity differs")
233
+ if recording["policy"] != self.policy or value_digest(recording["binding"]) != value_digest(_binding(self.configuration.parameters)):
234
+ raise ValueError("recording cohort or policy identity differs")
235
+ rows = recording["rows"]
236
+ if not isinstance(rows, list) or any(not isinstance(row, dict) or set(row) != {"case_id", "request_digest", "response", "reviewer_response", "source_prediction_digest"} for row in rows):
237
+ raise ValueError("recording rows differ from exact schema")
238
+ if [row["case_id"] for row in rows] != list(self.configuration.parameters["case_ids"]):
239
+ raise ValueError("recording ordered coverage differs")
240
+ if recording_model_snapshot(rows, recording["model_id"]) != recording["model_snapshot_digest"]:
241
+ raise ValueError("recording provider response snapshot differs")
242
+ for row in rows:
243
+ if row["request_digest"] != self.configuration.parameters["request_digests"][row["case_id"]]:
244
+ raise ValueError("recording request binding differs")
245
+ if self.policy.startswith("jev-"):
246
+ p = policies.jev_probabilities(row["response"])
247
+ first = max(policies.ORDER, key=lambda a: p[a])
248
+ if self.policy == "jev-reviewed":
249
+ prediction_digest = value_digest({"request_digest": row["request_digest"], "response": row["response"]})
250
+ if row["source_prediction_digest"] != prediction_digest:
251
+ raise ValueError("reviewer source prediction differs")
252
+ if first == "call_rag":
253
+ if row["reviewer_response"] is not None:
254
+ raise ValueError("Jev calls must bypass reviewer")
255
+ else:
256
+ policies.route_result(row["reviewer_response"], reviewer=True)
257
+ elif row["reviewer_response"] is not None or row["source_prediction_digest"] is not None:
258
+ raise ValueError("unexpected reviewer evidence")
259
+ else:
260
+ policies.route_result(row["response"])
261
+ if row["reviewer_response"] is not None or row["source_prediction_digest"] is not None:
262
+ raise ValueError("unexpected reviewer evidence")
263
+ self._rows = _freeze({row["case_id"]: row for row in rows})
264
+
265
+ def _call(self, endpoint, body, index):
266
+ try:
267
+ response = self._transport(endpoint, body, self._credentials[index])
268
+ def contains_secret(value):
269
+ if isinstance(value, str):
270
+ return any(secret in value for secret in self._credentials)
271
+ if isinstance(value, Mapping):
272
+ return any(contains_secret(k) or contains_secret(v) for k, v in value.items())
273
+ if isinstance(value, (list, tuple)):
274
+ return any(contains_secret(v) for v in value)
275
+ return False
276
+ if contains_secret(response):
277
+ raise ValueError("provider response contains a credential")
278
+ return _strict_json(canonical_json(response))
279
+ except Exception:
280
+ raise ValueError("provider execution failed; details withheld") from None
281
+
282
+ def __call__(self, request: dict) -> dict:
283
+ self.last_events = ()
284
+ self.last_intervention = None
285
+ _request(self.configuration, request)
286
+ if self.configuration.execution_mode == "live_provider" and (len(request["conversation_context"]) != 1 or len(canonical_json(request["conversation_context"])) > 3000):
287
+ raise ValueError("live context is outside approved authored scope")
288
+ events = []
289
+ replay = self.configuration.execution_mode == "recorded"
290
+ row = _plain(self._rows.get(request["case_id"]))
291
+ def stage(name, model, body, response, decision, elapsed, probabilities=None):
292
+ usage = response.get("usage") or {}
293
+ events.append({"schema_version": "sphynx.rag.invoke.candidate_stage.v1", "case_id": request["case_id"],
294
+ "stage": name, "model": model, "request_sha256": hashlib.sha256(body).hexdigest(),
295
+ "response_sha256": hashlib.sha256(json.dumps(response, sort_keys=True, separators=(",", ":"), allow_nan=False).encode()).hexdigest(),
296
+ "request": _strict_json(body), "response": _plain(response),
297
+ "candidate_reported_latency_ms": round(elapsed, 3),
298
+ "timing_provenance": "replay_adapter_elapsed_not_provider_latency" if replay else "provider_stage_wall_clock",
299
+ "candidate_reported_usage": {"input_tokens": _plain(usage.get("input_tokens")), "output_tokens": _plain(usage.get("output_tokens"))},
300
+ "decision": decision, "probabilities": _plain(probabilities)})
301
+ if self.policy in ("always-call", "always-clarify", "always-context", "simple-rule"):
302
+ action = {"always-call": "call_rag", "always-clarify": "clarify_first", "always-context": "do_not_call_rag"}.get(self.policy)
303
+ action = action or policies.simple_rule_action(request["user_turn"])
304
+ elif self.policy.startswith("jev-"):
305
+ body = policies.jev_body(request)
306
+ start = time.monotonic_ns()
307
+ response = row["response"] if replay else self._call(JEV_ENDPOINT, body, 0)
308
+ p = policies.jev_probabilities(response)
309
+ action = max(policies.ORDER, key=lambda a: p[a])
310
+ if self.policy == "jev-080":
311
+ action = policies.jev_080_action(p["do_not_call_rag"], p["clarify_first"])
312
+ elif self.policy == "jev-asymmetric":
313
+ action = policies.t023_asymmetric_action(p["do_not_call_rag"], p["clarify_first"])
314
+ stage("jev", policies.JEV_MODEL, body, response, action, (time.monotonic_ns()-start)/1e6, p)
315
+ first = action
316
+ if self.policy == "jev-reviewed" and first != "call_rag":
317
+ body = policies.route_body(request, "reviewer")
318
+ start = time.monotonic_ns()
319
+ review = row["reviewer_response"] if replay else self._call(REVIEW_ENDPOINT, body, 1)
320
+ result = policies.route_result(review, reviewer=True)
321
+ stage("noncall_reviewer", policies.MODEL, body, review, result["route"], (time.monotonic_ns()-start)/1e6)
322
+ if result["route"] == "REQUIRE_RAG":
323
+ action = "call_rag"
324
+ if self.policy == "jev-reviewed":
325
+ self.last_intervention = {"case_id": request["case_id"], "source_prediction_digest": value_digest({"request_digest": value_digest(request), "response": response}),
326
+ "initial_action": policies.ACTION[first], "final_action": policies.ACTION[action],
327
+ "changed": first != action, "scope": "same_recorded_jev_first_stage_before_and_after_noncall_review" if replay else "same_live_jev_first_stage_before_and_after_noncall_review"}
328
+ else:
329
+ body = policies.route_body(request, self.policy)
330
+ start = time.monotonic_ns()
331
+ response = row["response"]
332
+ result = policies.route_result(response)
333
+ stage(self.policy, policies.MODEL, body, response, result["route"], (time.monotonic_ns()-start)/1e6)
334
+ action = next(source for source, translated in policies.ACTION.items() if translated == result["route"])
335
+ self.last_events = tuple(events)
336
+ return {"schema_version": "sphynx.rag.invoke.response.v1", "case_id": request["case_id"], "action": policies.ACTION[action]}
337
+
338
+
339
+ def make_invocation_candidate(name: str, *, configuration: CandidateConfiguration,
340
+ configuration_path: str | os.PathLike[str], recordings=None,
341
+ allow_live: bool = False, approved_scope: Mapping | None = None) -> CandidateSpec:
342
+ """Persist caller-owned nonsecret configuration and return installed protocol argv.
343
+
344
+ configuration_path must be a new retained file. The subprocess checks its
345
+ exact bytes; credential values and raw requests are never stored here.
346
+ Live subprocess environment refs currently fail closed under the legacy
347
+ runner's minimal environment; explicit runner integration is required.
348
+ """
349
+ policy = _validate_configuration(configuration)
350
+ if name not in (policy, "routing." + policy + ".v1"):
351
+ raise ValueError("candidate name differs from configuration policy")
352
+ RoutingAdapter(configuration, recordings=recordings, allow_live=allow_live, approved_scope=approved_scope)
353
+ envelope = {"schema_version": ADAPTER_SCHEMA, "configuration": configuration.to_dict(),
354
+ "recordings": str(Path(recordings).resolve()) if recordings is not None else None,
355
+ "allow_live": allow_live, "approved_scope": _plain(approved_scope)}
356
+ raw = (canonical_json(envelope) + "\n").encode("utf-8")
357
+ path = Path(configuration_path).resolve()
358
+ with path.open("xb") as stream:
359
+ stream.write(raw)
360
+ return CandidateSpec("routing." + policy + ".v1", configuration.configuration_id,
361
+ (sys.executable, "-m", "sphynx.invocation_candidates", str(path), _file_digest(raw)), INTERFACE)
362
+
363
+
364
+ def protocol_main(argv=None) -> int:
365
+ try:
366
+ args = list(sys.argv[1:] if argv is None else argv)
367
+ if len(args) != 2:
368
+ raise ValueError("expected pinned adapter configuration path and digest")
369
+ require_digest(args[1], "adapter_digest")
370
+ raw = Path(args[0]).read_bytes()
371
+ if len(raw) > 1024 * 1024 or _file_digest(raw) != args[1]:
372
+ raise ValueError("adapter configuration digest differs")
373
+ envelope = _strict_json(raw)
374
+ if set(envelope) != {"schema_version", "configuration", "recordings", "allow_live", "approved_scope"} or envelope["schema_version"] != ADAPTER_SCHEMA:
375
+ raise ValueError("adapter envelope schema differs")
376
+ adapter = RoutingAdapter(CandidateConfiguration.from_dict(envelope["configuration"]), recordings=envelope["recordings"],
377
+ allow_live=envelope["allow_live"], approved_scope=envelope["approved_scope"])
378
+ request_raw = sys.stdin.buffer.read(1024 * 1024 + 1)
379
+ if len(request_raw) > 1024 * 1024:
380
+ raise ValueError("request too large")
381
+ response = adapter(_strict_json(request_raw))
382
+ for event in adapter.last_events:
383
+ print(json.dumps(event, sort_keys=True, allow_nan=False), file=sys.stderr, flush=True)
384
+ print(canonical_json(response), flush=True)
385
+ return 0
386
+ except Exception as exc:
387
+ print("routing_candidate_failure:" + type(exc).__name__, file=sys.stderr, flush=True)
388
+ return 2
389
+
390
+
391
+ if __name__ == "__main__":
392
+ raise SystemExit(protocol_main())
@@ -0,0 +1,40 @@
1
+ """Immutable, content-free routing discovery; importing this never executes a router."""
2
+ from types import MappingProxyType
3
+ from .catalog import CandidateDescriptor
4
+
5
+ POLICY_MANIFESTS = MappingProxyType({
6
+ 'always-call': MappingProxyType({'source_digest': 'sha256:92bf9f6db5ccee39e0702bfe23dbfef23935cb23792fa0ebc29450908c7e152b', 'supported_banks': ('excerpt-free-200', 'development-300', 'narrow-policy-90', 'decision-boundary-80', 'narrow-gate-20260925-r2', 'narrow-gate-20260925-r4', 'narrow-gate-20260925-r5'), 'execution_modes': ('trusted_local',), 'excerpt_policy': 'allow_supplied'}),
7
+ 'always-clarify': MappingProxyType({'source_digest': 'sha256:27a8f2384bc920bef28b9d925dbf6e03163fac85d27edbf14842cf45a7b67885', 'supported_banks': ('excerpt-free-200', 'development-300', 'narrow-policy-90', 'decision-boundary-80', 'narrow-gate-20260925-r2', 'narrow-gate-20260925-r4', 'narrow-gate-20260925-r5'), 'execution_modes': ('trusted_local',), 'excerpt_policy': 'allow_supplied'}),
8
+ 'always-context': MappingProxyType({'source_digest': 'sha256:38b160fec657e70f1af7011ea95e43705a8376a79eaee28db9a41093597a0391', 'supported_banks': ('excerpt-free-200', 'development-300', 'narrow-policy-90', 'decision-boundary-80', 'narrow-gate-20260925-r2', 'narrow-gate-20260925-r4', 'narrow-gate-20260925-r5'), 'execution_modes': ('trusted_local',), 'excerpt_policy': 'allow_supplied'}),
9
+ 'simple-rule': MappingProxyType({'source_digest': 'sha256:aadfbdf4291a994eef41021ead871a70dba79b2a8ecbf79f1fe290cca7b4b719', 'supported_banks': ('excerpt-free-200', 'development-300', 'narrow-policy-90', 'decision-boundary-80', 'narrow-gate-20260925-r2', 'narrow-gate-20260925-r4', 'narrow-gate-20260925-r5'), 'execution_modes': ('trusted_local',), 'excerpt_policy': 'allow_supplied'}),
10
+ 'jev-argmax': MappingProxyType({'source_digest': 'sha256:bedde8856f34b637210a2034005825fd359e5e2043414145d5f51af8b7252d91', 'supported_banks': ('excerpt-free-200', 'decision-boundary-80', 'narrow-gate-20260925-r5'), 'execution_modes': ('recorded', 'live_provider'), 'excerpt_policy': 'forbid'}),
11
+ 'jev-reviewed': MappingProxyType({'source_digest': 'sha256:87bdf91d3e4d61d0a2fdd4a4c75560b3e38e75c87c1df9732f1e7d8f752b145f', 'supported_banks': ('excerpt-free-200', 'decision-boundary-80', 'narrow-gate-20260925-r5'), 'execution_modes': ('recorded', 'live_provider'), 'excerpt_policy': 'forbid'}),
12
+ 'jev-080': MappingProxyType({'source_digest': 'sha256:ca20188db799e00618e0c22afd15838a634f0dc60b42dc3fbb3b2f201748d7fd', 'supported_banks': ('excerpt-free-200', 'development-300', 'narrow-policy-90', 'decision-boundary-80', 'narrow-gate-20260925-r2', 'narrow-gate-20260925-r4', 'narrow-gate-20260925-r5'), 'execution_modes': ('recorded',), 'excerpt_policy': 'allow_supplied'}),
13
+ 'jev-asymmetric': MappingProxyType({'source_digest': 'sha256:eb06ef68479a94c2759f71979ae2df6aca68a6857d1808d3e8a7547046024a9a', 'supported_banks': ('excerpt-free-200', 'development-300', 'narrow-policy-90', 'decision-boundary-80', 'narrow-gate-20260925-r2', 'narrow-gate-20260925-r4', 'narrow-gate-20260925-r5'), 'execution_modes': ('recorded',), 'excerpt_policy': 'allow_supplied'}),
14
+ 'typed': MappingProxyType({'source_digest': 'sha256:38bda9f73ac90ecac22e099e30b16edcd4705a26e06aa3e6b0d6f39064d2dd73', 'supported_banks': ('excerpt-free-200', 'development-300', 'narrow-policy-90', 'decision-boundary-80', 'narrow-gate-20260925-r2', 'narrow-gate-20260925-r4', 'narrow-gate-20260925-r5'), 'execution_modes': ('recorded',), 'excerpt_policy': 'allow_supplied'}),
15
+ 'narrow': MappingProxyType({'source_digest': 'sha256:1292541d95aa8a92a84933526bad43bce0a93f4bd05120882de5df7bfc02e972', 'supported_banks': ('excerpt-free-200', 'development-300', 'narrow-policy-90', 'decision-boundary-80', 'narrow-gate-20260925-r2', 'narrow-gate-20260925-r4', 'narrow-gate-20260925-r5'), 'execution_modes': ('recorded',), 'excerpt_policy': 'allow_supplied'}),
16
+ 'hybrid': MappingProxyType({'source_digest': 'sha256:618f61b1fa15081f4e599f227d61b86153044e9ff3728785506774c8e59eab46', 'supported_banks': ('excerpt-free-200', 'development-300', 'narrow-policy-90', 'decision-boundary-80', 'narrow-gate-20260925-r2', 'narrow-gate-20260925-r4', 'narrow-gate-20260925-r5'), 'execution_modes': ('recorded',), 'excerpt_policy': 'allow_supplied'}),
17
+ })
18
+
19
+ CANDIDATES = tuple(CandidateDescriptor(
20
+ candidate_id="routing." + name + ".v1", track="rag_invoke",
21
+ interface_version="sphynx.rag.invoke.v1", source_digest=manifest["source_digest"],
22
+ supported_dataset_ids=tuple("invocation." + bank for bank in manifest["supported_banks"]),
23
+ execution_modes=manifest["execution_modes"], implementation_status="available",
24
+ local_prerequisites=("exact bank/release/input/cohort/request binding", "excerpt_policy=" + manifest["excerpt_policy"]) + (("pinned complete recording and provider response snapshot",) if "recorded" in manifest["execution_modes"] else ()) + (("explicit live opt-in and approved excerpt-free scope", "named credential resolver; subprocess environment integration required") if "live_provider" in manifest["execution_modes"] else ()),
25
+ model_identity_required="recorded" in manifest["execution_modes"], supported_python=(">=3.11",),
26
+ ) for name, manifest in POLICY_MANIFESTS.items()) + (
27
+ CandidateDescriptor('routing.aidan-production.v1', "rag_invoke", "sphynx.rag.invoke.v1", None, (), ("recorded",), "blocked", reason='Deployed Aidan subject source/version is unverified; no reconstructed production adapter is authorized.'),
28
+ CandidateDescriptor('routing.aidan-gpt6sol-production.v1', "rag_invoke", "sphynx.rag.invoke.v1", None, (), ("recorded",), "blocked", reason='Cheap Aidan planner source/model identity is unverified as a deployed subject.'),
29
+ CandidateDescriptor('routing.binary-cascades.v1', "rag_invoke", "sphynx.rag.invoke.v1", 'sha256:a1cd3170e59f448abf5db157b9c012317c8398a3fbd22a02b2121d22be2bfce2', (), ("recorded",), "blocked", reason='Source-verified counterfactual cascades produce binary call/noncall, with explicit source fallbacks; the three-way interface cannot distinguish ASK_BUYER from ANSWER_FROM_CONTEXT. Use routing_policies.binary_cascade for binary source-parity only.'),
30
+ )
31
+
32
+ BANK_BINDINGS = MappingProxyType({
33
+ 'excerpt-free-200': MappingProxyType({'release': 'v1-development-200', 'input_digest': 'sha256:7316862d37a7185ed7c995278a5d5f9ae34ff0ae05afe5c4d281e2c00303d2bf'}),
34
+ 'development-300': MappingProxyType({'release': 'v1-20260918-r3-five-customer', 'input_digest': 'sha256:80336bade48c6bc1acadbae8c66e321d68008e0ee6d2d18fd473e56d566319a6'}),
35
+ 'narrow-policy-90': MappingProxyType({'release': 'v1-20260925', 'input_digest': 'sha256:ecde7c3141fa8b508386e5b59468296acba55e14aa4f64e1e2f8143de977878e'}),
36
+ 'decision-boundary-80': MappingProxyType({'release': 'v1-20260925', 'input_digest': 'sha256:f1aea9ab88c55586ed01a6bf5aee4ac78661ae11727dfea12ecee14da09ea2b6'}),
37
+ 'narrow-gate-20260925-r2': MappingProxyType({'release': 'narrow-gate-20260925-r2', 'input_digest': 'sha256:fea544f918248f710be172bcadfdf1e6506ca2f4e08ab97c6099bd3afed1d316'}),
38
+ 'narrow-gate-20260925-r4': MappingProxyType({'release': 'narrow-gate-20260925-r4', 'input_digest': 'sha256:2af3d98cb92146511e85de1a6c038d01f4bf7c117b622c61c6425a03f6e4a391'}),
39
+ 'narrow-gate-20260925-r5': MappingProxyType({'release': 'narrow-gate-20260925-r5', 'input_digest': 'sha256:c230ad79a9aba6cef17266b83345d273e93eba0871408ce23f87d59e27543194'}),
40
+ })
@@ -0,0 +1,30 @@
1
+ """Content-free frozen learned inference discovery; no fitted values/ML imports."""
2
+ from types import MappingProxyType
3
+ from .catalog import CandidateDescriptor
4
+ from .integration_contracts import ScoreField
5
+
6
+ _SOURCE='sha256:3323ca7ead3cf8cb69d9e7fc3a908dd877784f79ff94bb48ebdcb24cdb02c924'
7
+ METHOD_NAMES=('T2','T7','T14','T17','T18','T19','T20','T21','T22')
8
+ METHOD_METADATA=MappingProxyType({name:MappingProxyType({
9
+ 'method':name,'ranking_alias_of':'T2' if name in ('T7','T14','T17','T18') else 'T19' if name=='T20' else None,
10
+ 'historical_fitted_state_located':name in ('T19','T20','T21','T22'),
11
+ 'historical_complete_cohort_located':False,'fitted_values_bundled':False,'refitting_available':False,
12
+ 'probability_semantics':'state_label_mode_required; positive_unlabeled_diagnostic stays diagnostic' if name!='T2' and name!='T19' else 'no probability field',
13
+ 'historical_full_benchmark_parity_verified':False,
14
+ }) for name in METHOD_NAMES})
15
+ _FIELDS=(ScoreField('ranking_utility','utility','higher','ranking_utility'),
16
+ ScoreField('raw_minilm_logit','utility','higher','model_logit'))
17
+ CANDIDATES=tuple(CandidateDescriptor(
18
+ candidate_id='learned-'+name+'-frozen-v1',track='rerank',interface_version='sphynx.rerank-family.v1',
19
+ source_digest=_SOURCE,supported_dataset_ids=(),execution_modes=('recorded','trusted_local'),
20
+ implementation_status='available',reason=(
21
+ 'Frozen prediction code is source-verified; this does not recreate a historical experiment. '+
22
+ ('Historical coefficients/calibration exist externally; complete original N11 inputs/metadata remain missing.' if name in ('T19','T20','T21','T22') else
23
+ 'Original fitted ranker/calibrator not located; synthetic-source prediction parity does not supply the historical state.')),
24
+ local_prerequisites=('exact external fitted-state wrapper and SHA-256; payload/cohort/source binding',
25
+ 'complete exact label-free ten-feature metadata, with missing values explicitly null',
26
+ 'pinned MiniLM logits or an explicit offline model scorer configuration',
27
+ 'explicit excluded evaluation/cohort identity and domain; lineage declarations remain unverified',
28
+ 'separate probability grading policy; raw utilities or PU outputs are not calibrated probabilities'),
29
+ dependencies=(),score_fields=_FIELDS,supported_python=('3.11','3.12','3.13','3.14'),
30
+ ) for name in METHOD_NAMES)