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/rag.py ADDED
@@ -0,0 +1,329 @@
1
+ """The public, illustrative ``rag.rerank.v1`` frozen-pool pack.
2
+
3
+ The packaged JSON is byte-for-byte the product-neutral fixture from the local
4
+ ``rag-evals/fixtures/dataset-v2.json`` source pinned in T-001. The validation
5
+ rules below port its ``src/dataset-v2.ts`` dataset checks; that source is not a
6
+ runtime dependency. Reference labels are separate from :class:`FrozenPoolCase`
7
+ and must never be passed to a candidate process.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ import json
14
+ from dataclasses import dataclass
15
+ from datetime import datetime
16
+ from functools import lru_cache
17
+ from importlib import resources
18
+ from typing import Any, Mapping
19
+
20
+ from .contracts import (
21
+ CASE_SCHEMA_VERSION,
22
+ PACK_ID,
23
+ CaseReferences,
24
+ ContractError,
25
+ CorpusVersions,
26
+ EvidenceGroup,
27
+ FrozenCandidate,
28
+ FrozenPoolCase,
29
+ ReferenceCandidate,
30
+ _finite_number,
31
+ _id,
32
+ _text,
33
+ )
34
+ from .packs import PackDescriptor, get_pack
35
+
36
+ FIXTURE_SHA256 = "74884a0176738649d85cc8b681323103961939aaa0f3d5f1dd1565ed2e53d86b"
37
+ _FIXTURE_NAME = "rag-rerank-v1.json"
38
+ _SEVERITIES = frozenset({"low", "medium", "high", "critical"})
39
+ _CATEGORIES = frozenset(
40
+ {
41
+ "straightforward_factual", "semantic_paraphrase", "exact_product_feature_name",
42
+ "competitor_named_entity", "typo_misspelling", "acronym_abbreviation",
43
+ "short_underspecified", "long_conversational", "conversational_follow_up",
44
+ "ambiguous", "multi_part", "multi_document", "unsupported_not_in_kb",
45
+ "off_topic", "false_premise", "contradictory_sources", "stale_current_conflict",
46
+ "long_passage_truncation", "adjacent_chunk_dependency", "duplicate_documentation",
47
+ "presales_language", "postsales_language", "customer_specific_terminology",
48
+ }
49
+ )
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class RagRerankPack:
54
+ descriptor: PackDescriptor
55
+ cases: tuple[FrozenPoolCase, ...]
56
+ references: tuple[CaseReferences, ...]
57
+ metric_release: str
58
+ dataset_digest: str
59
+ relevance_threshold: int
60
+
61
+ def reference_for(self, case_id: str) -> CaseReferences:
62
+ for reference in self.references:
63
+ if reference.case_id == case_id:
64
+ return reference
65
+ raise ContractError(f"No reference for case {case_id!r}")
66
+
67
+
68
+ def _mapping(value: object, path: str) -> Mapping[str, Any]:
69
+ if not isinstance(value, Mapping) or any(not isinstance(key, str) for key in value):
70
+ raise ContractError(f"{path} must be a JSON object")
71
+ return value
72
+
73
+
74
+ def _list(value: object, path: str, *, nonempty: bool = False) -> list[Any]:
75
+ if not isinstance(value, list) or (nonempty and not value):
76
+ raise ContractError(f"{path} must be a {'nonempty ' if nonempty else ''}JSON array")
77
+ return value
78
+
79
+
80
+ def _unique_ids(value: object, path: str, *, nonempty: bool = False) -> tuple[str, ...]:
81
+ values = tuple(_id(item, f"{path}[{index}]") for index, item in enumerate(_list(value, path, nonempty=nonempty)))
82
+ if len(set(values)) != len(values):
83
+ raise ContractError(f"{path} must contain unique identifiers")
84
+ return values
85
+
86
+
87
+ def _optional_text(value: Mapping[str, Any], key: str, path: str) -> None:
88
+ if key in value:
89
+ _text(value[key], f"{path}.{key}")
90
+
91
+
92
+ def _validate_candidate_annotations(raw: Mapping[str, Any], path: str) -> None:
93
+ if "known_hard_negative" in raw and type(raw["known_hard_negative"]) is not bool:
94
+ raise ContractError(f"{path}.known_hard_negative must be boolean")
95
+ _optional_text(raw, "annotator_rationale", path)
96
+ if "evidence_spans" in raw:
97
+ for index, item in enumerate(_list(raw["evidence_spans"], f"{path}.evidence_spans")):
98
+ span = _mapping(item, f"{path}.evidence_spans[{index}]")
99
+ start, end = span.get("start"), span.get("end")
100
+ if type(start) is not int or type(end) is not int or start < 0 or end <= start or end > len(raw["input"]):
101
+ raise ContractError(f"{path}.evidence_spans[{index}] has invalid bounds")
102
+ if "text" in span and span["text"] != raw["input"][start:end]:
103
+ raise ContractError(f"{path}.evidence_spans[{index}].text must match the exact passage span")
104
+ if "annotations" in raw:
105
+ annotations = _list(raw["annotations"], f"{path}.annotations", nonempty=True)
106
+ annotators: set[str] = set()
107
+ for index, item in enumerate(annotations):
108
+ annotation = _mapping(item, f"{path}.annotations[{index}]")
109
+ annotator = _id(annotation.get("annotator_id"), f"{path}.annotations[{index}].annotator_id")
110
+ if annotator in annotators:
111
+ raise ContractError(f"{path}.annotations has duplicate annotator {annotator!r}")
112
+ annotators.add(annotator)
113
+ grade = annotation.get("relevance_grade")
114
+ if type(grade) is not int or not 0 <= grade <= 3:
115
+ raise ContractError(f"{path}.annotations[{index}].relevance_grade must be 0..3")
116
+ _optional_text(annotation, "rationale", f"{path}.annotations[{index}]")
117
+ if "created_at" in annotation:
118
+ timestamp = annotation["created_at"]
119
+ try:
120
+ datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
121
+ except (AttributeError, ValueError) as exc:
122
+ raise ContractError(f"{path}.annotations[{index}].created_at must be a timestamp") from exc
123
+ if "adjudication" in raw:
124
+ adjudication = _mapping(raw["adjudication"], f"{path}.adjudication")
125
+ _id(adjudication.get("adjudicator_id"), f"{path}.adjudication.adjudicator_id")
126
+ if adjudication.get("relevance_grade") != raw.get("relevance_grade"):
127
+ raise ContractError(f"{path}.adjudication.relevance_grade must match final grade")
128
+ _text(adjudication.get("rationale"), f"{path}.adjudication.rationale")
129
+
130
+
131
+ def _validate_task(raw_value: object, index: int, dataset_id: str, dataset_version: str, label_authority: str) -> tuple[FrozenPoolCase, CaseReferences]:
132
+ path = f"tasks[{index}]"
133
+ raw = _mapping(raw_value, path)
134
+ case_id = _id(raw.get("task_id"), f"{path}.task_id")
135
+ query_id = _id(raw.get("query_id"), f"{path}.query_id")
136
+ cluster_id = _id(raw.get("cluster_id"), f"{path}.cluster_id")
137
+ split_group_id = _id(raw.get("split_group_id"), f"{path}.split_group_id")
138
+ _id(raw.get("domain"), f"{path}.domain")
139
+ query = _text(raw.get("query"), f"{path}.query")
140
+ answerability = raw.get("answerability")
141
+ if answerability not in ("answerable", "unsupported"):
142
+ raise ContractError(f"{path}.answerability must be answerable or unsupported")
143
+ if not isinstance(raw.get("business_severity"), str) or raw["business_severity"] not in _SEVERITIES:
144
+ raise ContractError(f"{path}.business_severity is invalid")
145
+ categories = _unique_ids(raw.get("query_categories"), f"{path}.query_categories", nonempty=True)
146
+ if any(category not in _CATEGORIES for category in categories):
147
+ raise ContractError(f"{path}.query_categories contains an unsupported category")
148
+ if "query_variants" in raw:
149
+ variants = _list(raw["query_variants"], f"{path}.query_variants", nonempty=True)
150
+ for variant_index, variant in enumerate(variants):
151
+ _text(variant, f"{path}.query_variants[{variant_index}]")
152
+ if len(set(variants)) != len(variants):
153
+ raise ContractError(f"{path}.query_variants must be unique")
154
+ _optional_text(raw, "source_case_id", path)
155
+ if raw.get("suite_type") == "customer_acceptance":
156
+ _id(raw.get("customer"), f"{path}.customer")
157
+ elif "customer" in raw:
158
+ _id(raw["customer"], f"{path}.customer")
159
+ versions_raw = _mapping(raw.get("versions"), f"{path}.versions")
160
+ versions = CorpusVersions(**{field: _id(versions_raw.get(field), f"{path}.versions.{field}") for field in ("corpus", "index", "chunking", "embedding")})
161
+ candidate_values = _list(raw.get("candidates"), f"{path}.candidates", nonempty=True)
162
+ group_values = _list(raw.get("evidence_groups"), f"{path}.evidence_groups")
163
+ candidates: list[FrozenCandidate] = []
164
+ reference_candidates: list[ReferenceCandidate] = []
165
+ raw_candidates: list[Mapping[str, Any]] = []
166
+ for candidate_index, value in enumerate(candidate_values):
167
+ candidate_path = f"{path}.candidates[{candidate_index}]"
168
+ item = _mapping(value, candidate_path)
169
+ raw_candidates.append(item)
170
+ if type(item.get("dense_rank")) is not int or item["dense_rank"] != candidate_index + 1:
171
+ raise ContractError(f"{candidate_path}.dense_rank must equal pool position")
172
+ grade = item.get("relevance_grade")
173
+ if type(grade) is not int or not 0 <= grade <= 3:
174
+ raise ContractError(f"{candidate_path}.relevance_grade must be 0..3")
175
+ frozen = FrozenCandidate(
176
+ candidate_id=_id(item.get("candidate_id"), f"{candidate_path}.candidate_id"),
177
+ document_id=_id(item.get("document_id"), f"{candidate_path}.document_id"),
178
+ source_id=_id(item.get("source_id"), f"{candidate_path}.source_id"),
179
+ chunk_id=_id(item.get("chunk_id"), f"{candidate_path}.chunk_id"),
180
+ url=_text(item.get("url"), f"{candidate_path}.url"),
181
+ input=_text(item.get("input"), f"{candidate_path}.input"),
182
+ dense_rank=item["dense_rank"],
183
+ dense_score=_finite_number(item.get("dense_score"), f"{candidate_path}.dense_score"),
184
+ )
185
+ candidates.append(frozen)
186
+ reference_candidates.append(ReferenceCandidate(
187
+ frozen.candidate_id,
188
+ grade,
189
+ _unique_ids(item.get("evidence_group_ids"), f"{candidate_path}.evidence_group_ids"),
190
+ ))
191
+ _validate_candidate_annotations(item, candidate_path)
192
+ candidate_ids = {candidate.candidate_id for candidate in candidates}
193
+ if len(candidate_ids) != len(candidates):
194
+ raise ContractError(f"{path}.candidates must have unique IDs")
195
+ for candidate_index, item in enumerate(raw_candidates):
196
+ if "duplicate_of_candidate_id" in item:
197
+ duplicate_id = _id(item["duplicate_of_candidate_id"], f"{path}.candidates[{candidate_index}].duplicate_of_candidate_id")
198
+ if duplicate_id not in candidate_ids or duplicate_id == candidates[candidate_index].candidate_id:
199
+ raise ContractError(f"{path}.candidates[{candidate_index}].duplicate_of_candidate_id is invalid")
200
+ groups: list[EvidenceGroup] = []
201
+ for group_index, value in enumerate(group_values):
202
+ group_path = f"{path}.evidence_groups[{group_index}]"
203
+ item = _mapping(value, group_path)
204
+ groups.append(EvidenceGroup(
205
+ evidence_group_id=_id(item.get("evidence_group_id"), f"{group_path}.evidence_group_id"),
206
+ description=_text(item.get("description"), f"{group_path}.description"),
207
+ required=item.get("required"),
208
+ acceptable_candidate_ids=_unique_ids(item.get("acceptable_candidate_ids"), f"{group_path}.acceptable_candidate_ids"),
209
+ ))
210
+ group_ids = {group.evidence_group_id for group in groups}
211
+ if len(group_ids) != len(groups):
212
+ raise ContractError(f"{path}.evidence_groups must have unique IDs")
213
+ refs_by_id = {reference.candidate_id: reference for reference in reference_candidates}
214
+ for reference in reference_candidates:
215
+ for group_id in reference.evidence_group_ids:
216
+ if group_id not in group_ids:
217
+ raise ContractError(f"{path}.candidates[{reference.candidate_id}] links unknown evidence group {group_id}")
218
+ group = next(group for group in groups if group.evidence_group_id == group_id)
219
+ if reference.candidate_id not in group.acceptable_candidate_ids:
220
+ raise ContractError(f"{path} evidence-group link is not reciprocal for {reference.candidate_id}")
221
+ for group in groups:
222
+ for candidate_id in group.acceptable_candidate_ids:
223
+ reference = refs_by_id.get(candidate_id)
224
+ if reference is None or group.evidence_group_id not in reference.evidence_group_ids:
225
+ raise ContractError(f"{path} evidence-group link is not reciprocal for {candidate_id}")
226
+ if reference.relevance_grade < 2:
227
+ raise ContractError(f"{path} acceptable evidence {candidate_id} must have grade >= 2")
228
+ required_count = sum(group.required for group in groups)
229
+ if answerability == "answerable" and required_count == 0:
230
+ raise ContractError(f"{path} answerable case needs a required evidence group")
231
+ if answerability == "unsupported":
232
+ if required_count != 0 or raw.get("expected_unsupported_behavior") != "abstain":
233
+ raise ContractError(f"{path} unsupported case requires no required groups and abstain behavior")
234
+ elif "expected_unsupported_behavior" in raw:
235
+ raise ContractError(f"{path} answerable case cannot specify unsupported behavior")
236
+ case = FrozenPoolCase(
237
+ dataset_id=dataset_id,
238
+ dataset_version=dataset_version,
239
+ case_id=case_id,
240
+ query_id=query_id,
241
+ query=query,
242
+ split=raw.get("split"),
243
+ split_group_id=split_group_id,
244
+ cluster_id=cluster_id,
245
+ suite_type=raw.get("suite_type"),
246
+ versions=versions,
247
+ candidates=tuple(candidates),
248
+ )
249
+ references = CaseReferences(case_id, label_authority, tuple(reference_candidates), tuple(groups))
250
+ return case, references
251
+
252
+
253
+ def validate_dataset_v2(value: object) -> tuple[tuple[FrozenPoolCase, ...], tuple[CaseReferences, ...], int]:
254
+ """Validate dataset-v2 identity, pools, labels, and reciprocal group links.
255
+
256
+ This validates the *schema*. Release identity is checked separately by the
257
+ pinned fixture hash in :func:`load_rag_rerank_pack`. As v2 has no label
258
+ authority field, arbitrary input is marked provisional by this validator.
259
+ """
260
+
261
+ dataset = _mapping(value, "dataset")
262
+ if dataset.get("schema_version") != CASE_SCHEMA_VERSION:
263
+ raise ContractError(f"schema_version must be {CASE_SCHEMA_VERSION}")
264
+ dataset_id = _id(dataset.get("dataset_id"), "dataset_id")
265
+ dataset_version = _id(dataset.get("version"), "version")
266
+ threshold = dataset.get("default_relevance_threshold")
267
+ if type(threshold) is not int or threshold not in (1, 2, 3):
268
+ raise ContractError("default_relevance_threshold must be 1, 2, or 3")
269
+ tasks = _list(dataset.get("tasks"), "tasks", nonempty=True)
270
+ pairs = tuple(_validate_task(task, index, dataset_id, dataset_version, "provisional") for index, task in enumerate(tasks))
271
+ cases = tuple(case for case, _ in pairs)
272
+ references = tuple(reference for _, reference in pairs)
273
+ for field in ("case_id", "query_id"):
274
+ values = [getattr(case, field) for case in cases]
275
+ if len(set(values)) != len(values):
276
+ raise ContractError(f"tasks must have unique {field} values")
277
+ for field in ("cluster_id", "split_group_id"):
278
+ split_by_group: dict[str, str] = {}
279
+ for case in cases:
280
+ group = getattr(case, field)
281
+ if group in split_by_group and split_by_group[group] != case.split:
282
+ raise ContractError(f"{field} {group!r} crosses dataset splits")
283
+ split_by_group[group] = case.split
284
+ return cases, references, threshold
285
+
286
+
287
+ @lru_cache(maxsize=1)
288
+ def load_rag_rerank_pack() -> RagRerankPack:
289
+ """Load the installed six-case synthetic pack independently of current CWD."""
290
+
291
+ payload = resources.files("sphynx").joinpath("data", _FIXTURE_NAME).read_bytes()
292
+ sha = hashlib.sha256(payload).hexdigest()
293
+ if sha != FIXTURE_SHA256:
294
+ raise ContractError("Installed RAG fixture bytes differ from pinned illustrative release")
295
+ try:
296
+ value = json.loads(payload)
297
+ except (UnicodeDecodeError, json.JSONDecodeError) as exc:
298
+ raise ContractError("Installed RAG fixture is not UTF-8 JSON") from exc
299
+ cases, _, threshold = validate_dataset_v2(value)
300
+ # Synthetic authority applies only after the exact installed fixture byte
301
+ # hash above has been verified, never to an arbitrary v2 dataset.
302
+ references = tuple(
303
+ _validate_task(task, index, cases[index].dataset_id, cases[index].dataset_version, "synthetic_authored")[1]
304
+ for index, task in enumerate(value["tasks"])
305
+ )
306
+ descriptor = get_pack(PACK_ID)
307
+ if not descriptor.runnable or descriptor.case_count != len(cases) or descriptor.metric_release is None:
308
+ raise ContractError("RAG descriptor disagrees with the installed fixture or metric release")
309
+ if len(cases) != 6 or sum(len(case.candidates) for case in cases) != 25:
310
+ raise ContractError("Illustrative RAG release must contain exactly six cases and 25 passages")
311
+ return RagRerankPack(descriptor, cases, references, descriptor.metric_release, f"sha256:{sha}", threshold)
312
+
313
+
314
+ def load_cases(pack_id: str) -> tuple[FrozenPoolCase, ...]:
315
+ """Return only label-free cases in fixed pack order for candidate execution."""
316
+
317
+ if pack_id != PACK_ID:
318
+ raise ValueError(f"No case loader for pack {pack_id!r}; installed: {PACK_ID}")
319
+ return load_rag_rerank_pack().cases
320
+
321
+
322
+ def validate_pack_case_identity(case: FrozenPoolCase) -> None:
323
+ """Reject a changed pool/corpus/cohort before comparing to pack references."""
324
+
325
+ if not isinstance(case, FrozenPoolCase):
326
+ raise ContractError("case must be FrozenPoolCase")
327
+ original = next((item for item in load_cases(PACK_ID) if item.case_id == case.case_id), None)
328
+ if original is None or original.pool_digest != case.pool_digest:
329
+ raise ContractError(f"Case {case.case_id!r} does not match the frozen pack pool and corpus identity")
@@ -0,0 +1,309 @@
1
+ """Versioned, offline RAG diagnostics over caller-held paired traces.
2
+
3
+ These APIs regrade saved outputs. They never execute candidates, contact a model,
4
+ download a benchmark, grant hosted admission, or infer semantic review labels.
5
+ Reports contain owner-held text and references: keep them in private storage.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import copy
10
+ from importlib.resources import files
11
+ import json
12
+ from pathlib import Path
13
+ from typing import Any, Mapping, Sequence
14
+ from uuid import uuid4
15
+
16
+ from ._rag_diagnostics_common import VERSION, canonical, digest
17
+ from ._rag_diagnostics_contracts import (
18
+ ContractError, candidate_view, case_input_digest, load_dataset, validate_dataset, _json_domain,
19
+ )
20
+ from ._rag_diagnostics_engine import evaluate
21
+ from ._rag_diagnostics_catalog import INFO, describe
22
+ from ._rag_diagnostics_calibration import judge_audit, ppi_mean
23
+ from ._rag_diagnostics_importers import import_beir
24
+
25
+ __all__ = [
26
+ "load_rag_diagnostics_dataset", "validate_rag_diagnostics_dataset",
27
+ "rag_candidate_view", "grade_rag_diagnostics", "load_rag_diagnostics_demo",
28
+ "list_rag_diagnostics_metrics", "rag_diagnostics_capabilities",
29
+ "save_rag_diagnostics_report", "load_rag_diagnostics_report",
30
+ "render_rag_diagnostics_report", "import_rag_diagnostics_beir",
31
+ "audit_rag_judge", "estimate_rag_ppi_mean",
32
+ "create_rag_diagnostics_review_template",
33
+ ]
34
+
35
+ CONDITIONAL = {
36
+ "context.retrieval_sufficiency_recall": (
37
+ "Retrieval sufficiency recall", r"S_{\mathrm{retrieved}}/N_{\mathrm{answerable}}",
38
+ "Among independently source-answerable cases, count sufficient retrieved contexts. Unknown eligibility or missing judgments suppress the primary value.",
39
+ ["independent source answerability", "trace-bound retrieved-context sufficiency"],
40
+ ),
41
+ "generation.success_given_sufficient_context": (
42
+ "Success given sufficient context", r"N_{\mathrm{correct,\ answered,\ sufficient}}/N_{\mathrm{sufficient}}",
43
+ "Among sufficient delivered contexts, count correct non-abstaining answers. Correctness is reviewed independently of support.",
44
+ ["trace-bound prompt sufficiency", "independent answer correctness", "successful response and abstention status"],
45
+ ),
46
+ "generation.unsupported_answer_given_insufficient_context": (
47
+ "Unsupported answer given insufficient context", r"N_{\mathrm{unsupported,\ insufficient}}/N_{\mathrm{insufficient}}",
48
+ "Among insufficient delivered contexts, count responses with at least one unsupported claim. Requires exhaustive review; an empty appropriate abstention is not an unsupported answer.",
49
+ ["trace-bound prompt sufficiency", "exhaustive response claim review"],
50
+ ),
51
+ "outcome.abstention_recall": (
52
+ "Abstention recall", r"N_{\mathrm{abstained,\ expected}}/N_{\mathrm{expected}}",
53
+ "Among cases explicitly labeled as requiring abstention, count actual abstentions. Source answerability is not substituted for the expected action.",
54
+ ["independent expected-abstention label", "response status and abstention"],
55
+ ),
56
+ }
57
+
58
+
59
+ def load_rag_diagnostics_dataset(path: str | Path) -> dict[str, Any]:
60
+ """Read a bounded UTF-8 dataset and validate IDs, lineage and review digests."""
61
+ return load_dataset(path)
62
+
63
+
64
+ def validate_rag_diagnostics_dataset(dataset: dict[str, Any]) -> dict[str, Any]:
65
+ """Validate the versioned paired-trace contract; return the supplied object."""
66
+ return validate_dataset(dataset)
67
+
68
+
69
+ def rag_candidate_view(case: dict[str, Any]) -> dict[str, Any]:
70
+ """Copy only case ID, question and documents; omit protected scoring labels."""
71
+ return candidate_view(case)
72
+
73
+
74
+ def load_rag_diagnostics_demo() -> dict[str, Any]:
75
+ """Load 32 explicitly synthetic saved cases spanning 16 diagnostic kinds."""
76
+ value = json.loads(files("sphynx").joinpath("data/rag-diagnostics-synthetic.v1.json").read_text(encoding="utf-8"))
77
+ return validate_dataset(value)
78
+
79
+
80
+ def create_rag_diagnostics_review_template(
81
+ dataset: dict[str, Any], *, variant: str = "candidate"
82
+ ) -> dict[str, Any]:
83
+ """Create pending, exact-input review forms; no semantic labels are inferred.
84
+
85
+ Fill the form through independent review before copying a case review into
86
+ its protected references. claims_exhaustive=False and null judgments are
87
+ deliberate. For recorded traces, identify the actual human reviewer.
88
+ """
89
+ validate_dataset(dataset)
90
+ if variant not in ("baseline", "candidate"):
91
+ raise ContractError("Select baseline or candidate for review")
92
+ return {
93
+ "schema_version": "sphynx.rag-diagnostics.review-template.v1",
94
+ "status": "pending_review", "variant": variant,
95
+ "reviews": {
96
+ case["id"]: {
97
+ "trace_digest": digest(case["traces"][variant]),
98
+ "case_input_digest": case_input_digest(case),
99
+ "provenance": {"kind": "synthetic" if case["traces"][variant]["execution_mode"] == "synthetic_replay" else "human_adjudicated",
100
+ "reviewer": "REPLACE_WITH_ACTUAL_REVIEWER"},
101
+ "answer_correct": None, "answer_relevant": None,
102
+ "abstention_expected": None,
103
+ "context_sufficiency": {stage: None for stage in ("retrieved", "selected", "prompt")},
104
+ "claims_exhaustive": False, "claims": [],
105
+ } for case in dataset["cases"]
106
+ },
107
+ }
108
+
109
+
110
+ def grade_rag_diagnostics(
111
+ dataset: dict[str, Any], *, cutoff: int = 3, bootstrap_samples: int = 500
112
+ ) -> dict[str, Any]:
113
+ """Grade both saved variants with explicit unavailable values and paired deltas.
114
+
115
+ The input must use sphynx.rag-diagnostics.dataset.v1. Semantic review must be
116
+ bound to the exact trace and case inputs. Missing eligible pairs suppress the
117
+ primary comparison. Recorded telemetry is never replay grading duration.
118
+ """
119
+ result = evaluate(copy.deepcopy(dataset), cutoff, bootstrap_samples)
120
+ result["evaluation_id"] = "ragdiag-" + uuid4().hex
121
+ result["sdk_version"] = "0.5.0"
122
+ result["trust_tier"] = "local_unverified"
123
+ result["hosted_admitted"] = False
124
+ return result
125
+
126
+
127
+ def _requirements(identity: str) -> list[str]:
128
+ if identity.startswith("retrieval."):
129
+ return ["known qrels over the full reference population", "ordered recorded stage IDs"]
130
+ if identity.startswith("evidence."):
131
+ return ["required evidence atoms or relations", "protected support-document mapping", "recorded stage IDs"]
132
+ if identity.startswith("context."):
133
+ return ["exact recorded context", "independent trace-bound sufficiency review"]
134
+ if identity.startswith(("generation.", "citation.", "outcome.")):
135
+ return ["successful saved response or explicit failure", "trace-bound independent semantic review", "metric-specific claim, fact or citation judgments"]
136
+ return ["explicit saved execution status", "synthetic or component-reported telemetry where required"]
137
+
138
+
139
+ def list_rag_diagnostics_metrics(*, cutoff: int = 3) -> list[dict[str, Any]]:
140
+ """List 29 case definitions and four conditional definitions with requirements."""
141
+ if type(cutoff) is not int or not 1 <= cutoff <= 1000:
142
+ raise ValueError("cutoff must be an integer from 1 to 1000")
143
+ result = []
144
+ for base in INFO:
145
+ identities = (
146
+ [base + "@" + str(cutoff)] if base.startswith("retrieval.")
147
+ else [base + "." + stage for stage in ("retrieved", "selected", "prompt")]
148
+ if base == "context.sufficient" else [base]
149
+ )
150
+ for identity in identities:
151
+ item = describe(identity)
152
+ result.append({
153
+ "metric_id": identity, "metric_version": VERSION, "scope": "case",
154
+ **item, "requires": _requirements(identity),
155
+ "missing_policy": "Unavailable inputs remain null with a reason; zero-population cases are explicit.",
156
+ })
157
+ for identity, (title, formula, meaning, requires) in CONDITIONAL.items():
158
+ result.append({
159
+ "metric_id": identity, "metric_version": VERSION, "scope": "conditional",
160
+ "title": title, "formula": formula, "meaning": meaning, "requires": requires,
161
+ "missing_policy": "Unknown eligibility or missing eligible scores suppress the primary value; observed subsets are named separately.",
162
+ })
163
+ return result
164
+
165
+
166
+ def rag_diagnostics_capabilities(
167
+ dataset: dict[str, Any], *, cutoff: int = 3
168
+ ) -> dict[str, Any]:
169
+ """Resolve actual score availability from supplied inputs without execution."""
170
+ report = grade_rag_diagnostics(dataset, cutoff=cutoff, bootstrap_samples=0)
171
+ variants = {}
172
+ for variant in ("baseline", "candidate"):
173
+ variants[variant] = {
174
+ "case_metrics": {
175
+ key: {field: value[field] for field in (
176
+ "metric_id", "stage", "eligible_cases", "scored_cases",
177
+ "missing_cases", "not_applicable_cases", "complete", "reason_counts",
178
+ )}
179
+ for key, value in report["aggregates"][variant].items()
180
+ },
181
+ "conditional_metrics": report["conditional"][variant],
182
+ }
183
+ return {
184
+ "schema_version": "sphynx.rag-diagnostics.capabilities.v1",
185
+ "metric_version": VERSION, "dataset_digest": report["dataset_digest"],
186
+ "source": "Availability for supplied saved inputs; no hosted admission or fresh execution.",
187
+ "variants": variants,
188
+ }
189
+
190
+
191
+ def import_rag_diagnostics_beir(
192
+ corpus_path: str | Path, queries_path: str | Path, qrels_path: str | Path,
193
+ baseline_run_path: str | Path, candidate_run_path: str | Path, *,
194
+ release: str = "external-beir-replay.v1", cluster_mapping_path: str | Path | None = None,
195
+ expected_sha256: dict[str, str] | None = None,
196
+ ) -> dict[str, Any]:
197
+ """Import caller-held corpus/query/qrel files and two saved TREC runs offline.
198
+
199
+ Optional SHA256 pins must cover every supplied file. This creates no answers
200
+ or semantic reviews, downloads nothing, and grants no data or license access.
201
+ """
202
+ return import_beir(
203
+ corpus_path, queries_path, qrels_path, baseline_run_path, candidate_run_path,
204
+ release=release, cluster_mapping_path=cluster_mapping_path,
205
+ expected_sha256=expected_sha256,
206
+ )
207
+
208
+
209
+ def audit_rag_judge(
210
+ records: Sequence[Mapping[str, Any]], *, provenance_kind: str = "unspecified"
211
+ ) -> dict[str, Any]:
212
+ """Compute descriptive agreement over supplied human/judge labels; no model."""
213
+ return judge_audit(records, provenance_kind=provenance_kind)
214
+
215
+
216
+ def estimate_rag_ppi_mean(
217
+ predictions: Sequence[Mapping[str, Any]], audit: Sequence[Mapping[str, Any]], *,
218
+ declarations: Mapping[str, Any] | None = None, confidence: float = 0.95,
219
+ ) -> dict[str, Any]:
220
+ """Compute gated independent-sample PPI arithmetic, never validated coverage."""
221
+ return ppi_mean(predictions, audit, declarations=declarations, confidence=confidence)
222
+
223
+
224
+ def _validate_report(report: dict[str, Any]) -> None:
225
+ _json_domain(report)
226
+ if not isinstance(report, dict) or report.get("schema_version") != "sphynx.rag-diagnostics.report.v1" or report.get("metric_version") != VERSION:
227
+ raise ContractError("Unsupported RAG diagnostic report schema or metric version")
228
+
229
+
230
+ def save_rag_diagnostics_report(report: dict[str, Any], path: str | Path) -> dict[str, Any]:
231
+ """Save a checksummed private JSON report; refuse to overwrite an existing file."""
232
+ _validate_report(report)
233
+ envelope = {
234
+ "schema_version": "sphynx.rag-diagnostics.saved-report.v1",
235
+ "content_sha256": digest(report), "report": report,
236
+ }
237
+ raw = canonical(envelope) + b"\n"
238
+ if len(raw) > 20_000_000:
239
+ raise ContractError("Saved diagnostic report exceeds 20MB")
240
+ target = Path(path)
241
+ target.parent.mkdir(parents=True, exist_ok=True)
242
+ try:
243
+ with target.open("xb") as handle:
244
+ handle.write(raw)
245
+ except FileExistsError:
246
+ raise ContractError("Report already exists; choose a new report filename") from None
247
+ return {"path": str(target.resolve()), "content_sha256": envelope["content_sha256"],
248
+ "evaluation_id": report.get("evaluation_id"), "bytes": len(raw)}
249
+
250
+
251
+ def load_rag_diagnostics_report(path: str | Path) -> dict[str, Any]:
252
+ """Reopen a checksummed report without regrading or executing a candidate.
253
+
254
+ A checksum detects accidental byte/content changes; it is not a signature
255
+ or proof of the truth of owner-supplied review or telemetry.
256
+ """
257
+ target = Path(path)
258
+ if target.is_symlink() or not target.is_file() or target.stat().st_size > 20_000_000:
259
+ raise ContractError("Expected a regular diagnostic report no larger than 20MB")
260
+ raw = target.read_bytes()
261
+ if len(raw) > 20_000_000:
262
+ raise ContractError("Saved diagnostic report exceeds 20MB")
263
+ def pairs(items):
264
+ result = {}
265
+ for key, value in items:
266
+ if key in result:
267
+ raise ContractError("Duplicate JSON key in diagnostic report")
268
+ result[key] = value
269
+ return result
270
+ try:
271
+ envelope = json.loads(raw, object_pairs_hook=pairs,
272
+ parse_constant=lambda _: (_ for _ in ()).throw(ContractError("Nonfinite JSON number")))
273
+ except (ValueError, UnicodeDecodeError, RecursionError) as exc:
274
+ raise ContractError("Invalid diagnostic report JSON") from exc
275
+ _json_domain(envelope)
276
+ if type(envelope) is not dict or set(envelope) != {"schema_version", "content_sha256", "report"} or envelope["schema_version"] != "sphynx.rag-diagnostics.saved-report.v1":
277
+ raise ContractError("Unsupported saved diagnostic report envelope")
278
+ report = envelope["report"]
279
+ _validate_report(report)
280
+ if envelope["content_sha256"] != digest(report):
281
+ raise ContractError("Diagnostic report checksum mismatch")
282
+ return report
283
+
284
+
285
+ def render_rag_diagnostics_report(report: dict[str, Any]) -> str:
286
+ """Render aggregate scores and missingness as Markdown without raw case text."""
287
+ _validate_report(report)
288
+ lines = [
289
+ "# Sphynx saved-trace RAG diagnostics", "",
290
+ "LOCAL_UNVERIFIED. Regrading saved outputs does not run a candidate.",
291
+ "Semantic reviews are supplied labels; synthetic examples do not measure model quality.", "",
292
+ f"Dataset: {report['dataset_release']}; cases: {report['case_count']}; cutoff: {report['cutoff']}.", "",
293
+ "| Metric / stage | Baseline | Candidate | Missing pairs |",
294
+ "|---|---:|---:|---:|",
295
+ ]
296
+ def show(value):
297
+ return "Unavailable" if value is None else f"{value:.6g}"
298
+ for row in report["comparisons"]:
299
+ label = row["key"].replace("|", " / ").replace("\n", " ")
300
+ lines.append(f"| {label} | {show(row['baseline']['value'])} | {show(row['candidate']['value'])} | {row['missing_pairs']} |")
301
+ lines += ["", "Conditional metrics retain their own eligible populations:", "",
302
+ "| Metric | Baseline | Candidate |", "|---|---:|---:|"]
303
+ for left, right in zip(report["conditional"]["baseline"], report["conditional"]["candidate"]):
304
+ lines.append(f"| {left['metric_id']} | {show(left['value'])} | {show(right['value'])} |")
305
+ lines += ["", "Saved telemetry is unverified synthetic/component data. No fresh model timing was measured.",
306
+ "Failure localization is observational. PPI arithmetic does not establish validated coverage.", ""]
307
+ return "\n".join(lines)
308
+
309
+