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,186 @@
1
+ """Validate the local language-neutral case/trace boundary before grading."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import math
6
+ import copy
7
+ from pathlib import Path
8
+
9
+ from ._rag_diagnostics_common import STAGES, digest
10
+
11
+
12
+ class ContractError(ValueError):
13
+ pass
14
+
15
+
16
+ def require(condition, message):
17
+ if not condition:
18
+ raise ContractError(message)
19
+
20
+
21
+ def unique_ids(values, name):
22
+ require(type(values) is list and all(type(v) is str and 0 < len(v) <= 180 for v in values), name+' must be an ID list')
23
+ require(len(values) == len(set(values)), name+' has duplicate IDs')
24
+ return set(values)
25
+
26
+
27
+ def optional_bool(value, name):
28
+ require(value is None or type(value) is bool, name+' must be boolean or null')
29
+
30
+
31
+ def _json_domain(value, depth=0, ancestors=None):
32
+ """Reject non-JSON objects, nonfinite numbers and recursive direct inputs."""
33
+ require(depth <= 64, 'Dataset nesting exceeds the local 64-level limit')
34
+ if value is None or type(value) in (str, bool, int):
35
+ return
36
+ if type(value) is float:
37
+ require(math.isfinite(value), 'Dataset numbers must be finite')
38
+ return
39
+ require(type(value) in (dict, list), 'Dataset values must be JSON objects, lists or scalar values')
40
+ ancestors = set() if ancestors is None else ancestors
41
+ identity = id(value)
42
+ require(identity not in ancestors, 'Dataset must not contain recursive values')
43
+ ancestors.add(identity)
44
+ if type(value) is dict:
45
+ require(all(type(key) is str for key in value), 'JSON object keys must be strings')
46
+ children = value.values()
47
+ else:
48
+ children = value
49
+ for child in children:
50
+ _json_domain(child, depth+1, ancestors)
51
+ ancestors.remove(identity)
52
+
53
+
54
+ def case_input_digest(case):
55
+ """Bind a semantic review to exact public candidate inputs, not just IDs."""
56
+ return digest({key: case[key] for key in ('id', 'question', 'documents')})
57
+
58
+
59
+ def validate_dataset(data):
60
+ _json_domain(data)
61
+ require(type(data) is dict and data.get('schema_version') == 'sphynx.rag-diagnostics.dataset.v1', 'Unsupported dataset schema')
62
+ require(type(data.get('release')) is str and data['release'], 'Dataset release is required')
63
+ require(type(data.get('suites')) is list and 0 < len(data['suites']) <= 5000, 'Expected 1–5000 suites')
64
+ require(all(type(s) is dict for s in data['suites']), 'Each suite must be an object')
65
+ suite_ids=unique_ids([s.get('id') for s in data['suites']], 'suite IDs')
66
+ require(type(data.get('cases')) is list and 0 < len(data['cases']) <= 5000, 'Expected 1–5000 cases')
67
+ require(all(type(c) is dict for c in data['cases']), 'Each case must be an object')
68
+ unique_ids([c.get('id') for c in data['cases']], 'case IDs')
69
+ for case in data['cases']:
70
+ name=case['id']
71
+ require(case.get('suite_id') in suite_ids, name+': unknown suite')
72
+ require(type(case.get('cluster_id')) is str and case['cluster_id'], name+': independent cluster identity required')
73
+ require(type(case.get('question')) is str and 0 < len(case['question']) <= 20000, name+': question required')
74
+ docs=case.get('documents')
75
+ require(type(docs) is list and len(docs) <= 10000, name+': invalid documents')
76
+ require(all(type(d) is dict for d in docs), name+': each document must be an object')
77
+ ids=unique_ids([d.get('id') for d in docs], name+' document IDs')
78
+ require(all(type(d.get('text')) is str for d in docs), name+': document text required')
79
+ refs=case.get('references')
80
+ require(type(refs) is dict, name+': protected references required')
81
+ reviews=refs.get('reviews', {})
82
+ require(type(reviews) is dict and set(reviews) <= {'baseline','candidate'}, name+': invalid review variants')
83
+ optional_bool(refs.get('corpus_answerable'), name+' corpus answerability')
84
+ qrels=refs.get('qrels')
85
+ require(qrels is None or (type(qrels) is dict and set(qrels) <= ids and all(type(g) is int and 0 <= g <= 100 for g in qrels.values())), name+': invalid qrels')
86
+ for required,mapping in [('required_atoms','atom_support'),('required_relations','relation_support')]:
87
+ needed=refs.get(required)
88
+ if needed is not None:
89
+ unique_ids(needed, name+' '+required)
90
+ support=refs.get(mapping)
91
+ if support is not None:
92
+ require(type(support) is dict, name+': invalid '+mapping)
93
+ for key,values in support.items():
94
+ require(type(key) is str and key, name+': invalid support key')
95
+ require(unique_ids(values, name+' support') <= ids, name+': support references unknown documents')
96
+ traces=case.get('traces')
97
+ require(type(traces) is dict and set(traces)=={'baseline','candidate'}, name+': paired traces required')
98
+ for variant,trace in traces.items():
99
+ require(type(trace) is dict and trace.get('execution_mode') in ('synthetic_replay','recorded_replay'), name+': replay mode required')
100
+ require(type(trace.get('id')) is str and trace['id'], name+': trace identity required')
101
+ stages=trace.get('stages')
102
+ require(type(stages) is dict and set(stages)==set(STAGES), name+': explicit stage lists/nulls required')
103
+ previous=None
104
+ for stage in STAGES:
105
+ values=stages[stage]
106
+ if values is None:
107
+ continue
108
+ found=unique_ids(values, name+' '+stage)
109
+ require(found <= ids, name+': stage references unknown document')
110
+ if previous is not None:
111
+ require(found <= previous, name+': stage cannot invent documents after upstream selection')
112
+ previous=found
113
+ response=trace.get('response')
114
+ require(type(response) is dict and response.get('status') in ('ok','error','timeout'), name+': response status required')
115
+ require(type(response.get('text')) is str, name+': response text required')
116
+ optional_bool(response.get('abstained'), name+' abstained')
117
+ ops=trace.get('operational')
118
+ require(type(ops) is dict and ops.get('origin') in ('synthetic','component_reported','attested'), name+': telemetry origin required')
119
+ require(ops.get('origin')!='attested', name+': SDK cannot validate telemetry attestation')
120
+ for key in ('latency_ms','cost_usd','tokens'):
121
+ value=ops.get(key)
122
+ require(value is None or (type(value) in (int,float) and math.isfinite(value) and value>=0), name+': invalid telemetry '+key)
123
+ if key=='tokens' and value is not None:
124
+ require(type(value) is int, name+': tokens must be integer')
125
+ review=reviews.get(variant)
126
+ if review is None:
127
+ continue
128
+ require(type(review) is dict and review.get('trace_digest')==digest(trace), name+': stale or mismatched review')
129
+ require(review.get('case_input_digest')==case_input_digest(case), name+': stale or mismatched review case inputs')
130
+ provenance=review.get('provenance')
131
+ require(type(provenance) is dict and provenance.get('kind') in ('synthetic','human_adjudicated') and type(provenance.get('reviewer')) is str and provenance['reviewer'].strip(), name+': review provenance required')
132
+ require(trace['execution_mode']!='synthetic_replay' or provenance['kind']=='synthetic', name+': synthetic trace requires synthetic review provenance')
133
+ for field in ('answer_correct','answer_relevant','abstention_expected'):
134
+ optional_bool(review.get(field), name+' '+field)
135
+ suff=review.get('context_sufficiency',{})
136
+ require(type(suff) is dict and set(suff) <= {'retrieved','selected','prompt'}, name+': invalid sufficiency stages')
137
+ for stage,value in suff.items():
138
+ optional_bool(value, name+' sufficient '+stage)
139
+ require(value is None or stages[stage] is not None, name+': cannot judge missing context')
140
+ require(type(review.get('claims_exhaustive')) is bool and type(review.get('claims')) is list, name+': claim audit state required')
141
+ claims=review['claims']
142
+ require(all(type(c) is dict for c in claims), name+': each reviewed claim must be an object')
143
+ unique_ids([c.get('id') for c in claims], name+' claim IDs')
144
+ prompt=set(stages['prompt'] or [])
145
+ for claim in claims:
146
+ require(type(claim.get('text')) is str and claim['text'], name+': claim text required')
147
+ for field in ('supported_by_context','correct','joint_citation_support'):
148
+ optional_bool(claim.get(field), name+' claim '+field)
149
+ require(type(claim.get('requires_citation')) is bool, name+': citation requirement required')
150
+ cited=unique_ids(claim.get('cited_ids'), name+' cited IDs')
151
+ require(cited <= ids, name+': citation ID does not exist')
152
+ # A dangling/out-of-prompt citation is an observed citation validity
153
+ # failure, not an assertion of semantic support.
154
+ if claim.get('joint_citation_support') is True:
155
+ require(cited and cited <= prompt, name+': supported citation leaves handed context')
156
+ unique_ids(claim.get('reference_atoms'), name+' expressed reference atoms')
157
+ if refs.get('required_atoms') is not None:
158
+ require(set(claim['reference_atoms']) <= set(refs['required_atoms']), name+': unknown expressed reference atom')
159
+ for field in ('individual_support','leave_one_out_support'):
160
+ judgments=claim.get(field)
161
+ require(type(judgments) is dict and set(judgments) <= cited, name+': invalid citation judgments')
162
+ for value in judgments.values():
163
+ optional_bool(value, name+' citation judgment')
164
+ return data
165
+
166
+
167
+ def load_dataset(path):
168
+ raw=Path(path).read_bytes()
169
+ require(len(raw)<=20_000_000, 'Dataset exceeds 20MB local limit')
170
+ def pairs(items):
171
+ out={}
172
+ for key,value in items:
173
+ require(key not in out, 'Duplicate JSON object key: '+key)
174
+ out[key]=value
175
+ return out
176
+ try:
177
+ data=json.loads(raw,object_pairs_hook=pairs,parse_constant=lambda value: (_ for _ in ()).throw(ContractError('Nonfinite JSON number')))
178
+ except (json.JSONDecodeError, UnicodeDecodeError, RecursionError) as error:
179
+ raise ContractError('Invalid or excessively nested JSON dataset') from error
180
+ return validate_dataset(data)
181
+
182
+
183
+ def candidate_view(case):
184
+ """Protected labels are never part of a candidate payload."""
185
+ return copy.deepcopy({'id':case['id'],'question':case['question'],
186
+ 'documents':[{key:doc[key] for key in ('id','text')} for doc in case['documents']]})
@@ -0,0 +1,243 @@
1
+ """Grade saved traces and compare compatible cases. Never executes candidates."""
2
+ from __future__ import annotations
3
+
4
+ from collections import Counter, defaultdict
5
+ from datetime import datetime, timezone
6
+ import random
7
+ import statistics
8
+
9
+ from ._rag_diagnostics_common import digest, metric
10
+ from ._rag_diagnostics_contracts import validate_dataset
11
+ from ._rag_diagnostics_ranking import ranking_metrics, evidence_metrics
12
+ from ._rag_diagnostics_semantic import semantic_metrics
13
+
14
+
15
+ def operational_metrics(case,trace):
16
+ rows=[]
17
+ ops=trace['operational']
18
+ for name,unit in [('latency_ms','ms'),('cost_usd','USD'),('tokens','tokens')]:
19
+ value=ops.get(name)
20
+ row=metric('operational.recorded_'+name,case['id'],'operational',value,
21
+ grader_type=ops['origin']+'_telemetry',unit=unit,direction='lower',
22
+ reason='Saved '+ops['origin']+' telemetry; replay grading duration is not model execution time')
23
+ rows.append(row)
24
+ rows.append(metric('operational.failure',case['id'],'operational',float(trace['response']['status']!='ok'),numerator=int(trace['response']['status']!='ok'),denominator=1,direction='lower'))
25
+ rows.append(metric('operational.timeout',case['id'],'operational',float(trace['response']['status']=='timeout'),numerator=int(trace['response']['status']=='timeout'),denominator=1,direction='lower'))
26
+ return rows
27
+
28
+
29
+ def metric_key(row):
30
+ return row['stage']+'|'+row['metric_id']
31
+
32
+
33
+ def diagnose(case,trace,rows,review):
34
+ values={row['metric_id']:row['value'] for row in rows}
35
+ evidence={row['stage']:row['value'] for row in rows if row['metric_id']=='evidence.atom_recall'}
36
+ first_loss=None;prior_unknown=False
37
+ for stage in ('indexed','retrieved','reranked','selected','prompt'):
38
+ value=evidence.get(stage)
39
+ if value is None:
40
+ prior_unknown=True
41
+ elif value<1:
42
+ first_loss=stage;break
43
+ result={'first_observed_evidence_loss':first_loss,'earlier_stages_unknown':prior_unknown,
44
+ 'interpretation':'Observational localization over saved traces; no causal intervention was executed.'}
45
+ status=trace['response']['status']
46
+ source=case['references'].get('corpus_answerable')
47
+ sufficient=review.get('context_sufficiency',{}).get('prompt') if review else None
48
+ correct=values.get('generation.answer_correctness')
49
+ faithful=values.get('generation.faithfulness')
50
+ cited=values.get('citation.completeness')
51
+ citation_precision=values.get('citation.necessity_precision')
52
+ citation_failed=any(value is not None and value<1 for value in (cited,citation_precision))
53
+ if status!='ok':label='runtime_'+status
54
+ elif source is None:label='unknown_source_answerability'
55
+ elif source is False:
56
+ if trace['response']['abstained'] is True:
57
+ if values.get('outcome.grounded_task_success')==1:
58
+ label='appropriate_abstention'
59
+ elif not review or review.get('claims_exhaustive') is not True:
60
+ label='unknown_claim_support'
61
+ elif any(claim.get('supported_by_context') is False for claim in review['claims']):
62
+ label='unsupported_abstention'
63
+ elif any(claim.get('supported_by_context') is None for claim in review['claims']):
64
+ label='unknown_claim_support'
65
+ elif citation_failed:
66
+ label='citation_failure'
67
+ else:
68
+ label='incomplete_abstention_evidence' if values.get('outcome.grounded_task_success') is None else 'abstention_policy_failure'
69
+ else:
70
+ label='source_gap'
71
+ elif sufficient is None:label='unknown_context_sufficiency'
72
+ elif sufficient is False:
73
+ label='correct_with_insufficient_context' if correct==1 else 'insufficient_delivered_context'
74
+ elif correct is None:label='unknown_answer_correctness'
75
+ elif correct==0:label='incorrect_despite_sufficient_context'
76
+ elif values.get('generation.response_relevance')==0:label='answer_not_relevant'
77
+ elif faithful is None:label='unknown_claim_support'
78
+ elif faithful<1:label='unsupported_answer'
79
+ elif citation_failed:label='citation_failure'
80
+ elif values.get('outcome.grounded_task_success')==1:label='grounded_success'
81
+ else:label='incomplete_success_evidence'
82
+ result['outcome']=label
83
+ return result
84
+
85
+
86
+ def grade_case(case,variant,cutoff):
87
+ trace=case['traces'][variant]
88
+ review=case['references'].get('reviews',{}).get(variant)
89
+ rows=ranking_metrics(case,trace,cutoff)+evidence_metrics(case,trace)+semantic_metrics(case,trace,review)+operational_metrics(case,trace)
90
+ for row in rows:
91
+ row['label_provenance']=review.get('provenance') if row['grader_type']!='deterministic' and review else {'kind':'synthetic' if trace['execution_mode']=='synthetic_replay' else 'owner_supplied_reference'}
92
+ return {'case_id':case['id'],'suite_id':case['suite_id'],'cluster_id':case['cluster_id'],
93
+ 'variant':variant,'trace_id':trace['id'],'trace_digest':digest(trace),
94
+ 'execution_mode':trace['execution_mode'],'metrics':rows,
95
+ 'diagnosis':diagnose(case,trace,rows,review)}
96
+
97
+
98
+ def aggregate(rows):
99
+ grouped=defaultdict(list)
100
+ for case in rows:
101
+ for row in case['metrics']:grouped[metric_key(row)].append(row)
102
+ results={}
103
+ for key,records in grouped.items():
104
+ available=[r for r in records if r['value'] is not None]
105
+ excluded=[r for r in records if r['status']=='unavailable_zero_population']
106
+ missing=len(records)-len(available)-len(excluded)
107
+ observed=statistics.fmean(r['value'] for r in available) if available else None
108
+ sample=records[0]
109
+ results[key]={'metric_id':sample['metric_id'],'stage':sample['stage'],'direction':sample['direction'],'unit':sample['unit'],
110
+ 'value':observed if not missing else None,'observed_value':observed,
111
+ 'scored_cases':len(available),'eligible_cases':len(records)-len(excluded),
112
+ 'missing_cases':missing,'not_applicable_cases':len(excluded),'complete':not missing,
113
+ 'aggregation':'macro mean of case metrics; diagnostic cohort only',
114
+ 'reason_counts':dict(Counter(r['reason'] for r in records if r['reason']))}
115
+ return results
116
+
117
+
118
+ def _conditional(cases,variant,name,eligible,score):
119
+ n=0;total=0;missing=0;unknown_eligibility=0
120
+ for case in cases:
121
+ trace=case['traces'][variant];review=case['references'].get('reviews',{}).get(variant)
122
+ selected=eligible(case,trace,review)
123
+ if selected is None:unknown_eligibility+=1;continue
124
+ if not selected:continue
125
+ total+=1
126
+ value=score(case,trace,review)
127
+ if value is None:missing+=1
128
+ else:n+=value
129
+ observed_denominator=total-missing
130
+ observed=n/observed_denominator if observed_denominator else None
131
+ complete=not missing and not unknown_eligibility
132
+ return {'metric_id':name,'value':observed if complete else None,'observed_value':observed,
133
+ 'numerator':n,'denominator':total,'scored_cases':observed_denominator,
134
+ 'missing_cases':missing,'unknown_eligibility_cases':unknown_eligibility,
135
+ 'complete':complete,'status':'available' if complete and total else 'unavailable_incomplete_or_zero_population'}
136
+
137
+
138
+ def conditional_metrics(cases,variant):
139
+ def suff(review,stage='prompt'):
140
+ return review.get('context_sufficiency',{}).get(stage) if review else None
141
+ def correct(case,trace,review):
142
+ if not review or trace['response']['status']!='ok' or trace['response']['abstained'] is None:return None
143
+ return 0 if trace['response']['abstained'] is True else review.get('answer_correct')
144
+ def unsupported(case,trace,review):
145
+ if trace['response']['status']!='ok':return None
146
+ if not review or review.get('claims_exhaustive') is not True or trace['stages']['prompt'] is None:return None
147
+ claims=review.get('claims',[])
148
+ if not claims:return 0 if trace['response']['abstained'] is True else None
149
+ if any(c.get('supported_by_context') is False for c in claims):return 1
150
+ if any(c.get('supported_by_context') is None for c in claims):return None
151
+ return 0
152
+ return [
153
+ _conditional(cases,variant,'context.retrieval_sufficiency_recall',lambda c,t,r:c['references'].get('corpus_answerable'),lambda c,t,r:suff(r,'retrieved')),
154
+ _conditional(cases,variant,'generation.success_given_sufficient_context',lambda c,t,r:suff(r),correct),
155
+ _conditional(cases,variant,'generation.unsupported_answer_given_insufficient_context',lambda c,t,r:None if suff(r) is None else not suff(r),unsupported),
156
+ _conditional(cases,variant,'outcome.abstention_recall',lambda c,t,r:r.get('abstention_expected') if r else None,lambda c,t,r:t['response']['abstained'] if t['response']['status']=='ok' else None),
157
+ ]
158
+
159
+
160
+ def paired_comparisons(cases,left,right,samples=1000,seed=17,*,allow_bootstrap=True,interval_scope=None):
161
+ by_left={r['case_id']:r for r in left};by_right={r['case_id']:r for r in right}
162
+ a=aggregate(left);b=aggregate(right);results=[]
163
+ for key in a:
164
+ pairs=[];excluded=0
165
+ for case in cases:
166
+ ar=next(r for r in by_left[case['id']]['metrics'] if metric_key(r)==key)
167
+ br=next(r for r in by_right[case['id']]['metrics'] if metric_key(r)==key)
168
+ if ar['value'] is not None and br['value'] is not None:
169
+ pairs.append({'case_id':case['id'],'cluster_id':case['cluster_id'],
170
+ 'baseline':ar['value'],'candidate':br['value'],'delta':br['value']-ar['value']})
171
+ elif ar['status']=='unavailable_zero_population' and br['status']=='unavailable_zero_population':excluded+=1
172
+ clusters=defaultdict(list)
173
+ for pair in pairs:clusters[pair['cluster_id']].append(pair['delta'])
174
+ keys=sorted(clusters);rng=random.Random(seed);draws=[]
175
+ if allow_bootstrap and len(keys)>=2 and samples:
176
+ for _ in range(samples):
177
+ chosen=[d for group in rng.choices(keys,k=len(keys)) for d in clusters[group]]
178
+ draws.append(statistics.fmean(chosen))
179
+ draws.sort()
180
+ interval=[draws[int(.025*(len(draws)-1))],draws[int(.975*(len(draws)-1))]] if draws else None
181
+ missing_pairs=len(cases)-len(pairs)-excluded
182
+ observed_delta=statistics.fmean(p['delta'] for p in pairs) if pairs else None
183
+ task_outcome=a[key]['metric_id'] in ('outcome.task_success','outcome.grounded_task_success')
184
+ row={'key':key,**a[key],'baseline':a[key],'candidate':b[key],
185
+ 'paired_delta':observed_delta if not missing_pairs else None,
186
+ 'observed_subset_delta':observed_delta,
187
+ 'paired_cases':len(pairs),'excluded_zero_population_pairs':excluded,
188
+ 'missing_pairs':missing_pairs,'eligible_pairs':len(cases)-excluded,
189
+ 'paired_complete':not missing_pairs,'independent_clusters':len(keys),
190
+ 'development_cluster_bootstrap_95':interval if not missing_pairs else None,
191
+ 'observed_subset_bootstrap_95':interval,'bootstrap_samples':samples if allow_bootstrap else 0,
192
+ 'requested_bootstrap_samples':samples,'seed':seed,'bootstrap_allowed':allow_bootstrap,
193
+ 'interval_scope':interval_scope or 'Synthetic diagnostic illustration; not population performance evidence. Primary paired claims require all eligible pairs.',
194
+ 'observed_subset_scope':'Only complete pairs are included; missing eligible pairs can bias this diagnostic subset.',
195
+ 'improved_cases':[p['case_id'] for p in pairs if p['delta']*(1 if a[key]['direction']=='higher' else -1)>0],
196
+ 'worsened_cases':[p['case_id'] for p in pairs if p['delta']*(1 if a[key]['direction']=='higher' else -1)<0],
197
+ 'task_rescued_cases':[p['case_id'] for p in pairs if task_outcome and p['baseline']==0 and p['candidate']==1],
198
+ 'task_regressed_cases':[p['case_id'] for p in pairs if task_outcome and p['baseline']==1 and p['candidate']==0]}
199
+ results.append(row)
200
+ return results
201
+
202
+
203
+ def evaluate(data,cutoff=3,bootstrap_samples=500):
204
+ validate_dataset(data)
205
+ if type(bootstrap_samples) is not int or not 0<=bootstrap_samples<=5000:raise ValueError('bootstrap_samples must be 0–5000')
206
+ cases=data['cases'];baseline=[grade_case(c,'baseline',cutoff) for c in cases];candidate=[grade_case(c,'candidate',cutoff) for c in cases]
207
+ all_rows=baseline+candidate
208
+ modes={trace['execution_mode'] for case in cases for trace in case['traces'].values()}
209
+ all_synthetic=modes=={'synthetic_replay'}
210
+ provenance=data.get('provenance') if type(data.get('provenance')) is dict else {}
211
+ independence_declared=provenance.get('independent_clusters_verified') is True
212
+ allow_bootstrap=all_synthetic or independence_declared
213
+ if all_synthetic:
214
+ cohort='synthetic diagnostic cases; not representative production traffic'
215
+ interval_scope='Synthetic diagnostic illustration; not population performance evidence. Primary paired claims require all eligible pairs.'
216
+ independence_basis='Synthetic fixture clusters; arithmetic illustration only.'
217
+ elif not allow_bootstrap:
218
+ cohort='mixed synthetic and recorded replay diagnostics' if len(modes)>1 else 'recorded replay diagnostics; representativeness not verified'
219
+ interval_scope='Cluster independence is unknown for recorded traces; no bootstrap interval is computed.'
220
+ independence_basis='No explicit independent-cluster declaration; inference disabled.'
221
+ else:
222
+ cohort='mixed synthetic and recorded replay diagnostics' if len(modes)>1 else 'recorded replay diagnostics; representativeness not verified'
223
+ interval_scope='Independent clusters are caller asserted, not verified. Development diagnostic interval only; not validated population performance evidence. Primary paired claims require all eligible pairs.'
224
+ independence_basis='Caller asserted independent_clusters_verified=true; the prototype does not verify this assertion.'
225
+ return {'schema_version':'sphynx.rag-diagnostics.report.v1','metric_version':'sphynx.rag-diagnostics.metric.v1',
226
+ 'generated_at':datetime.now(timezone.utc).isoformat(),'dataset_release':data['release'],
227
+ 'dataset_digest':digest(data),'source':'local saved-trace grading; no candidate execution',
228
+ 'cohort':cohort,'execution_modes':sorted(modes),
229
+ 'bootstrap_independence':{'allowed':allow_bootstrap,'basis':independence_basis,'performance_inference_validated':False},
230
+ 'cutoff':cutoff,'case_count':len(cases),'suite_count':len(data['suites']),
231
+ 'metric_definition_count':len({r['metric_id'] for c in all_rows for r in c['metrics']}),
232
+ 'stage_metric_view_count':len(aggregate(baseline)),
233
+ 'suites':data['suites'],'cases':cases,'case_results':all_rows,
234
+ 'aggregates':{'baseline':aggregate(baseline),'candidate':aggregate(candidate)},
235
+ 'conditional':{v:conditional_metrics(cases,v) for v in ('baseline','candidate')},
236
+ 'comparisons':paired_comparisons(cases,baseline,candidate,bootstrap_samples,
237
+ allow_bootstrap=allow_bootstrap,interval_scope=interval_scope),
238
+ 'diagnosis_counts':{v:dict(Counter(r['diagnosis']['outcome'] for r in (baseline if v=='baseline' else candidate))) for v in ('baseline','candidate')},
239
+ 'limitations':['Semantic judgments are supplied synthetic/adjudicated labels, not automated judges.',
240
+ 'No imported public benchmark or customer performance claim follows from these fixtures.',
241
+ 'Unjudged documents remain unknown; known-qrel ranking uses zero gain without removing their ranks.',
242
+ 'First observed evidence loss is descriptive, not proof of causality.',
243
+ 'Recorded telemetry is separate from replay grading time; no verified cost accounting.']}