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/metrics.py ADDED
@@ -0,0 +1,282 @@
1
+ """Versioned illustrative frozen-pool RAG ranking metrics.
2
+
3
+ The gain, discount, relevance threshold, and required-group rules follow the
4
+ local ``rag-evals/src/metrics-v2.ts`` source pinned by T-001. This narrower
5
+ Sphynx release deliberately publishes only four ranking metrics and runner
6
+ wall latency. No answer-quality or abstention grade is inferred.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import math
12
+ from dataclasses import dataclass
13
+ from typing import Sequence
14
+
15
+ from .contracts import CaseReferences, ContractError, FrozenPoolCase, _finite_number
16
+ from .rag import RagRerankPack, load_rag_rerank_pack
17
+
18
+ METRIC_RELEASE = "rag.rerank.metric.v1"
19
+ GRADER_RELEASE = "rag.rerank.grader.v1"
20
+ _QUALITY_METRICS = ("ndcg_at_5", "hit_at_5", "mrr_at_5", "evidence_group_recall_at_5")
21
+ _LATENCY_METRIC = "runner_latency_ms"
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class RankingMetricDefinition:
26
+ metric_id: str
27
+ release: str
28
+ unit: str
29
+ direction: str
30
+ primary: bool
31
+ eligible_population: str
32
+ aggregation: str
33
+ undefined_policy: str
34
+ missing_policy: str
35
+
36
+ def to_dict(self) -> dict[str, str | bool]:
37
+ return vars(self).copy()
38
+
39
+
40
+ METRIC_DEFINITIONS = (
41
+ RankingMetricDefinition(
42
+ "ndcg_at_5", METRIC_RELEASE, "ratio", "higher", True,
43
+ "answerable cases in the fixed pack", "arithmetic mean across eligible cases",
44
+ "null for unsupported cases or zero ideal DCG", "null and incomplete aggregate when an eligible case fails or lacks a valid ranking",
45
+ ),
46
+ RankingMetricDefinition(
47
+ "hit_at_5", METRIC_RELEASE, "ratio", "higher", False,
48
+ "answerable cases in the fixed pack", "arithmetic mean across eligible cases",
49
+ "null for unsupported cases", "null and incomplete aggregate when an eligible case fails or lacks a valid ranking",
50
+ ),
51
+ RankingMetricDefinition(
52
+ "mrr_at_5", METRIC_RELEASE, "ratio", "higher", False,
53
+ "answerable cases in the fixed pack", "arithmetic mean across eligible cases",
54
+ "null for unsupported cases", "null and incomplete aggregate when an eligible case fails or lacks a valid ranking",
55
+ ),
56
+ RankingMetricDefinition(
57
+ "evidence_group_recall_at_5", METRIC_RELEASE, "ratio", "higher", False,
58
+ "answerable cases with required evidence groups", "arithmetic mean across eligible cases",
59
+ "null for unsupported cases or no required groups", "null and incomplete aggregate when an eligible case fails or lacks a valid ranking",
60
+ ),
61
+ RankingMetricDefinition(
62
+ _LATENCY_METRIC, METRIC_RELEASE, "ms", "lower", False,
63
+ "all attempted cases with a runner monotonic span", "per-case span; aggregate p50 and p95",
64
+ "null when no runner span exists", "null and incomplete aggregate when an attempted case lacks a runner span",
65
+ ),
66
+ )
67
+
68
+
69
+ def metric_definitions() -> tuple[RankingMetricDefinition, ...]:
70
+ return METRIC_DEFINITIONS
71
+
72
+
73
+ @dataclass(frozen=True)
74
+ class CaseMetricResult:
75
+ case_id: str
76
+ pool_digest: str
77
+ terminal_status: str
78
+ answerable: bool
79
+ values: tuple[tuple[str, float | None], ...]
80
+ undefined_reasons: tuple[tuple[str, str], ...]
81
+ metric_release: str = METRIC_RELEASE
82
+ grader_release: str = GRADER_RELEASE
83
+
84
+ def value_for(self, metric_id: str) -> float | None:
85
+ return dict(self.values)[metric_id]
86
+
87
+ def to_dict(self) -> dict[str, object]:
88
+ return {
89
+ "case_id": self.case_id,
90
+ "pool_digest": self.pool_digest,
91
+ "terminal_status": self.terminal_status,
92
+ "answerable": self.answerable,
93
+ "metric_release": self.metric_release,
94
+ "grader_release": self.grader_release,
95
+ "values": dict(self.values),
96
+ "undefined_reasons": dict(self.undefined_reasons),
97
+ }
98
+
99
+
100
+ @dataclass(frozen=True)
101
+ class MetricAggregate:
102
+ metric_id: str
103
+ release: str
104
+ unit: str
105
+ direction: str
106
+ value: float | None
107
+ p95: float | None
108
+ eligible_count: int
109
+ observed_count: int
110
+ missing_count: int
111
+ undefined_count: int
112
+ complete: bool
113
+
114
+ def to_dict(self) -> dict[str, str | float | int | bool | None]:
115
+ return vars(self).copy()
116
+
117
+
118
+ def _dcg(grades: Sequence[int], cutoff: int = 5) -> float:
119
+ return sum((2 ** grade - 1) / math.log2(index + 2) for index, grade in enumerate(grades[:cutoff]))
120
+
121
+
122
+ def _percentile(values: Sequence[float], fraction: float) -> float:
123
+ ordered = sorted(values)
124
+ position = (len(ordered) - 1) * fraction
125
+ lower = math.floor(position)
126
+ upper = math.ceil(position)
127
+ return ordered[lower] + (ordered[upper] - ordered[lower]) * (position - lower)
128
+
129
+
130
+ def grade_case(
131
+ case: FrozenPoolCase,
132
+ references: CaseReferences,
133
+ ranked_ids: Sequence[str] | None,
134
+ latency_ms: float | None,
135
+ *,
136
+ terminal_status: str = "ok",
137
+ relevance_threshold: int = 2,
138
+ pack: RagRerankPack | None = None,
139
+ ) -> CaseMetricResult:
140
+ """Grade one attempt against the pinned pack, retaining explicit absence.
141
+
142
+ ``ranked_ids`` must cover the entire frozen pool exactly once for an ``ok``
143
+ attempt. Use :func:`sphynx.contracts.ranked_candidate_ids` to apply the
144
+ stable descending-score, original-pool-order tie rule first. Latency is
145
+ supplied from the runner's monotonic wall clock, not candidate output.
146
+ """
147
+
148
+ pack = pack or load_rag_rerank_pack()
149
+ original = next((item for item in pack.cases if item.case_id == case.case_id), None)
150
+ if original is None or original.pool_digest != case.pool_digest:
151
+ raise ContractError(f"Case {case.case_id!r} does not match the frozen pack pool and corpus identity")
152
+ if not isinstance(references, CaseReferences) or references.case_id != case.case_id:
153
+ raise ContractError("references must match the frozen case identity")
154
+ if references != pack.reference_for(case.case_id):
155
+ raise ContractError("references differ from the selected frozen pack labels")
156
+ expected_ids = {candidate.candidate_id for candidate in case.candidates}
157
+ by_id = {candidate.candidate_id: candidate for candidate in references.candidates}
158
+ if set(by_id) != expected_ids:
159
+ raise ContractError("reference candidate IDs do not match the frozen pool")
160
+ pinned_threshold = pack.relevance_threshold
161
+ if type(relevance_threshold) is not int or relevance_threshold != pinned_threshold:
162
+ raise ContractError(
163
+ f"{METRIC_RELEASE} requires the pack's relevance_threshold={pinned_threshold}; "
164
+ "another threshold requires a new metric and grader release"
165
+ )
166
+ if type(terminal_status) is not str or not terminal_status:
167
+ raise ContractError("terminal_status must be a nonempty string")
168
+ if latency_ms is not None:
169
+ latency_ms = _finite_number(latency_ms, "runner latency_ms")
170
+ if latency_ms < 0:
171
+ raise ContractError("runner latency_ms cannot be negative")
172
+ if terminal_status == "ok":
173
+ if ranked_ids is None or isinstance(ranked_ids, (str, bytes)):
174
+ raise ContractError("ok attempt requires a full ranked ID sequence")
175
+ ranked_ids = tuple(ranked_ids)
176
+ if len(ranked_ids) != len(expected_ids) or set(ranked_ids) != expected_ids:
177
+ raise ContractError("ranked IDs must cover the exact frozen pool once")
178
+ elif ranked_ids is not None:
179
+ raise ContractError("failed attempt cannot carry a quality ranking")
180
+ required_groups = tuple(group for group in references.evidence_groups if group.required)
181
+ answerable = bool(required_groups)
182
+ reasons: dict[str, str] = {}
183
+ values: dict[str, float | None] = {metric: None for metric in _QUALITY_METRICS}
184
+ if not answerable:
185
+ reasons.update({metric: "unsupported_query" for metric in _QUALITY_METRICS})
186
+ elif terminal_status != "ok":
187
+ reasons.update({metric: f"candidate_{terminal_status}" for metric in _QUALITY_METRICS})
188
+ else:
189
+ assert ranked_ids is not None
190
+ grades = [by_id[candidate_id].relevance_grade for candidate_id in ranked_ids]
191
+ ideal = sorted((reference.relevance_grade for reference in references.candidates), reverse=True)
192
+ ideal_dcg = _dcg(ideal)
193
+ if ideal_dcg == 0:
194
+ reasons["ndcg_at_5"] = "zero_ideal_dcg"
195
+ else:
196
+ values["ndcg_at_5"] = _dcg(grades) / ideal_dcg
197
+ first_relevant = next((index for index, grade in enumerate(grades[:5], start=1) if grade >= relevance_threshold), None)
198
+ values["hit_at_5"] = 1.0 if first_relevant is not None else 0.0
199
+ values["mrr_at_5"] = 1.0 / first_relevant if first_relevant is not None else 0.0
200
+ top_ids = set(ranked_ids[:5])
201
+ covered = sum(bool(top_ids.intersection(group.acceptable_candidate_ids)) for group in required_groups)
202
+ values["evidence_group_recall_at_5"] = covered / len(required_groups)
203
+ values[_LATENCY_METRIC] = latency_ms
204
+ if latency_ms is None:
205
+ reasons[_LATENCY_METRIC] = "runner_span_not_captured"
206
+ return CaseMetricResult(
207
+ case.case_id, case.pool_digest, terminal_status, answerable,
208
+ tuple(values.items()), tuple(reasons.items()),
209
+ )
210
+
211
+
212
+ def aggregate_case_metrics(results: Sequence[CaseMetricResult], *, pack: RagRerankPack | None = None) -> dict[str, MetricAggregate]:
213
+ """Aggregate a complete selected cohort without masking missing cases.
214
+
215
+ The caller must supply one result for every fixed pack case. If any eligible
216
+ case has no value, the metric's aggregate is ``None`` and ``complete`` is
217
+ false. Unsupported cases are counted as undefined, not as failures/zeros.
218
+ """
219
+
220
+ pack = pack or load_rag_rerank_pack()
221
+ if len(results) != len(pack.cases):
222
+ raise ContractError("aggregation requires one result for every fixed pack case")
223
+ if any(not isinstance(result, CaseMetricResult) for result in results):
224
+ raise ContractError("aggregation requires CaseMetricResult values")
225
+ by_case = {result.case_id: result for result in results}
226
+ if len(by_case) != len(results) or set(by_case) != {case.case_id for case in pack.cases}:
227
+ raise ContractError("aggregate case IDs must equal the fixed pack case IDs exactly once")
228
+ for case in pack.cases:
229
+ result = by_case[case.case_id]
230
+ if result.pool_digest != case.pool_digest:
231
+ raise ContractError(f"aggregate pool digest mismatch for {case.case_id}")
232
+ if result.metric_release != pack.metric_release or result.grader_release != GRADER_RELEASE:
233
+ raise ContractError(f"aggregate release mismatch for {case.case_id}")
234
+ expected_answerable = any(group.required for group in pack.reference_for(case.case_id).evidence_groups)
235
+ if result.answerable is not expected_answerable:
236
+ raise ContractError(f"aggregate answerability mismatch for {case.case_id}")
237
+ if type(result.terminal_status) is not str or not result.terminal_status:
238
+ raise ContractError(f"aggregate terminal status is invalid for {case.case_id}")
239
+ value_map = dict(result.values)
240
+ if len(result.values) != len(METRIC_DEFINITIONS) or set(value_map) != {rule.metric_id for rule in METRIC_DEFINITIONS}:
241
+ raise ContractError(f"aggregate metric fields mismatch for {case.case_id}")
242
+ for metric_id in _QUALITY_METRICS:
243
+ value = value_map[metric_id]
244
+ if value is not None and (type(value) not in (int, float) or not math.isfinite(value) or not 0 <= value <= 1):
245
+ raise ContractError(f"aggregate {metric_id} value is invalid for {case.case_id}")
246
+ if not expected_answerable or result.terminal_status != "ok":
247
+ if value is not None:
248
+ raise ContractError(f"aggregate {metric_id} must be undefined or missing for {case.case_id}")
249
+ elif value is None:
250
+ raise ContractError(f"aggregate {metric_id} is missing from successful answerable case {case.case_id}")
251
+ latency = value_map[_LATENCY_METRIC]
252
+ if latency is not None and (type(latency) not in (int, float) or not math.isfinite(latency) or latency < 0):
253
+ raise ContractError(f"aggregate runner latency is invalid for {case.case_id}")
254
+ aggregate: dict[str, MetricAggregate] = {}
255
+ for definition in METRIC_DEFINITIONS:
256
+ values: list[float] = []
257
+ eligible_count = missing_count = undefined_count = 0
258
+ for case in pack.cases:
259
+ result = by_case[case.case_id]
260
+ eligible = definition.metric_id == _LATENCY_METRIC or any(
261
+ group.required for group in pack.reference_for(case.case_id).evidence_groups
262
+ )
263
+ if not eligible:
264
+ undefined_count += 1
265
+ continue
266
+ eligible_count += 1
267
+ value = result.value_for(definition.metric_id)
268
+ if value is None:
269
+ missing_count += 1
270
+ else:
271
+ values.append(value)
272
+ complete = missing_count == 0 and eligible_count > 0
273
+ if complete:
274
+ value = _percentile(values, 0.5) if definition.metric_id == _LATENCY_METRIC else sum(values) / len(values)
275
+ p95 = _percentile(values, 0.95) if definition.metric_id == _LATENCY_METRIC else None
276
+ else:
277
+ value = p95 = None
278
+ aggregate[definition.metric_id] = MetricAggregate(
279
+ definition.metric_id, definition.release, definition.unit, definition.direction,
280
+ value, p95, eligible_count, len(values), missing_count, undefined_count, complete,
281
+ )
282
+ return aggregate