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/_pr_client.py ADDED
@@ -0,0 +1,237 @@
1
+ """Durable PR operation handles over the existing authenticated transport."""
2
+ from __future__ import annotations
3
+ from collections.abc import Callable
4
+ from typing import Any, TYPE_CHECKING
5
+ from urllib.parse import quote
6
+ import math
7
+ import time
8
+
9
+ from .client import HostedClientError, _retry_key
10
+ from .pr_evaluations import PrEvaluationRequest, PrEvaluationStatus, PrEvaluationResult, _saved
11
+ from ._analytical_values import identifier, require, MetricProjectionSummary, ReportProjectionSummary, ComparisonProjectionSummary
12
+ from .analytical_views import ProjectionPage
13
+
14
+ if TYPE_CHECKING:
15
+ from .client import Client
16
+ from .diagnostics import Diagnostic
17
+ from .progress import ProgressView
18
+ from .pr_evaluations import PrCaseChangePage
19
+
20
+ _READ_RETRY={'hosted_transport_failed','hosted_request_failed','service_unavailable','store_unavailable',
21
+ 'projection_unavailable','pr_evaluation_conflict','result_not_ready','evaluation_projection_pending'}
22
+ _PARSE_ERRORS=(ValueError,TypeError,KeyError,AttributeError,RecursionError,UnicodeError)
23
+
24
+
25
+ class PrEvaluationError(HostedClientError):
26
+ """Keep the original request/key or known handle after an uncertain response."""
27
+ def __init__(self,cause:HostedClientError,*,pr_evaluation_id:str|None=None,request:PrEvaluationRequest|None=None,
28
+ idempotency_key:str|None=None,handle:PrEvaluation|None=None,submission:bool=False,operation_failed:bool=False):
29
+ self.pr_evaluation_id,self.request,self.idempotency_key=pr_evaluation_id,request,idempotency_key
30
+ self._submission=submission
31
+ self._operation_failed=operation_failed
32
+ message=('PR submission needs reconciliation; retain the original request and key' if submission
33
+ else 'PR evaluation failed; read result() on the original handle to inspect retained coverage' if operation_failed
34
+ else 'PR evaluation read did not complete; retain the original handle and inspect its status')
35
+ super().__init__(cause.code,message,handle=handle)
36
+ @property
37
+ def diagnostic(self)->Diagnostic:
38
+ from .diagnostics import Diagnostic
39
+ recovery={} if self.pr_evaluation_id is None else {'record_id':self.pr_evaluation_id}
40
+ if self._operation_failed:
41
+ return Diagnostic(self.code,'The PR operation failed; its permitted results remain readable.',
42
+ ('Read result() on the same handle to inspect coverage and errors before choosing a new operation.',),
43
+ retryability='fix_required',recovery=recovery)
44
+ if self._submission:
45
+ return Diagnostic(self.code,'The PR submission outcome needs reconciliation.',
46
+ ('Replay the original immutable request with the original idempotency key; retain any returned handle.',),
47
+ retryability='reconcile_before_retry',recovery=recovery)
48
+ if self.code in _READ_RETRY or self.code=='wait_timeout':
49
+ return Diagnostic(self.code,'Read the original PR evaluation handle again.',
50
+ ('Resume waiting or read result() on the same handle; use get_pr_evaluation() after restarting.',),
51
+ retryability='read_original_handle',recovery=recovery)
52
+ return Diagnostic(self.code,'The selected PR evaluation needs an access or input check.',
53
+ ('Inspect the retained handle; have the operator check current access and private profile admission.',),
54
+ retryability='fix_required',recovery=recovery)
55
+
56
+
57
+ def _invalid()->HostedClientError:
58
+ return HostedClientError('invalid_server_response','the PR operation response could not be verified')
59
+
60
+
61
+ def _path(identity:str)->str:return '/v1/pr-evaluations/'+_saved(identity,'preval')
62
+
63
+
64
+ def _budget(deadline:float|None)->None:
65
+ if deadline is not None and time.monotonic()>=deadline:raise HostedClientError('wait_timeout','the read budget expired')
66
+
67
+
68
+ def _bound(previous:PrEvaluationStatus,fresh:PrEvaluationStatus)->None:
69
+ require((fresh.id,fresh.request,fresh.template,fresh.expected_children,fresh.created_at,fresh.deadline_at)==
70
+ (previous.id,previous.request,previous.template,previous.expected_children,previous.created_at,previous.deadline_at))
71
+ def roster(status):
72
+ return {r.requirement_id:(r.suite_id,r.scope,r.mode,r.bank_id,r.release_id,r.cohort) for r in status.requirements}
73
+ require(roster(previous)==roster(fresh))
74
+ if previous.revisions is not None:require((previous.revisions,previous.plan_digest)==(fresh.revisions,fresh.plan_digest))
75
+ if previous.decision_id is not None:require(fresh.decision_id==previous.decision_id)
76
+ if previous.status in ('completed','failed'):require(previous.status==fresh.status)
77
+
78
+
79
+ def _status(client:Client,identity:str,*,handle:PrEvaluation|None=None,deadline:float|None=None)->PrEvaluationStatus:
80
+ try:
81
+ try:
82
+ _budget(deadline)
83
+ options={} if deadline is None else {'deadline':deadline}
84
+ status=PrEvaluationStatus.from_dict(client._request('GET',_path(identity),**options))
85
+ require(status.id==identity)
86
+ if handle is not None:_bound(handle._status,status)
87
+ return status
88
+ except _PARSE_ERRORS:raise _invalid() from None
89
+ except HostedClientError as error:
90
+ raise PrEvaluationError(error,pr_evaluation_id=identity,handle=handle) from None
91
+
92
+
93
+ def submit(client:Client,request:PrEvaluationRequest,*,idempotency_key:str|None=None)->PrEvaluation:
94
+ require(isinstance(request,PrEvaluationRequest),'submit a typed PrEvaluationRequest')
95
+ original=PrEvaluationRequest.from_dict(request.to_dict());key=_retry_key(idempotency_key)
96
+ body={'schema_version':'sphynx.pr-evaluation-submission.v1','request':original.to_dict(),'idempotency_key':key}
97
+ try:
98
+ raw=client._request('POST','/v1/pr-evaluations',body,retry_key=key)
99
+ try:
100
+ status=PrEvaluationStatus.from_dict(raw);require(status.request==original)
101
+ except _PARSE_ERRORS:raise _invalid() from None
102
+ return PrEvaluation(client,status,idempotency_key=key)
103
+ except HostedClientError as error:
104
+ raise PrEvaluationError(error,request=original,idempotency_key=key,submission=True) from None
105
+
106
+
107
+ def get(client:Client,identity:str)->PrEvaluation:
108
+ _saved(identity,'preval');return PrEvaluation(client,_status(client,identity))
109
+
110
+
111
+ class PrEvaluation:
112
+ """One durable private PR operation. Polling never submits candidate work."""
113
+ def __init__(self,client:Client,status:PrEvaluationStatus,*,idempotency_key:str|None=None):
114
+ require(isinstance(status,PrEvaluationStatus))
115
+ self._client,self._status,self.id,self.idempotency_key=client,status,status.id,idempotency_key
116
+ self._last_result:PrEvaluationResult|None=None
117
+ @property
118
+ def status(self)->str:return self._status.status
119
+ @property
120
+ def request(self)->PrEvaluationRequest:return self._status.request
121
+ @property
122
+ def observation(self)->PrEvaluationStatus:return self._status
123
+ @property
124
+ def progress(self)->ProgressView:
125
+ from .progress import _progress
126
+ return _progress('submission',self.status,{'submission_id':self.id})
127
+ def refresh(self)->PrEvaluation:
128
+ return self._refresh()
129
+ def _refresh(self,deadline:float|None=None)->PrEvaluation:
130
+ fresh=_status(self._client,self.id,handle=self,deadline=deadline)
131
+ self._status=fresh;return self
132
+ def result(self)->PrEvaluationResult:
133
+ """Read finished reports or terminal coverage without executing work."""
134
+ return self._result()
135
+ def _result(self,deadline:float|None=None)->PrEvaluationResult:
136
+ try:
137
+ options={} if deadline is None else {'deadline':deadline}
138
+ _budget(deadline)
139
+ raw=self._client._request('GET',_path(self.id)+'/result',**options)
140
+ try:
141
+ result=PrEvaluationResult.from_dict(raw);_bound(self._status,result.operation)
142
+ except _PARSE_ERRORS:raise _invalid() from None
143
+ self._status,self._last_result=result.operation,result
144
+ return result
145
+ except HostedClientError as error:raise PrEvaluationError(error,pr_evaluation_id=self.id,handle=self) from None
146
+ def projection(self,requirement_id:str)->ProjectionPage:
147
+ """Read one authorized existing v2 projection; it grants no acceptance."""
148
+ requirement=next((r for r in self._status.requirements if r.requirement_id==identifier(requirement_id)),None)
149
+ require(requirement is not None,'requirement_id must belong to the original operation')
150
+ assert requirement is not None
151
+ try:
152
+ raw=self._client._request('GET',_path(self.id)+'/requirements/'+quote(requirement_id,safe='')+'/projection')
153
+ try:
154
+ page=ProjectionPage.from_dict(raw)
155
+ require(page.selection is None and len(page.items)==1 and
156
+ (page.bank_id,page.release_id,page.cohort_id)==(requirement.bank_id,requirement.release_id,requirement.cohort.identity_id))
157
+ if page.items[0].availability=='available':
158
+ reports=[r.summary for r in page.rows if r.row_kind=='report'];metrics=[r.summary for r in page.rows if r.row_kind=='metric']
159
+ comparisons=[r.summary for r in page.rows if r.row_kind=='comparison']
160
+ require(len(reports)==1 and len(comparisons)<=1 and len(metrics)+len(comparisons)+1==len(page.rows) and isinstance(reports[0],ReportProjectionSummary))
161
+ report=reports[0];assert isinstance(report,ReportProjectionSummary)
162
+ require((report.scope,report.mode)==(requirement.scope,requirement.mode))
163
+ typed=[m for m in metrics if isinstance(m,MetricProjectionSummary)];require(len(typed)==len(metrics))
164
+ require(len({m.metric_id for m in typed})==len(typed) and all((m.run_id,m.grading_id)==(report.run_id,report.grading_id) for m in typed))
165
+ require(all(isinstance(c,ComparisonProjectionSummary) and c.candidate_grading_id==report.grading_id for c in comparisons))
166
+ if self._last_result is not None:
167
+ observed=next(r for r in self._last_result.requirements if r.status.requirement_id==requirement_id)
168
+ if observed.report is not None:
169
+ proof=observed.report.projection;require(page.team_id==observed.report.team_id and
170
+ (page.items[0].event_id,page.items[0].source_digest)==(proof.event_id,proof.source_digest))
171
+ if page.items[0].availability=='available':
172
+ require(report==observed.report.report)
173
+ require({m.metric_id:m for m in typed}=={m.metric_id:m for m in observed.report.metrics})
174
+ if observed.comparison.regressions is not None:
175
+ require(len(comparisons)==1 and isinstance(comparisons[0],ComparisonProjectionSummary)
176
+ and comparisons[0].artifact_digest==observed.comparison.regressions.comparison_digest)
177
+ return page
178
+ except _PARSE_ERRORS:raise _invalid() from None
179
+ except HostedClientError as error:raise PrEvaluationError(error,pr_evaluation_id=self.id,handle=self) from None
180
+ def case_changes(self,requirement_id:str,*,cursor:str|None=None,limit:int=100)->PrCaseChangePage:
181
+ """Read case scores from a committed comparison. This starts no work."""
182
+ from .pr_evaluations import PrCaseChangePage, _case_cursor
183
+ require(any(r.requirement_id==identifier(requirement_id) for r in self._status.requirements),
184
+ 'requirement_id must belong to the original operation')
185
+ require(type(limit) is int and 1<=limit<=500,'limit must be an integer from 1 to 500')
186
+ query='?limit='+str(limit)
187
+ if cursor is not None:query+='&cursor='+quote(_case_cursor(cursor),safe='')
188
+ try:
189
+ raw=self._client._request('GET',_path(self.id)+'/requirements/'+quote(requirement_id,safe='')+'/case-changes'+query)
190
+ try:
191
+ page=PrCaseChangePage.from_dict(raw)
192
+ require((page.plan_id,page.requirement_id)==(self.id,requirement_id) and len(page.items)<=limit)
193
+ if cursor is None:require(page.offset==0)
194
+ if self._last_result is not None:
195
+ observed=next(r for r in self._last_result.requirements if r.status.requirement_id==requirement_id)
196
+ require(observed.report is not None and observed.comparison.regressions is not None)
197
+ assert observed.report is not None and observed.comparison.regressions is not None
198
+ require((page.source_digest,page.comparison_digest,page.total)==(observed.report.projection.source_digest,
199
+ observed.comparison.regressions.comparison_digest,observed.comparison.regressions.case_change_count))
200
+ return page
201
+ except _PARSE_ERRORS:raise _invalid() from None
202
+ except HostedClientError as error:raise PrEvaluationError(error,pr_evaluation_id=self.id,handle=self) from None
203
+ def wait(self,*,timeout:float=900,poll_interval:float=2,on_progress:Callable[[str],None]|None=None)->PrEvaluationResult:
204
+ """Wait on this handle; socket/retry phases use the remaining read budget."""
205
+ return self._wait(timeout=timeout,poll_interval=poll_interval,on_progress=on_progress,reports_only=False)
206
+ def wait_for_reports(self,*,timeout:float=900,poll_interval:float=2,on_progress:Callable[[str],None]|None=None)->PrEvaluationResult:
207
+ """Read completed required reports while human review or policy is pending.
208
+
209
+ A returned report is not an acceptance decision. Use current_acceptance
210
+ and the bank review status in the returned result for that distinction.
211
+ """
212
+ return self._wait(timeout=timeout,poll_interval=poll_interval,on_progress=on_progress,reports_only=True)
213
+ def _wait(self,*,timeout:float,poll_interval:float,on_progress:Callable[[str],None]|None,reports_only:bool)->PrEvaluationResult:
214
+ for value in (timeout,poll_interval):
215
+ if type(value) not in (int,float) or not math.isfinite(value) or value<=0:raise ValueError('timeout and poll_interval must be finite positive seconds')
216
+ deadline=time.monotonic()+timeout;previous=None
217
+ while True:
218
+ if self.status!=previous:
219
+ if on_progress is not None:on_progress(self.status)
220
+ previous=self.status
221
+ if self.status=='failed':
222
+ code=self._status.error.code if self._status.error is not None else 'pr_evaluation_failed'
223
+ raise PrEvaluationError(HostedClientError(code,'PR evaluation failed; inspect result() for retained coverage'),pr_evaluation_id=self.id,handle=self,operation_failed=True)
224
+ ready=bool(self._status.requirements) and all(r.state in ('completed','incomplete') for r in self._status.requirements)
225
+ if self.status=='completed' or reports_only and ready:
226
+ try:return self._result(deadline)
227
+ except PrEvaluationError as error:
228
+ if error.code not in _READ_RETRY:raise
229
+ remaining=deadline-time.monotonic()
230
+ if remaining<=0:raise PrEvaluationError(HostedClientError('wait_timeout','waiting expired'),pr_evaluation_id=self.id,handle=self)
231
+ time.sleep(min(poll_interval,remaining))
232
+ try:self._refresh(deadline)
233
+ except PrEvaluationError as error:
234
+ if error.code not in _READ_RETRY:raise
235
+ if time.monotonic()>=deadline:raise PrEvaluationError(HostedClientError('wait_timeout','waiting expired'),pr_evaluation_id=self.id,handle=self) from None
236
+ def to_dict(self)->dict[str,Any]:return self._status.to_dict()
237
+ def __repr__(self)->str:return f'PrEvaluation(id={self.id!r}, status={self.status!r})'
@@ -0,0 +1,285 @@
1
+ """Offline judge audit and gated PPI arithmetic over supplied labels.
2
+
3
+ This module does not run a model or check that a declared sampling design is
4
+ true. Descriptive audit scores apply only to the supplied records. PPI output is
5
+ an arithmetic demonstration, never an automatically validated performance claim
6
+ or ARES parity result.
7
+
8
+ ``judge_audit`` rows contain ``id``, ``human_label`` (bool or None),
9
+ ``judge_label`` (bool or None) and ``judge_status`` (judged, abstained, missing).
10
+ An omitted status means judged for a boolean label and missing otherwise.
11
+
12
+ ``ppi_mean`` uses a separate prediction cohort and labeled audit cohort. Rows
13
+ contain ``id``, ``cluster_id``, numeric ``prediction`` in [0, 1], and the same
14
+ frozen ``predictor_digest``. Audit rows also need a boolean ``human_label``.
15
+
16
+ Required PPI declarations are ``predictor_digest`` (sha256:<64 hex digits>),
17
+ ``predictor_frozen``, ``audit_representative``, ``prediction_representative``,
18
+ ``same_target_population``, ``cohorts_independent``, ``cohorts_disjoint``,
19
+ ``iid_rows``, ``audit_held_out_from_predictor_tuning`` (all True),
20
+ ``target_population``, ``sampling_protocol``, ``predictor_model``,
21
+ ``predictor_revision``, ``prompt_version`` and ``metric_version`` (nonempty strings), and
22
+ ``provenance_kind`` (synthetic or independent_human_audit).
23
+
24
+ Repeated or shared cluster IDs need a different variance estimator and are not
25
+ accepted. Unknown labels, overlapping IDs and undeclared designs are not
26
+ silently filtered. At least two complete observations in each cohort are needed
27
+ for sample variances. Small synthetic samples still do not support inference.
28
+
29
+ Method source: https://arxiv.org/html/2301.09633v3 (independent-samples PPI
30
+ mean estimator). ARES: https://aclanthology.org/2024.naacl-long.20/ . This module
31
+ does not reproduce ARES judge training or establish finite-sample coverage.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import math
37
+ import re
38
+ from statistics import NormalDist, fmean, variance
39
+ from typing import Any, Mapping, Sequence
40
+
41
+
42
+ class CalibrationError(ValueError):
43
+ """The supplied audit records do not follow the explicit label contract."""
44
+
45
+
46
+ _DECLARATIONS = (
47
+ "predictor_frozen",
48
+ "audit_representative",
49
+ "prediction_representative",
50
+ "same_target_population",
51
+ "cohorts_independent",
52
+ "cohorts_disjoint",
53
+ "iid_rows",
54
+ "audit_held_out_from_predictor_tuning",
55
+ )
56
+ _DIGEST = re.compile(r"sha256:[0-9a-f]{64}\Z")
57
+
58
+
59
+ def _ratio(numerator: int, denominator: int, reason: str) -> dict[str, Any]:
60
+ return {
61
+ "value": numerator / denominator if denominator else None,
62
+ "numerator": numerator,
63
+ "denominator": denominator,
64
+ "status": "observed" if denominator else "undefined",
65
+ "reason": None if denominator else reason,
66
+ }
67
+
68
+
69
+ def _rows(value: Sequence[Mapping[str, Any]]) -> list[Mapping[str, Any]]:
70
+ if not isinstance(value, (list, tuple)) or any(not isinstance(row, Mapping) for row in value):
71
+ raise CalibrationError("records_must_be_an_array_of_objects")
72
+ result = list(value)
73
+ ids = [row.get("id") for row in result]
74
+ if any(type(value) is not str or not value.strip() for value in ids):
75
+ raise CalibrationError("record_ids_must_be_nonempty_strings")
76
+ if len(set(ids)) != len(ids):
77
+ raise CalibrationError("duplicate_record_ids")
78
+ return result
79
+
80
+
81
+ def judge_audit(
82
+ records: Sequence[Mapping[str, Any]], *, provenance_kind: str = "unspecified"
83
+ ) -> dict[str, Any]:
84
+ """Return descriptive confusion metrics with explicit missing populations.
85
+
86
+ Accuracy and disagreement use only decisive judge labels paired with known
87
+ human labels. Abstention and missing-judge rates use all supplied records.
88
+ Neither unknown human labels nor judge abstentions become negative labels.
89
+ """
90
+
91
+ rows = _rows(records)
92
+ tp = fp = tn = fn = 0
93
+ missing_human = missing_judge = abstained = decisive = 0
94
+ for row in rows:
95
+ human, judge = row.get("human_label"), row.get("judge_label")
96
+ if human is not None and type(human) is not bool:
97
+ raise CalibrationError("human_label_must_be_boolean_or_null")
98
+ if judge is not None and type(judge) is not bool:
99
+ raise CalibrationError("judge_label_must_be_boolean_or_null")
100
+ status = row.get("judge_status", "judged" if judge is not None else "missing")
101
+ if status not in {"judged", "abstained", "missing"}:
102
+ raise CalibrationError("unsupported_judge_status")
103
+ if (status == "judged") != (judge is not None):
104
+ raise CalibrationError("judge_status_and_label_disagree")
105
+ missing_human += human is None
106
+ missing_judge += status == "missing"
107
+ abstained += status == "abstained"
108
+ decisive += status == "judged"
109
+ if human is None or judge is None:
110
+ continue
111
+ tp += human and judge
112
+ fp += not human and judge
113
+ tn += not human and not judge
114
+ fn += human and not judge
115
+ paired = tp + fp + tn + fn
116
+ count = len(rows)
117
+ metrics = {
118
+ "accuracy": _ratio(tp + tn, paired, "no_paired_decisive_judgments"),
119
+ "precision": _ratio(tp, tp + fp, "no_predicted_positives"),
120
+ "recall": _ratio(tp, tp + fn, "no_human_positives"),
121
+ "specificity": _ratio(tn, tn + fp, "no_human_negatives"),
122
+ "disagreement": _ratio(fp + fn, paired, "no_paired_decisive_judgments"),
123
+ "judge_abstention_rate": _ratio(abstained, count, "empty_audit"),
124
+ "judge_missing_rate": _ratio(missing_judge, count, "empty_audit"),
125
+ "human_label_coverage": _ratio(count - missing_human, count, "empty_audit"),
126
+ "paired_decisive_coverage": _ratio(paired, count, "empty_audit"),
127
+ }
128
+ chance_agreement = None
129
+ kappa = None
130
+ kappa_reason = "no_paired_decisive_judgments"
131
+ if paired:
132
+ chance_agreement = ((tp + fp) * (tp + fn) + (tn + fn) * (tn + fp)) / paired**2
133
+ if chance_agreement < 1:
134
+ kappa = ((tp + tn) / paired - chance_agreement) / (1 - chance_agreement)
135
+ kappa_reason = None
136
+ else:
137
+ kappa_reason = "constant_labels_make_chance_agreement_one"
138
+ metrics["cohen_kappa"] = {
139
+ "value": kappa,
140
+ "denominator": paired,
141
+ "expected_agreement": chance_agreement,
142
+ "status": "observed" if kappa is not None else "undefined",
143
+ "reason": kappa_reason,
144
+ }
145
+ return {
146
+ "schema_version": "sphynx.rag-diagnostics.judge-audit.v1",
147
+ "scope": "supplied_records_descriptive_only",
148
+ "provenance_kind": provenance_kind,
149
+ "counts": {
150
+ "records": count,
151
+ "human_labeled": count - missing_human,
152
+ "missing_human_label": missing_human,
153
+ "judge_decisive": decisive,
154
+ "judge_abstained": abstained,
155
+ "missing_judge_label": missing_judge,
156
+ "paired_decisive": paired,
157
+ },
158
+ "confusion": {"true_positive": tp, "false_positive": fp, "true_negative": tn, "false_negative": fn},
159
+ "metrics": metrics,
160
+ "performance_inference_validated": False,
161
+ "ares_parity": False,
162
+ "notice": "Scores describe the supplied records. Unknown, abstained and missing labels are not negatives. Independent sampling and population transfer are not established.",
163
+ }
164
+
165
+
166
+ def _unavailable(reason: str, predictions: int, audit: int) -> dict[str, Any]:
167
+ return {
168
+ "schema_version": "sphynx.rag-diagnostics.ppi-mean-arithmetic.v1",
169
+ "status": "unavailable",
170
+ "reason": reason,
171
+ "prediction_count": predictions,
172
+ "audit_count": audit,
173
+ "raw_point_estimate": None,
174
+ "raw_normal_interval": None,
175
+ "performance_confidence_interval": None,
176
+ "performance_inference_validated": False,
177
+ "ares_parity": False,
178
+ }
179
+
180
+
181
+ def ppi_mean(
182
+ prediction_cohort: Sequence[Mapping[str, Any]],
183
+ audit_cohort: Sequence[Mapping[str, Any]],
184
+ *,
185
+ declarations: Mapping[str, Any] | None = None,
186
+ confidence: float = 0.95,
187
+ ) -> dict[str, Any]:
188
+ """Compute a raw rectified-mean example under declared disjoint IID design.
189
+
190
+ Point estimate = mean(prediction cohort) + mean(human - audit prediction).
191
+ Variance = s²(prediction cohort)/N + s²(audit residuals)/n.
192
+
193
+ The normal interval is intentionally unbounded and separately named. This
194
+ helper never clips the estimate or interval to [0, 1], never claims a finite
195
+ sample coverage guarantee, and never upgrades declarations into verified
196
+ independence. Synthetic output remains demonstration arithmetic.
197
+ """
198
+
199
+ try:
200
+ predictions, audit = _rows(prediction_cohort), _rows(audit_cohort)
201
+ except CalibrationError as error:
202
+ return _unavailable(str(error), len(prediction_cohort) if isinstance(prediction_cohort, (list, tuple)) else 0,
203
+ len(audit_cohort) if isinstance(audit_cohort, (list, tuple)) else 0)
204
+ unavailable = lambda reason: _unavailable(reason, len(predictions), len(audit))
205
+ if not isinstance(declarations, Mapping):
206
+ return unavailable("sampling_and_provenance_declarations_required")
207
+ missing = [name for name in _DECLARATIONS if declarations.get(name) is not True]
208
+ if missing:
209
+ return unavailable("sampling_declarations_not_affirmed:" + ",".join(missing))
210
+ for name in ("target_population", "sampling_protocol", "predictor_model",
211
+ "predictor_revision", "prompt_version", "metric_version"):
212
+ if type(declarations.get(name)) is not str or not declarations[name].strip():
213
+ return unavailable(name + "_required")
214
+ digest = declarations.get("predictor_digest")
215
+ if type(digest) is not str or _DIGEST.fullmatch(digest) is None:
216
+ return unavailable("frozen_predictor_digest_required")
217
+ if declarations.get("provenance_kind") not in {"synthetic", "independent_human_audit"}:
218
+ return unavailable("explicit_provenance_kind_required")
219
+ if type(confidence) not in (int, float) or not math.isfinite(confidence) or not 0 < confidence < 1:
220
+ return unavailable("invalid_confidence")
221
+ quantile_probability = 0.5 + confidence / 2
222
+ if not 0 < quantile_probability < 1:
223
+ return unavailable("normal_quantile_not_representable")
224
+ if len(predictions) < 2 or len(audit) < 2:
225
+ return unavailable("at_least_two_observations_per_cohort_required")
226
+ if {row["id"] for row in predictions} & {row["id"] for row in audit}:
227
+ return unavailable("cohorts_overlap")
228
+ clusters = []
229
+ for row in predictions + audit:
230
+ if row.get("predictor_digest") != digest:
231
+ return unavailable("predictor_identity_mismatch")
232
+ value = row.get("prediction")
233
+ if type(value) not in (int, float) or not math.isfinite(value) or not 0 <= value <= 1:
234
+ return unavailable("prediction_must_be_finite_probability")
235
+ cluster = row.get("cluster_id")
236
+ if type(cluster) is not str or not cluster.strip():
237
+ return unavailable("explicit_independent_cluster_ids_required")
238
+ clusters.append(cluster)
239
+ if len(set(clusters)) != len(clusters):
240
+ return unavailable("shared_or_repeated_clusters_need_different_variance")
241
+ if any(type(row.get("human_label")) is not bool for row in audit):
242
+ return unavailable("complete_boolean_human_audit_labels_required")
243
+ prediction_values = [float(row["prediction"]) for row in predictions]
244
+ residuals = [float(row["human_label"]) - float(row["prediction"]) for row in audit]
245
+ prediction_mean = fmean(prediction_values)
246
+ correction = fmean(residuals)
247
+ point = prediction_mean + correction
248
+ prediction_variance = variance(prediction_values) / len(predictions)
249
+ residual_variance = variance(residuals) / len(audit)
250
+ standard_error = math.sqrt(prediction_variance + residual_variance)
251
+ quantile = NormalDist().inv_cdf(quantile_probability)
252
+ synthetic = declarations["provenance_kind"] == "synthetic"
253
+ return {
254
+ "schema_version": "sphynx.rag-diagnostics.ppi-mean-arithmetic.v1",
255
+ "status": "arithmetic_only",
256
+ "reason": "synthetic_demonstration_no_performance_inference" if synthetic else "sampling_declarations_not_independently_verified",
257
+ "prediction_count": len(predictions),
258
+ "audit_count": len(audit),
259
+ "raw_prediction_mean": prediction_mean,
260
+ "raw_human_residual_correction": correction,
261
+ "raw_point_estimate": point,
262
+ "raw_normal_interval": {
263
+ "lower_unbounded": point - quantile * standard_error,
264
+ "upper_unbounded": point + quantile * standard_error,
265
+ "confidence": float(confidence),
266
+ "standard_error": standard_error,
267
+ "prediction_variance_component": prediction_variance,
268
+ "audit_residual_variance_component": residual_variance,
269
+ "method": "independent_disjoint_iid_normal_approximation",
270
+ "finite_sample_coverage_guarantee": False,
271
+ },
272
+ "performance_confidence_interval": None,
273
+ "performance_inference_validated": False,
274
+ "sampling_design_verified": False,
275
+ "synthetic_demonstration": synthetic,
276
+ "ares_parity": False,
277
+ "predictor_digest": digest,
278
+ "declared_predictor_identity": {
279
+ key: declarations[key]
280
+ for key in ("predictor_model", "predictor_revision", "prompt_version", "metric_version")
281
+ },
282
+ "declared_target_population": declarations["target_population"],
283
+ "declared_sampling_protocol": declarations["sampling_protocol"],
284
+ "notice": "Raw PPI mean arithmetic only. The normal approximation is not validated performance confidence. Synthetic or tiny samples do not establish population quality. Sampling, independent human review and frozen predictor provenance require separate verification.",
285
+ }
@@ -0,0 +1,39 @@
1
+ """Readable metric definitions for the local review surface."""
2
+ INFO={
3
+ 'retrieval.recall':('Document recall',r'R_k / R','Known positive qrels found in the first k results / all known positive qrels. The full-reference denominator stays fixed when selection drops documents.'),
4
+ 'retrieval.precision':('Document precision',r'R_k / k','Known positive qrels in the first k positions / k, including unfilled positions. Unjudged entries keep their original ranks.'),
5
+ 'retrieval.average_precision':('Average precision',r'\frac{1}{R}\sum_{r=1}^{k}P@r\,\mathbf{1}[r\text{ relevant}]','AP per case. Macro-averaging eligible case AP values produces MAP. Denominator is all known positives, not min(R,k).'),
6
+ 'retrieval.ndcg_linear':('nDCG · linear gain',r'\frac{\sum_{r=1}^{k}g_r/\log_2(r+1)}{\operatorname{IDCG}@k}','trec_eval-style linear relevance gain. This is a distinct metric from the existing Sphynx exponential-gain nDCG.'),
7
+ 'retrieval.reciprocal_rank':('Reciprocal rank',r'1/r_{\mathrm{first\ relevant}}','Zero when no known positive appears by k. Macro-averaging the per-case values produces MRR.'),
8
+ 'retrieval.unjudged_fraction':('Unjudged fraction',r'U_k / |L_{:k}|','Unknown judgments / returned items by k. An unknown contributes zero gain to the compatibility projection, but remains unknown, never a negative label.'),
9
+ 'evidence.atom_recall':('Required-atom recall',r'|A_{\mathrm{covered}}|/|A_{\mathrm{required}}|','Coverage of distinct required evidence atoms at each recorded stage. This is an evidence proxy, not context sufficiency.'),
10
+ 'evidence.relation_recall':('Required-relation recall',r'|E_{\mathrm{covered}}|/|E_{\mathrm{required}}|','Coverage of explicitly labeled required relations; independent from individual atom coverage.'),
11
+ 'evidence.complete_required_atoms':('All required atoms',r'\mathbf{1}[A_{\mathrm{required}}\subseteq A_{\mathrm{covered}}]','One only if every required atom is covered. It does not establish independently judged sufficiency.'),
12
+ 'context.sufficient':('Context sufficiency',r'\mathbf{1}[\text{reviewed context is sufficient}]','Independent question/context judgment for the exact saved stage. A plausible but stale answer can have sufficient context and still be wrong.'),
13
+ 'generation.answer_correctness':('Answer correctness',r'\mathbf{1}[\text{answer is correct}]','Independent explicit review of the answer. This is not computed from phrase overlap or context faithfulness.'),
14
+ 'generation.response_relevance':('Response relevance',r'\mathbf{1}[\text{answer addresses the question}]','Explicit reviewed relevance, not embedding similarity or keyword matching.'),
15
+ 'generation.required_fact_coverage':('Required-fact coverage',r'|A_{\mathrm{expressed\ correctly}}|/|A_{\mathrm{required}}|','Unique required atoms linked to correct reviewed answer claims. Omissions and unresolved claim truth remain visible.'),
16
+ 'generation.faithfulness':('Claim faithfulness',r'|C_{\mathrm{supported}}|/|C_{\mathrm{all}}|','Requires an exhaustive response claim audit and resolved context-support judgments. It says nothing by itself about source truth.'),
17
+ 'generation.unsupported_claim_fraction':('Unsupported-claim fraction',r'|C_{\mathrm{unsupported}}|/|C_{\mathrm{all}}|','All response claims must be audited. Unknown support is unavailable; it is not counted as a supported or unsupported claim.'),
18
+ 'generation.claim_correctness':('Claim correctness',r'|C_{\mathrm{correct}}|/|C_{\mathrm{all}}|','Claim truth reviewed independently of its support by retrieved material.'),
19
+ 'citation.reference_validity':('Citation ID validity',r'|\mathrm{valid\ references}|/|\mathrm{references}|','Checks known document identities. It does not establish citation entailment.'),
20
+ 'citation.necessity_precision':('Citation necessity precision',r'|\mathrm{supporting\ or\ necessary\ citations}|/|\mathrm{citations}|','ALCE-inspired: joint support must hold; a citation earns credit if it supports alone or its removal breaks joint support. All judgments are supplied labels.'),
21
+ 'citation.completeness':('Citation completeness',r'|C_{\mathrm{jointly\ cited\ and\ supported}}|/|C_{\mathrm{citation\ required}}|','An uncited citation-required claim scores zero. No citations makes precision undefined, not perfect. Not official ALCE segmentation/model parity.'),
22
+ 'outcome.abstention_correctness':('Abstention policy correctness',r'\mathbf{1}[\mathrm{actual}=\mathrm{expected}]','Expected abstention is an explicit independent label; source answerability is not substituted for it.'),
23
+ 'outcome.task_success':('Task success',r'\mathbf{1}[\text{correct relevant answer or expected abstention}]','Conjunctive endpoint gate. A runtime failure counts as failure; missing semantic truth is unavailable.'),
24
+ 'outcome.grounded_task_success':('Grounded task success',r'\mathbf{1}[\text{task success}\land\text{support}\land\text{required citations}]','Requires exhaustive faithfulness, required facts and citation requirements, or an explicitly appropriate abstention. No weighted overall RAG score.'),
25
+ 'operational.recorded_latency_ms':('Recorded latency',r'\mathrm{saved\ latency}_{\mathrm{ms}}','Saved synthetic/component telemetry, never the elapsed time used to replay grading. This local view reports case values and their macro mean; it does not benchmark fresh inference.'),
26
+ 'operational.recorded_cost_usd':('Recorded cost',r'\mathrm{saved\ USD}','Fixture/component-reported cost only. No pricing model or verified cost attestation is implemented.'),
27
+ 'operational.recorded_tokens':('Recorded tokens',r'\mathrm{saved\ token\ count}','Fixture/component-reported count, not tokenization performed by the prototype.'),
28
+ 'operational.failure':('Execution failure',r'\mathbf{1}[\mathrm{status}\ne\mathrm{ok}]','Explicit saved error/timeout status, including failed cases in the operational denominator.'),
29
+ 'operational.timeout':('Execution timeout',r'\mathbf{1}[\mathrm{status}=\mathrm{timeout}]','A timeout is an execution failure, not an empty successful response.'),
30
+ }
31
+
32
+
33
+ def describe(metric_id):
34
+ base=metric_id.split('@')[0]
35
+ if base.startswith('context.sufficient.'):
36
+ base='context.sufficient'
37
+ title,formula,meaning=INFO.get(base,(metric_id,'','See the recorded numerator, denominator and unavailable reason.'))
38
+ if '@' in metric_id:title+=' @'+metric_id.split('@')[1]
39
+ return {'title':title,'formula':formula,'meaning':meaning}
@@ -0,0 +1,26 @@
1
+ """Shared wire records; no model/provider or Sphynx imports."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import json
6
+
7
+ VERSION = 'sphynx.rag-diagnostics.metric.v1'
8
+ STAGES = ('indexed', 'retrieved', 'reranked', 'selected', 'prompt')
9
+
10
+
11
+ def canonical(value):
12
+ return json.dumps(value, sort_keys=True, separators=(',', ':'), ensure_ascii=False, allow_nan=False).encode('utf-8')
13
+
14
+
15
+ def digest(value):
16
+ return hashlib.sha256(canonical(value)).hexdigest()
17
+
18
+
19
+ def metric(metric_id, case_id, stage, value=None, *, numerator=None,
20
+ denominator=None, status=None, reason=None, grader_type='deterministic',
21
+ evidence_refs=None, population='this case', direction='higher', unit='ratio'):
22
+ return {'metric_id': metric_id, 'metric_version': VERSION, 'case_id': case_id,
23
+ 'stage': stage, 'value': value, 'numerator': numerator,
24
+ 'denominator': denominator, 'status': status or ('available' if value is not None else 'unavailable_missing_input'),
25
+ 'reason': reason, 'grader_type': grader_type, 'evidence_refs': evidence_refs or [],
26
+ 'population': population, 'direction': direction, 'unit': unit}