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,288 @@
1
+ """Paper-specific semantic arithmetic over exact-input captured judgments.
2
+
3
+ Model extraction and entailment are performed by the separate judge execution
4
+ module. These functions never call a model or turn model labels into human gold.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import math
9
+ import re
10
+
11
+ from .contracts import require
12
+ from .scoring import labels_for, measure, normalize_answer, ratio, result
13
+
14
+
15
+ def booleans(values):
16
+ require(type(values) is list and all(type(v) is bool for v in values), 'Expected resolved boolean judgments')
17
+ return values
18
+
19
+
20
+ def evaluate_sufficient_context(case, trace, corpus, *, judgments=()):
21
+ """Sufficiency and conditional generation outcomes; no qrel substitution."""
22
+ labels = labels_for(judgments, case, trace, 'sufficient_context')
23
+ sufficient = None if labels is None else labels.get('sufficient')
24
+ correct = None if labels is None else labels.get('answer_correct')
25
+ require(sufficient is None or type(sufficient) is bool, 'Invalid sufficiency verdict')
26
+ require(correct is None or type(correct) is bool, 'Invalid correctness verdict')
27
+ source = case['references']['answerable']
28
+ metrics = {'context_sufficient': measure(None if sufficient is None else float(sufficient)),
29
+ 'retrieval_sufficiency_recall': measure(None if source is not True or sufficient is None else float(sufficient),
30
+ denominator=1 if source is True else None, reason='Conditioned on independently source-answerable cases'),
31
+ 'success_given_sufficient_context': measure(None if sufficient is not True or correct is None else
32
+ float(correct and trace['response']['status'] == 'ok' and not trace['response']['abstained']),
33
+ denominator=1 if sufficient is True else None),
34
+ 'incorrect_response_given_insufficient_context': measure(None if sufficient is not False or correct is None else
35
+ float(not correct and not trace['response']['abstained']), denominator=1 if sufficient is False else None)}
36
+ return result('sufficient_context', case, trace, corpus, metrics, judgments=judgments,
37
+ notes=['Sufficiency is a question/context judgment, not proof of correct generation.',
38
+ 'Conditional exclusions and missing judgments must be aggregated explicitly.'])
39
+
40
+
41
+ def evaluate_ragas(case, trace, corpus, *, judgments=()):
42
+ """RAGAs claim support, reference-claim recall and ranked context precision.
43
+
44
+ Response relevancy consumes retained reverse-question embedding vectors;
45
+ no embedding similarity is fabricated from text overlap.
46
+ """
47
+ labels = labels_for(judgments, case, trace, 'ragas')
48
+ metrics = {}
49
+ for key, name in [('claim_supported', 'faithfulness'), ('reference_claim_supported', 'context_recall')]:
50
+ if labels is None or labels.get(key) is None:
51
+ metrics[name] = measure(reason='Required claim entailment judgments unavailable')
52
+ else:
53
+ values = booleans(labels[key]); metrics[name] = ratio(sum(values), len(values))
54
+ relevant = None if labels is None else labels.get('context_relevant')
55
+ if relevant is None:
56
+ metrics['context_precision'] = measure(reason='Ranked context relevance judgments unavailable')
57
+ else:
58
+ values = booleans(relevant)
59
+ require(trace['stages']['prompt'] is not None and len(values) == len(trace['stages']['prompt']),
60
+ 'Context judgments do not cover the exact ranked prompt')
61
+ found = 0; precision_sum = 0.0
62
+ for rank, value in enumerate(values, 1):
63
+ found += value
64
+ if value:
65
+ precision_sum += found/rank
66
+ # RAGAs uses an epsilon denominator, yielding zero when no contexts are
67
+ # relevant. Record that convention instead of changing it to missing.
68
+ metrics['context_precision'] = measure(precision_sum/found if found else 0.0,
69
+ numerator=precision_sum, denominator=found)
70
+ vectors = None if labels is None else labels.get('question_vectors')
71
+ query = None if labels is None else labels.get('query_vector')
72
+ if vectors is None or query is None:
73
+ metrics['response_relevancy'] = measure(reason='Reverse-question embeddings unavailable')
74
+ else:
75
+ require(type(query) is list and query and type(vectors) is list and vectors, 'Invalid embedding population')
76
+ sims = []
77
+ for vector in vectors:
78
+ require(type(vector) is list and len(vector) == len(query)
79
+ and all(type(x) in (int, float) and math.isfinite(x) for x in query+vector),
80
+ 'Invalid retained embedding vector')
81
+ norms = math.sqrt(sum(x*x for x in query)*sum(x*x for x in vector))
82
+ require(norms > 0, 'Zero-norm embedding has no cosine similarity')
83
+ sims.append(sum(x*y for x, y in zip(query, vector))/norms)
84
+ noncommittal = labels.get('noncommittal')
85
+ require(type(noncommittal) is bool, 'Reverse-question noncommittal judgment required')
86
+ metrics['response_relevancy'] = measure(0.0 if noncommittal else sum(sims)/len(sims), unit='cosine')
87
+ return result('ragas', case, trace, corpus, metrics, judgments=judgments,
88
+ notes=['Named Sphynx protocol with explicit retained claim/context/vector inputs; not all metrics in the evolving Ragas package.'])
89
+
90
+
91
+ def evaluate_alce(case, trace, corpus, *, judgments=(), citation_cap=3):
92
+ """ALCE joint citation entailment and useful/necessary citation precision.
93
+
94
+ Sentence segmentation is a retained input to the judgment. The complete
95
+ ordered sentence texts must reconstruct the response modulo whitespace.
96
+ NLI labels bind joint, individual and leave-one-out cited document sets.
97
+ """
98
+ require(type(citation_cap) is int and citation_cap >= 1, 'Invalid citation cap')
99
+ labels = labels_for(judgments, case, trace, 'alce')
100
+ metrics = {}; rows = None if labels is None else labels.get('sentences')
101
+ if rows is None:
102
+ metrics = {name: measure(reason='Sentence-bound NLI judgments unavailable')
103
+ for name in ('citation_recall', 'citation_precision')}
104
+ else:
105
+ require(labels.get('citation_cap',3)==citation_cap,'ALCE citation-cap judgment differs from scoring policy')
106
+ require(type(rows) is list and all(type(r) is dict and type(r.get('text')) is str for r in rows),
107
+ 'Invalid ALCE sentence population')
108
+ require(' '.join(' '.join(r['text'] for r in rows).split()) == ' '.join(trace['response']['text'].split()),
109
+ 'ALCE sentences do not cover the complete response')
110
+ prompt = trace['stages']['prompt']
111
+ require(prompt is not None, 'Delivered context is required for ALCE')
112
+ supported = useful = citations = 0
113
+ for row in rows:
114
+ cited = row.get('cited_ids')
115
+ require(type(cited) is list and all(type(d) is str for d in cited), 'Invalid citation list')
116
+ numbers = [int(n) for n in re.findall(r'\[(\d+)\]', row['text'])]
117
+ expected = [prompt[n-1] if 1 <= n <= len(prompt) else 'invalid:'+str(n) for n in numbers]
118
+ require(cited == expected, 'Citations differ from emitted numeric references')
119
+ # Unlike the old diagnostics contract, repeated emitted references
120
+ # retain their occurrence count in the native ALCE denominator.
121
+ valid = bool(cited) and all(d in prompt for d in cited)
122
+ if not valid:
123
+ continue
124
+ cited = cited[:citation_cap]; citations += len(cited)
125
+ joint = row.get('joint_support')
126
+ require(type(joint) is bool, 'Joint citation entailment unresolved')
127
+ supported += joint
128
+ if not joint:
129
+ continue
130
+ if len(cited) == 1:
131
+ useful += 1; continue
132
+ alone, without = row.get('individual_support'), row.get('leave_one_out_support')
133
+ require(type(alone) is dict and type(without) is dict, 'Citation ablations unavailable')
134
+ for did in cited:
135
+ require(type(alone.get(did)) is bool and type(without.get(did)) is bool,
136
+ 'Citation ablation judgment unresolved')
137
+ useful += alone[did] or not without[did]
138
+ metrics['citation_recall'] = ratio(supported, len(rows))
139
+ metrics['citation_precision'] = measure(useful/citations if citations else 0.0,
140
+ numerator=useful, denominator=citations, reason='ALCE zero-citation convention' if not citations else None)
141
+ native = case['references']['native']
142
+ groups = native.get('asqa_short_answers')
143
+ if groups is not None:
144
+ text = normalize_answer(re.sub(r'\[\d+\]', '', trace['response']['text']))
145
+ require(type(groups) is list and groups and all(type(g) is list and g for g in groups), 'Invalid ASQA answer groups')
146
+ hits = [any(normalize_answer(a) in text for a in aliases) for aliases in groups]
147
+ metrics['asqa_str_em'] = ratio(sum(hits), len(hits))
148
+ metrics['asqa_str_em_hit'] = measure(float(all(hits)))
149
+ groups = native.get('qampari_answers')
150
+ if groups is not None:
151
+ require(type(groups) is list and groups and all(type(g) is list and g for g in groups), 'Invalid QAMPARI answers')
152
+ text = re.sub(r'\[\d+\]', '', trace['response']['text']).rstrip().rstrip('.').rstrip(',')
153
+ predictions = [normalize_answer(p.strip()) for p in text.split(',')]
154
+ predictions = [p for p in predictions if p]
155
+ gold = [[normalize_answer(a) for a in aliases] for aliases in groups]
156
+ precision = sum(p in {a for aliases in gold for a in aliases} for p in predictions)/len(predictions) if predictions else 0.0
157
+ recovered = sum(any(a in predictions for a in aliases) for aliases in gold)
158
+ recall = recovered/len(gold); top5 = min(5, recovered)/min(5, len(gold))
159
+ metrics.update(qampari_precision=measure(precision), qampari_recall=measure(recall),
160
+ qampari_recall_top5=measure(top5), qampari_f1=measure(2*precision*recall/(precision+recall) if precision+recall else 0.0),
161
+ qampari_f1_top5=measure(2*precision*top5/(precision+top5) if precision+top5 else 0.0))
162
+ return result('alce', case, trace, corpus, metrics, judgments=judgments,parameters=dict(citation_cap=citation_cap),
163
+ notes=['Citation cap is explicit; sentence/NLI model identity is in the retained judgment.',
164
+ 'ROUGE, MAUVE and QA-model auxiliary metrics are not silently approximated.'])
165
+
166
+
167
+ def evaluate_factscore(case, trace, corpus, *, judgments=(), gamma=10):
168
+ """FActScore atomic precision and exp(1-gamma/n) short-response penalty."""
169
+ require(type(gamma) in (int, float) and math.isfinite(gamma) and gamma >= 0, 'Invalid FActScore gamma')
170
+ labels = labels_for(judgments, case, trace, 'factscore')
171
+ supported = None if labels is None else labels.get('fact_supported')
172
+ metrics = {key:measure(reason='Atomic facts and knowledge-source verification unavailable')
173
+ for key in ('atomic_precision','factscore','response_eligible','length_penalty','fact_count')}
174
+ if supported is None:
175
+ pass
176
+ else:
177
+ supported = booleans(supported)
178
+ if not supported:
179
+ metrics.update(atomic_precision=measure(reason='No extracted facts'),
180
+ factscore=measure(reason='No extracted facts'), response_eligible=measure(0.0),
181
+ fact_count=measure(0,unit='facts'))
182
+ else:
183
+ count = len(supported); raw = sum(supported)/count
184
+ penalty = 1.0 if not gamma or count >= gamma else math.exp(1-gamma/count)
185
+ metrics = {'atomic_precision': ratio(sum(supported), count),
186
+ 'factscore': measure(raw*penalty), 'response_eligible': measure(1.0),
187
+ 'length_penalty': measure(penalty), 'fact_count': measure(count, unit='facts')}
188
+ return result('factscore', case, trace, corpus, metrics, judgments=judgments,parameters=dict(gamma=gamma),
189
+ notes=['Knowledge-source verification is distinct from support in the delivered RAG context.',
190
+ 'Facts/extraction and knowledge source must be captured by the judge workflow.'])
191
+
192
+
193
+ def evaluate_ragchecker(case, trace, corpus, *, judgments=()):
194
+ """RAGChecker's eleven diagnostic rates from captured entailment matrices."""
195
+ labels = labels_for(judgments, case, trace, 'ragchecker')
196
+ names = ('precision','recall','f1','claim_recall','context_precision','context_utilization',
197
+ 'noise_sensitivity_in_relevant','noise_sensitivity_in_irrelevant','hallucination','self_knowledge','faithfulness')
198
+ if labels is None:
199
+ metrics = {name: measure(reason='Claim entailment matrices unavailable') for name in names}
200
+ else:
201
+ a2r = None if labels.get('answer2response') is None else booleans(labels['answer2response'])
202
+ r2a = None if labels.get('response2answer') is None else booleans(labels['response2answer'])
203
+ d2a, d2r = labels.get('retrieved2answer'), labels.get('retrieved2response')
204
+ prompt = trace['stages']['prompt']
205
+ metrics={name:measure(reason='Required directional entailment inputs unavailable') for name in names}
206
+ def known_ratio(n,d):
207
+ return measure(n/d if d else 0.0,numerator=n,denominator=d,
208
+ reason='RAGChecker resolved-empty zero convention' if not d else None)
209
+ for values,name in [(a2r,'precision'),(r2a,'recall')]:
210
+ if values is not None:metrics[name]=known_ratio(sum(values),len(values))
211
+ if a2r is not None and r2a is not None:
212
+ p,r=metrics['precision']['value'],metrics['recall']['value']
213
+ metrics['f1']=measure(2*p*r/(p+r) if p+r else 0.0)
214
+ def matrix(value,known_claims,claim_texts):
215
+ if value is None:return None
216
+ require(type(value) is list and prompt is not None and len(value)==len(prompt),'Context matrices differ from delivered context')
217
+ count=len(known_claims) if known_claims is not None else len(claim_texts) if claim_texts is not None else len(value[0]) if value else 0
218
+ for row in value:require(len(booleans(row))==count,'Entailment matrix shape differs')
219
+ return [any(row[i] for row in value) for i in range(count)]
220
+ retrieved=matrix(d2a,r2a,labels.get('reference_claims'))
221
+ grounded=matrix(d2r,a2r,labels.get('response_claims'))
222
+ relevant=None if d2a is None else [any(row) for row in d2a]
223
+ if retrieved is not None:
224
+ metrics['claim_recall']=known_ratio(sum(retrieved),len(retrieved))
225
+ metrics['context_precision']=known_ratio(sum(relevant),len(relevant))
226
+ if r2a is not None:
227
+ metrics['context_utilization']=known_ratio(sum(x and y for x,y in zip(retrieved,r2a)),sum(retrieved))
228
+ if grounded is not None:metrics['faithfulness']=known_ratio(sum(grounded),len(grounded))
229
+ if grounded is not None and a2r is not None:
230
+ metrics['hallucination']=known_ratio(sum(not c and not s for c,s in zip(a2r,grounded)),len(a2r))
231
+ metrics['self_knowledge']=known_ratio(sum(c and not s for c,s in zip(a2r,grounded)),len(a2r))
232
+ if relevant is not None and d2r is not None and a2r is not None:
233
+ relevant_support=[any(row[i] for row,flag in zip(d2r,relevant) if flag) for i in range(len(a2r))]
234
+ irrelevant_support=[not relevant_support[i] and any(row[i] for row,flag in zip(d2r,relevant) if not flag) for i in range(len(a2r))]
235
+ for supports,name in [(relevant_support,'noise_sensitivity_in_relevant'),(irrelevant_support,'noise_sensitivity_in_irrelevant')]:
236
+ metrics[name]=known_ratio(sum(not c and s for c,s in zip(a2r,supports)),len(a2r))
237
+ return result('ragchecker', case, trace, corpus, metrics, judgments=judgments,
238
+ notes=['Claims, chunk boundaries and directional entailments are retained inputs, not inferred from token overlap.'])
239
+
240
+
241
+ def evaluate_ares(predictions, audit, *, declarations=None, confidence=0.95):
242
+ """ARES-style held human calibration and explicitly gated PPI arithmetic.
243
+
244
+ Training/inference belong to the captured judge adapter. This entry point
245
+ deliberately retains the independent-sample estimator's unvalidated-coverage
246
+ status; benchmark arithmetic does not establish Sable population coverage.
247
+ """
248
+ from ..rag_diagnostics import estimate_rag_ppi_mean
249
+ from .._rag_diagnostics_calibration import judge_audit
250
+ from .registry import paper_spec
251
+ estimate = estimate_rag_ppi_mean(predictions, audit, declarations=declarations, confidence=confidence)
252
+ audit_rows = [dict(id=r['id'], human_label=r.get('human_label'), judge_label=(r['prediction'] >= 0.5)
253
+ if type(r.get('prediction')) in (int,float) and math.isfinite(r['prediction'])
254
+ and 0<=r['prediction']<=1 else None) for r in audit]
255
+ return dict(protocol=paper_spec('ares'), estimate=estimate,
256
+ audit=judge_audit(audit_rows, provenance_kind='caller-supplied-independent-audit'),
257
+ training_performed=False, validated_population_coverage=False)
258
+
259
+
260
+ def evaluate_trec_rag_2025(case, trace, corpus, *, judgments=(), cutoff=10):
261
+ """TREC RAG retrieval and nugget/attribution dimensions, never one score."""
262
+ from .scoring import evaluate_beir
263
+ metrics = {'retrieval.'+k:v for k,v in evaluate_beir(case,trace,corpus,cutoff=cutoff)['metrics'].items()}
264
+ labels = labels_for(judgments, case, trace, 'trec_rag_2025')
265
+ for field, metric in [('nuggets_covered','nugget_coverage'),('sentences_supported','sentence_attribution')]:
266
+ values = None if labels is None else labels.get(field)
267
+ native = case['references']['native'].get('nuggets') if field=='nuggets_covered' else trace['native'].get('sentence_citations')
268
+ if values is None or native is None:
269
+ metrics[metric]=measure(reason='Complete native annotation units and their judgments are required')
270
+ else:
271
+ require(type(native) is list and len(booleans(values))==len(native),'TREC judgment census differs from native units')
272
+ metrics[metric]=ratio(sum(values),len(values))
273
+ return result('trec_rag_2025', case, trace, corpus, metrics, judgments=judgments,parameters=dict(cutoff=cutoff),
274
+ notes=['Native narrative/nugget annotations stay protected. This is not a TREC submission or a claim of official leaderboard equivalence.'])
275
+
276
+
277
+ def evaluate_lost_in_the_middle(case, trace, corpus, *, judgments=()):
278
+ """Score one frozen context-position trial; sweep orchestration is separate."""
279
+ from .scoring import answer_scores
280
+ refs = case['references']['answers']
281
+ metrics = {'answer_accuracy': measure(reason='Reference answer aliases unavailable')}
282
+ if refs is not None:
283
+ text = normalize_answer(trace['response']['text'])
284
+ metrics['answer_accuracy'] = measure(float(trace['response']['status']=='ok'
285
+ and any(normalize_answer(a) in text for a in refs)))
286
+ return result('lost_in_the_middle', case, trace, corpus, metrics,
287
+ notes=['Normalized answer-containing accuracy follows the QA sweep boundary; key-value retrieval is a distinct experiment.',
288
+ 'Position, token budget, trial order and source content are bound by the sweep plan.'])
@@ -0,0 +1,80 @@
1
+ """TREC 2025 RAG native topic/segment and sentence-citation boundaries.
2
+
3
+ See the organizer's Aug 13 2025 v2 guidelines. These format adapters do not
4
+ reproduce AutoNuggetizer's model prompts or claim official leaderboard scores.
5
+ """
6
+ from __future__ import annotations
7
+ import copy
8
+
9
+ from .contracts import digest, make_case, make_trace, parse, require, validate_trace
10
+ from .datasets import _document, _read, _write_bundle
11
+
12
+
13
+ def prepare_trec_rag_2025_dataset(topics_path, segments_path, output_dir, *,
14
+ topics_sha256, segments_sha256, qrels_path=None, qrels_sha256=None,
15
+ annotations=None, collection_scope):
16
+ """Load organizer id/title topic JSONL and docid/segment corpus JSONL.
17
+
18
+ Pin an explicit local corpus/shard. The 120M+ collection is never fetched
19
+ implicitly or represented as a complete collection when only a shard exists.
20
+ Supplied annotations are a protected id->native-object mapping, e.g. nuggets.
21
+ """
22
+ require(type(collection_scope) is str and collection_scope.strip(),'Declare the exact local collection scope')
23
+ topics=[parse(l) for l in _read(topics_path,topics_sha256).splitlines() if l.strip()]
24
+ segments=[parse(l) for l in _read(segments_path,segments_sha256).splitlines() if l.strip()]
25
+ corpus={}
26
+ for row in segments:
27
+ require(row['docid'] not in corpus,'Duplicate native segment ID')
28
+ _document(corpus,row['segment'],title=row['title'],key=row['docid'])
29
+ qrels={}
30
+ require((qrels_path is None)==(qrels_sha256 is None),'Qrels path and pin must be supplied together')
31
+ if qrels_path is not None:
32
+ for line in _read(qrels_path,qrels_sha256).decode('utf-8').splitlines():
33
+ if not line.strip():continue
34
+ q,iteration,d,g=line.split()
35
+ require(d in corpus and d not in qrels.setdefault(q,{}),'Qrel is duplicate or outside the declared collection')
36
+ qrels[q][d]=int(g)
37
+ annotations=annotations or {};require(type(annotations) is dict,'Native annotations must be keyed by topic ID')
38
+ seen=set();cases=[]
39
+ for row in topics:
40
+ qid=row['id'];require(qid not in seen,'Duplicate narrative ID');seen.add(qid)
41
+ cases.append(make_case(qid,row['title'],list(corpus),origin='public',snapshot=segments_sha256,
42
+ family='trec_rag_2025',source_id='trec_rag_2025',split='test',
43
+ references=dict(qrels=qrels.get(qid),native=dict(topic=row,collection_scope=collection_scope,
44
+ **copy.deepcopy(annotations.get(qid,{}))))))
45
+ require(set(qrels)<=seen and set(annotations)<=seen,'Native annotations reference absent topics')
46
+ return _write_bundle(corpus,cases,output_dir,dict(kind='trec-rag-2025-native',topics_sha256=topics_sha256,
47
+ segments_sha256=segments_sha256,qrels_sha256=qrels_sha256,annotations_sha256=digest(annotations),
48
+ collection_scope=collection_scope,official_autonuggetizer_executed=False))
49
+
50
+
51
+ def trace_from_trec_rag_2025(case, corpus, record, *, candidate_id, source_sha256):
52
+ """Read native narrative/references/answer rows, preserving citation units.
53
+
54
+ Numeric citations are zero-based indices into references; string citations
55
+ are native segment IDs. The original output is retained unchanged.
56
+ """
57
+ require(record['metadata']['narrative_id']==case['id'] and record['metadata']['narrative']==case['question'],
58
+ 'Native submission narrative differs')
59
+ references=record['references']
60
+ require(type(references) is list and len(references)<=100 and len(set(references))==len(references)
61
+ and set(references)<=set(case['document_ids']),'Invalid TREC reference inventory')
62
+ rows=record['answer'];require(type(rows) is list,'Native sentence array required')
63
+ units=[]
64
+ for row in rows:
65
+ require(type(row) is dict and set(row)=={'text','citations'} and type(row['text']) is str
66
+ and type(row['citations']) is list,'Invalid native TREC sentence')
67
+ ids=[]
68
+ for cite in row['citations']:
69
+ if type(cite) is int:
70
+ require(0<=cite<len(references),'Citation index outside references');did=references[cite]
71
+ else:
72
+ require(type(cite) is str and cite in references,'Citation segment outside references');did=cite
73
+ ids.append(did)
74
+ units.append(dict(text=row['text'],cited_ids=ids))
75
+ text=' '.join(r['text'] for r in units)
76
+ require(len(text.split())<=400,'Native response exceeds 400-word boundary')
77
+ trace=make_trace(case,corpus,text=text,ranked=references,prompt=references,
78
+ candidate_id=candidate_id,source_digest=source_sha256)
79
+ trace['native']=dict(trec_rag_2025=copy.deepcopy(record),sentence_citations=units)
80
+ return validate_trace(trace,case,corpus)