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,385 @@
1
+ """Explicit official-source intake, normalized once rather than repeated per case.
2
+
3
+ Downloads are opt-in. Importing this module never downloads data or runs a model.
4
+ Benchmark labels are retained as dataset annotations, never semantic judge truth.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+ import copy
10
+ from datetime import datetime, timezone
11
+ import hashlib
12
+ import io
13
+ import json
14
+ from pathlib import Path, PurePosixPath
15
+ import ssl
16
+ import urllib.request
17
+ import urllib.error
18
+ from zipfile import ZipFile
19
+
20
+ from ._rag_diagnostics_contracts import ContractError
21
+ from ._rag_diagnostics_importers import _json
22
+
23
+ MAX_SOURCE_BYTES = 100_000_000
24
+ MAX_UNPACKED_BYTES = 200_000_000
25
+ REGISTRY = {
26
+ 'beir.scifact.test': {
27
+ 'name': 'SciFact, BEIR retrieval test split',
28
+ 'source_url': 'https://public.ukp.informatik.tu-darmstadt.de/thakur/BEIR/datasets/scifact.zip',
29
+ 'published_md5': '5f7d1de60b170fc8027bb7898e2efca1',
30
+ 'published_sha256': None,
31
+ 'format': 'beir-zip', 'split': 'test',
32
+ 'upstream': 'https://github.com/beir-cellar/beir/wiki/Datasets-available',
33
+ 'license_source': 'https://github.com/allenai/scifact/blob/master/LICENSE.md',
34
+ 'licenses': ['CC-BY-4.0 (claims/evidence annotations)', 'ODC-By-1.0 (abstract corpus)'],
35
+ 'scope': 'BEIR retrieval split; not the hidden original SciFact verification test set',
36
+ },
37
+ 'hotpotqa.distractor.dev': {
38
+ 'name': 'HotpotQA distractor development set',
39
+ 'source_url': 'https://curtis.ml.cmu.edu/datasets/hotpot/hotpot_dev_distractor_v1.json',
40
+ 'published_md5': None, 'format': 'hotpot-json', 'split': 'dev',
41
+ 'published_sha256': '4e9ecb5c8d3b719f624d66b60f8d56bf227f03914f5f0753d6fa1b359d7104ea',
42
+ 'checksum_source': 'https://huggingface.co/datasets/hotpotqa/hotpot_qa/raw/bf65d2739d97a2681f26ef950b2fc400575dbba3/dataset_infos.json',
43
+ 'fallback_url': 'https://huggingface.co/datasets/namlh2004/hotpotqa/resolve/7e54db4656209750ff487f6fdf8e39a66dba136b/hotpot_dev_distractor_v1.json',
44
+ 'fallback_sha256': 'e3da074df24e8369009918aa5cdbdd254dadcde4c63f7569d36afd6f2268caa8',
45
+ 'upstream': 'https://hotpotqa.github.io/',
46
+ 'license_source': 'https://hotpotqa.github.io/',
47
+ 'licenses': ['CC-BY-SA-4.0'],
48
+ 'scope': 'Per-question supplied distractor context; not full-Wikipedia open retrieval',
49
+ },
50
+ }
51
+
52
+
53
+ def _need(condition, message):
54
+ if not condition:
55
+ raise ContractError(message)
56
+
57
+
58
+ def _sha(raw):
59
+ return hashlib.sha256(raw).hexdigest()
60
+
61
+
62
+ def _raw(path):
63
+ path = Path(path)
64
+ _need(path.is_file() and not path.is_symlink() and 0 < path.stat().st_size <= MAX_SOURCE_BYTES,
65
+ 'Expected a regular source file no larger than 100MB')
66
+ raw = path.read_bytes()
67
+ _need(0 < len(raw) <= MAX_SOURCE_BYTES, 'Empty or oversized benchmark source')
68
+ return raw
69
+
70
+
71
+ def _id(value):
72
+ _need(type(value) is str and 0 < len(value) <= 180 and not any(c.isspace() for c in value), 'Invalid benchmark ID')
73
+ return value
74
+
75
+
76
+ def list_official_benchmarks() -> list[dict]:
77
+ """Describe the two supported upstream formats without accessing any files."""
78
+ return [dict(dataset_id=key, **copy.deepcopy(value)) for key, value in REGISTRY.items()]
79
+
80
+
81
+ def fetch_official_benchmark_source(dataset_id: str, destination: str | Path) -> dict:
82
+ """Download one allowlisted public source to a new file, with bounded I/O.
83
+
84
+ The returned SHA256 freezes the received bytes. A published MD5, when present,
85
+ is an upstream compatibility check, not a cryptographic signature.
86
+ """
87
+ _need(dataset_id in REGISTRY, 'Unsupported official dataset')
88
+ info = REGISTRY[dataset_id]
89
+ destination = Path(destination)
90
+ _need(not destination.exists(), 'Source destination already exists')
91
+ url = info['source_url']
92
+ request = urllib.request.Request(url, headers={'User-Agent': 'Sphynx-benchmark-intake/1'})
93
+ fallback = False
94
+ try:
95
+ with urllib.request.urlopen(request, timeout=15, context=ssl.create_default_context()) as response:
96
+ _need(response.url == url, 'Unexpected upstream redirect; review the source URL')
97
+ raw = response.read(MAX_SOURCE_BYTES + 1)
98
+ except (urllib.error.URLError, TimeoutError):
99
+ _need('fallback_url' in info, 'Official source unavailable and no pinned fallback exists')
100
+ fallback = True
101
+ with urllib.request.urlopen(info['fallback_url'], timeout=30, context=ssl.create_default_context()) as response:
102
+ mirror = response.read(MAX_SOURCE_BYTES + 1)
103
+ _need(len(mirror) <= MAX_SOURCE_BYTES and _sha(mirror) == info['fallback_sha256'], 'Pinned mirror bytes differ')
104
+ # The mirror changed JSON whitespace only. Reconstruct the upstream
105
+ # ujson serialization, and require the independently recorded ORIGINAL
106
+ # byte hash. This never accepts semantic similarity in place of identity.
107
+ raw = json.dumps(_json(mirror), ensure_ascii=True, allow_nan=False,
108
+ separators=(',', ':')).replace('/', '\\/').encode('utf-8')
109
+ _need(0 < len(raw) <= MAX_SOURCE_BYTES, 'Empty or oversized official download')
110
+ md5 = hashlib.md5(raw, usedforsecurity=False).hexdigest()
111
+ _need(info['published_md5'] is None or md5 == info['published_md5'], 'Published upstream checksum differs')
112
+ _need(info['published_sha256'] is None or _sha(raw) == info['published_sha256'], 'Published upstream SHA256 differs')
113
+ destination.parent.mkdir(parents=True, exist_ok=True)
114
+ with destination.open('xb') as handle:
115
+ handle.write(raw)
116
+ return {'dataset_id': dataset_id, 'source_url': url, 'path': str(destination.resolve()),
117
+ 'sha256': _sha(raw), 'bytes': len(raw), 'md5': md5,
118
+ 'upstream_md5_matched': info['published_md5'] is not None,
119
+ 'upstream_sha256_matched': info['published_sha256'] is not None,
120
+ 'fallback_used': fallback,
121
+ 'transfer_url': info['fallback_url'] if fallback else url,
122
+ 'fallback_sha256': info.get('fallback_sha256') if fallback else None,
123
+ 'reconstruction': 'compact ASCII JSON with escaped forward slashes; exact original SHA256 matched' if fallback else None}
124
+
125
+
126
+ def _jsonl(raw):
127
+ rows = []
128
+ for line in raw.splitlines():
129
+ if line.strip():
130
+ row = _json(line)
131
+ _need(type(row) is dict, 'Expected JSONL objects')
132
+ rows.append(row)
133
+ _need(rows, 'Expected nonempty JSONL input')
134
+ return rows
135
+
136
+
137
+ def _beir(raw):
138
+ with ZipFile(io.BytesIO(raw)) as archive:
139
+ entries = archive.infolist()
140
+ _need(len(entries) <= 1000 and len({i.filename for i in entries}) == len(entries), 'Duplicate or excessive archive members')
141
+ _need(sum(i.file_size for i in entries) <= MAX_UNPACKED_BYTES, 'Archive exceeds uncompressed limit')
142
+ for item in entries:
143
+ path = PurePosixPath(item.filename)
144
+ _need(not path.is_absolute() and '..' not in path.parts and '\\' not in item.filename
145
+ and ':' not in item.filename and (item.external_attr >> 16) & 0o170000 != 0o120000,
146
+ 'Unsafe benchmark archive member')
147
+ def member(name):
148
+ info = archive.getinfo('scifact/' + name)
149
+ _need(info.file_size <= MAX_SOURCE_BYTES, 'Oversized archive member')
150
+ raw = archive.read(info)
151
+ _need(len(raw) <= MAX_SOURCE_BYTES, 'Oversized archive member')
152
+ return raw
153
+ corpus, queries = _jsonl(member('corpus.jsonl')), _jsonl(member('queries.jsonl'))
154
+ text = member('qrels/test.tsv').decode('utf-8')
155
+ corp = {}
156
+ for row in corpus:
157
+ did = _id(row.get('_id'))
158
+ _need(did not in corp and type(row.get('text')) is str and type(row.get('title', '')) is str, 'Invalid corpus row')
159
+ corp[did] = row
160
+ all_queries = {}
161
+ for row in queries:
162
+ qid = _id(row.get('_id'))
163
+ _need(qid not in all_queries and type(row.get('text')) is str and row['text'].strip(), 'Invalid query row')
164
+ all_queries[qid] = row
165
+ qrels, pairs = [], set()
166
+ lines = [line for line in text.splitlines() if line.strip()]
167
+ _need(lines and lines[0].split('\t') == ['query-id', 'corpus-id', 'score'], 'Expected BEIR qrel header')
168
+ for line in lines[1:]:
169
+ parts = line.split('\t')
170
+ _need(len(parts) == 3 and parts[2].isdigit(), 'Invalid qrel row')
171
+ qid, did, score = parts[0], parts[1], int(parts[2])
172
+ _need(qid in all_queries and did in corp and (qid, did) not in pairs and 0 <= score <= 100, 'Invalid qrel population')
173
+ pairs.add((qid, did))
174
+ qrels.append((qid, did, score))
175
+ selected = {qid for qid, _, _ in qrels}
176
+ _need(selected, 'Empty evaluation qrel split')
177
+ return list(corp.values()), [row for qid, row in all_queries.items() if qid in selected], qrels, []
178
+
179
+
180
+ def _hotpot(raw):
181
+ data = _json(raw)
182
+ _need(type(data) is list and 0 < len(data) <= 100000, 'Expected HotpotQA records')
183
+ corpus, queries, qrels, annotations, ids = {}, [], [], [], set()
184
+ for row in data:
185
+ _need(type(row) is dict, 'Invalid HotpotQA case')
186
+ qid = _id(row.get('_id'))
187
+ _need(qid not in ids and type(row.get('question')) is str and row['question'].strip()
188
+ and type(row.get('answer')) is str, 'Invalid or duplicate HotpotQA question')
189
+ ids.add(qid)
190
+ context = row.get('context')
191
+ _need(type(context) is list and 0 < len(context) <= 100, 'Invalid distractor context')
192
+ by_title, context_ids = {}, []
193
+ for item in context:
194
+ _need(type(item) is list and len(item) == 2 and type(item[0]) is str
195
+ and type(item[1]) is list and all(type(s) is str for s in item[1]), 'Invalid context sentences')
196
+ title, sentences = item
197
+ _need(title not in by_title, 'Duplicate context title')
198
+ did = 'hp-' + _sha(json.dumps(item, ensure_ascii=False, separators=(',', ':')).encode())
199
+ corpus[did] = {'_id': did, 'title': title, 'text': ''.join(sentences), 'sentences': sentences}
200
+ by_title[title] = (did, len(sentences))
201
+ context_ids.append(did)
202
+ supporting = row.get('supporting_facts')
203
+ _need(type(supporting) is list and supporting, 'Supporting-fact annotations required')
204
+ evidence, seen_facts, issues = set(), set(), []
205
+ for item in supporting:
206
+ _need(type(item) is list and len(item) == 2 and type(item[0]) is str and type(item[1]) is int
207
+ and item[1] >= 0, 'Malformed supporting-fact annotation')
208
+ fact = tuple(item)
209
+ if fact in seen_facts:
210
+ issues.append({'kind': 'duplicate_supporting_fact', 'fact': item})
211
+ seen_facts.add(fact)
212
+ if item[0] not in by_title:
213
+ issues.append({'kind': 'missing_supporting_title', 'fact': item})
214
+ elif item[1] >= by_title[item[0]][1]:
215
+ issues.append({'kind': 'supporting_sentence_out_of_range', 'fact': item})
216
+ else:
217
+ evidence.add(by_title[item[0]][0])
218
+ queries.append({'_id': qid, 'text': row['question'], 'context_ids': context_ids,
219
+ 'question_type': row.get('type'), 'difficulty': row.get('level')})
220
+ if not issues:
221
+ qrels.extend((qid, did, 1) for did in sorted(evidence))
222
+ annotations.append({'query_id': qid, 'answer': row['answer'], 'supporting_facts': supporting,
223
+ 'authority': 'upstream_dataset_annotation', 'independent_trace_review': False,
224
+ 'support_reference_status': 'unavailable_upstream_annotation_issue' if issues else 'available',
225
+ 'validation_issues': issues})
226
+ return list(corpus.values()), queries, qrels, annotations
227
+
228
+
229
+ def prepare_official_benchmark(dataset_id: str, source_path: str | Path, output_dir: str | Path,
230
+ *, expected_sha256: str) -> dict:
231
+ """Normalize pinned upstream bytes into a new checksummed, private bundle.
232
+
233
+ Source shape and checksum are validated; caller-supplied bytes do not prove
234
+ their origin. Preserve the download receipt alongside this bundle.
235
+ """
236
+ _need(dataset_id in REGISTRY, 'Unsupported official dataset')
237
+ raw = _raw(source_path)
238
+ _need(type(expected_sha256) is str and _sha(raw) == expected_sha256, 'Source SHA256 differs from explicit pin')
239
+ info = REGISTRY[dataset_id]
240
+ if info['published_md5'] is not None:
241
+ _need(hashlib.md5(raw, usedforsecurity=False).hexdigest() == info['published_md5'],
242
+ 'Source differs from published upstream checksum')
243
+ if info['published_sha256'] is not None:
244
+ _need(_sha(raw) == info['published_sha256'], 'Source differs from published upstream SHA256')
245
+ corpus, queries, qrels, annotations = _beir(raw) if info['format'] == 'beir-zip' else _hotpot(raw)
246
+ output = Path(output_dir)
247
+ _need(not output.exists(), 'Benchmark bundle already exists; choose a new release directory')
248
+ output.mkdir(parents=True, exist_ok=False)
249
+ files = {}
250
+ def write(name, content):
251
+ _need(len(content) <= MAX_UNPACKED_BYTES, 'Normalized file exceeds bundle limit')
252
+ with (output / name).open('xb') as handle:
253
+ handle.write(content)
254
+ files[name] = {'sha256': _sha(content), 'bytes': len(content)}
255
+ def rows(items):
256
+ return b''.join((json.dumps(row, ensure_ascii=False, allow_nan=False, separators=(',', ':'))+'\n').encode('utf-8') for row in items)
257
+ write('corpus.jsonl', rows(corpus))
258
+ write('queries.jsonl', rows(queries))
259
+ write('qrels.tsv', ('query-id\tcorpus-id\tscore\n' + ''.join(f'{q}\t{d}\t{s}\n' for q,d,s in qrels)).encode('utf-8'))
260
+ write('annotations.jsonl', rows(annotations))
261
+ manifest = {'schema_version': 'sphynx.official-benchmark.v1', 'dataset_id': dataset_id,
262
+ 'prepared_at_utc': datetime.now(timezone.utc).isoformat(), 'upstream': info,
263
+ 'source_sha256': expected_sha256, 'source_bytes': len(raw), 'files': files,
264
+ 'counts': {'documents': len(corpus), 'queries': len(queries), 'qrels': len(qrels), 'answer_annotations': len(annotations)},
265
+ 'reference_validation': {'queries_with_issues': sum(bool(a['validation_issues']) for a in annotations),
266
+ 'policy': 'retain every query and raw annotation; affected support references are unavailable, never repaired or counted as zero'},
267
+ 'intake_source_sha256': _sha(Path(__file__).read_bytes()),
268
+ 'semantic_review': 'KIV; upstream annotations do not review candidate outputs',
269
+ 'population': 'the complete selected public split; not representative Sable customer traffic',
270
+ 'sampling': {'automatic_subsampling': False, 'independent_clusters_verified': False},
271
+ 'execution': {'models_run': False, 'hosted_admission': False},
272
+ 'origin_basis': 'exact caller-pinned bytes; preserve the independent upstream download receipt'}
273
+ with (output / 'manifest.json').open('x', encoding='utf-8', newline='\n') as handle:
274
+ json.dump(manifest, handle, indent=2, ensure_ascii=False, allow_nan=False)
275
+ handle.write('\n')
276
+ return {'path': str(output.resolve()), 'manifest_sha256': _sha((output/'manifest.json').read_bytes()), **manifest['counts']}
277
+
278
+
279
+ @dataclass(frozen=True)
280
+ class OfficialBenchmark:
281
+ """Verified normalized data with labels separate from candidate-safe inputs."""
282
+ manifest: dict
283
+ corpus: dict
284
+ queries: dict
285
+ qrels: dict
286
+ annotations: dict
287
+
288
+ def candidate_case(self, query_id: str) -> dict:
289
+ query = self.queries[query_id]
290
+ ids = query['context_ids'] if 'context_ids' in query else self.corpus
291
+ return {'id': query_id, 'question': query['text'], 'documents': [
292
+ {'id': did, 'title': self.corpus[did].get('title', ''), 'text': self.corpus[did]['text']}
293
+ for did in ids]}
294
+
295
+
296
+ def load_official_benchmark(path: str | Path, *, expected_manifest_sha256: str) -> OfficialBenchmark:
297
+ """Verify every normalized file against an explicitly pinned final manifest."""
298
+ root = Path(path)
299
+ raw = _raw(root / 'manifest.json')
300
+ _need(_sha(raw) == expected_manifest_sha256, 'Benchmark manifest checksum differs')
301
+ manifest = _json(raw)
302
+ _need(type(manifest) is dict and manifest.get('schema_version') == 'sphynx.official-benchmark.v1'
303
+ and manifest.get('dataset_id') in REGISTRY, 'Unsupported official benchmark manifest')
304
+ names = {'corpus.jsonl', 'queries.jsonl', 'qrels.tsv', 'annotations.jsonl'}
305
+ _need(set(manifest.get('files', {})) == names, 'Invalid bundle file closure')
306
+ contents = {}
307
+ for name in sorted(names):
308
+ target = root / name
309
+ _need(target.is_file() and not target.is_symlink() and target.stat().st_size <= MAX_UNPACKED_BYTES, 'Missing or oversized bundle file')
310
+ data = target.read_bytes()
311
+ _need(_sha(data) == manifest['files'][name]['sha256'] and len(data) == manifest['files'][name]['bytes'], 'Normalized benchmark bytes changed')
312
+ contents[name] = data
313
+ def keyed(rows, key):
314
+ result = {}
315
+ for row in rows:
316
+ identity = _id(row.get(key))
317
+ _need(identity not in result, 'Duplicate normalized identity')
318
+ result[identity] = row
319
+ return result
320
+ corpus = keyed(_jsonl(contents['corpus.jsonl']), '_id')
321
+ queries = keyed(_jsonl(contents['queries.jsonl']), '_id')
322
+ annotations = keyed(_jsonl(contents['annotations.jsonl']), 'query_id') if contents['annotations.jsonl'] else {}
323
+ _need(set(annotations) <= set(queries), 'Unknown annotated question')
324
+ for row in corpus.values():
325
+ _need(type(row.get('text')) is str and type(row.get('title', '')) is str, 'Invalid normalized passage')
326
+ corpus_ids = set(corpus)
327
+ for row in queries.values():
328
+ _need(type(row.get('text')) is str and row['text'].strip(), 'Invalid normalized query')
329
+ if 'context_ids' in row:
330
+ ids = row['context_ids']
331
+ _need(type(ids) is list and all(type(i) is str for i in ids)
332
+ and len(ids) == len(set(ids)) and set(ids) <= corpus_ids, 'Invalid normalized context')
333
+ qrels = {qid: {} for qid in queries}
334
+ for qid, annotation in annotations.items():
335
+ _need(type(annotation.get('validation_issues')) is list and annotation.get('support_reference_status')
336
+ in ('available', 'unavailable_upstream_annotation_issue'), 'Missing reference validation status')
337
+ if annotation['validation_issues']:
338
+ _need(annotation['support_reference_status'] == 'unavailable_upstream_annotation_issue', 'Inconsistent reference status')
339
+ qrels[qid] = None
340
+ lines = contents['qrels.tsv'].decode('utf-8').splitlines()
341
+ _need(lines and lines[0] == 'query-id\tcorpus-id\tscore', 'Invalid normalized qrel header')
342
+ for line in lines[1:]:
343
+ parts = line.split('\t')
344
+ _need(len(parts) == 3 and parts[2].isdigit(), 'Invalid normalized qrel')
345
+ qid, did, value = parts
346
+ _need(qid in queries and did in corpus and qrels[qid] is not None and did not in qrels[qid] and 0 <= int(value) <= 100,
347
+ 'Invalid normalized qrel identity or grade')
348
+ _need('context_ids' not in queries[qid] or did in queries[qid]['context_ids'], 'Qrel outside question context')
349
+ qrels[qid][did] = int(value)
350
+ _need(manifest.get('counts') == {'documents': len(corpus), 'queries': len(queries),
351
+ 'qrels': sum(len(values) for values in qrels.values() if values is not None),
352
+ 'answer_annotations': len(annotations)}, 'Normalized population differs')
353
+ return OfficialBenchmark(manifest, corpus, queries, qrels, annotations)
354
+
355
+
356
+ def main(argv=None):
357
+ """Explicit source-download and offline import commands; no inference."""
358
+ import argparse
359
+ parser = argparse.ArgumentParser(description=__doc__)
360
+ commands = parser.add_subparsers(dest='command', required=True)
361
+ commands.add_parser('list')
362
+ fetch = commands.add_parser('fetch')
363
+ fetch.add_argument('dataset_id', choices=REGISTRY)
364
+ fetch.add_argument('--output', required=True)
365
+ prepare = commands.add_parser('prepare')
366
+ prepare.add_argument('dataset_id', choices=REGISTRY)
367
+ prepare.add_argument('--source', required=True)
368
+ prepare.add_argument('--sha256', required=True)
369
+ prepare.add_argument('--output', required=True)
370
+ args = parser.parse_args(argv)
371
+ try:
372
+ if args.command == 'list':
373
+ result = list_official_benchmarks()
374
+ elif args.command == 'fetch':
375
+ result = fetch_official_benchmark_source(args.dataset_id, args.output)
376
+ else:
377
+ result = prepare_official_benchmark(args.dataset_id, args.source, args.output, expected_sha256=args.sha256)
378
+ except (ContractError, OSError, ValueError) as exc:
379
+ parser.error(str(exc))
380
+ print(json.dumps(result, indent=2, ensure_ascii=False, allow_nan=False))
381
+ return 0
382
+
383
+
384
+ if __name__ == '__main__':
385
+ raise SystemExit(main())
sphynx/packs.py ADDED
@@ -0,0 +1,258 @@
1
+ """Installed pack descriptors and candidate protocol compatibility checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from .contracts import (
8
+ CASE_SCHEMA_VERSION,
9
+ INTERFACE_VERSION,
10
+ PACK_ID,
11
+ SABLE_RERANK_DEV_PACK_ID,
12
+ REQUEST_SCHEMA_VERSION,
13
+ RESPONSE_SCHEMA_VERSION,
14
+ CandidateSpec,
15
+ )
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class ExecutionPolicy:
20
+ """Pack-owned local trial and timeout policy; no CPU/memory isolation."""
21
+
22
+ policy_version: str
23
+ trials: int
24
+ per_case_timeout_seconds: float
25
+ resource_policy_id: str
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class PackDescriptor:
30
+ pack_id: str
31
+ pack_release: str
32
+ title: str
33
+ track: str
34
+ interface_version: str
35
+ case_schema_version: str
36
+ request_schema_version: str
37
+ response_schema_version: str
38
+ status: str
39
+ description: str
40
+ execution_policy: ExecutionPolicy
41
+ case_count: int | None = None
42
+ metric_release: str | None = None
43
+ required_inputs: tuple[str, ...] = ()
44
+ setup_instructions: tuple[str, ...] = ()
45
+ run_api: str | None = None
46
+
47
+ @property
48
+ def runnable(self) -> bool:
49
+ return self.status == "runnable"
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class CandidateCheck:
54
+ candidate_name: str
55
+ pack_id: str
56
+ compatible: bool
57
+ pack_runnable: bool
58
+ reasons: tuple[str, ...]
59
+ pack: PackDescriptor | None
60
+
61
+
62
+ _PACKS: tuple[PackDescriptor, ...] = (
63
+ PackDescriptor(
64
+ pack_id=PACK_ID,
65
+ pack_release="v1",
66
+ title="Illustrative frozen-pool RAG reranking",
67
+ track="rag_rerank",
68
+ interface_version=INTERFACE_VERSION,
69
+ case_schema_version=CASE_SCHEMA_VERSION,
70
+ request_schema_version=REQUEST_SCHEMA_VERSION,
71
+ response_schema_version=RESPONSE_SCHEMA_VERSION,
72
+ status="runnable",
73
+ description="Six synthetic illustrative frozen-pool cases; local scores are not a validated benchmark.",
74
+ execution_policy=ExecutionPolicy(
75
+ policy_version="v1",
76
+ trials=1,
77
+ per_case_timeout_seconds=30.0,
78
+ resource_policy_id="local-unbounded.v1",
79
+ ),
80
+ case_count=6,
81
+ metric_release="rag.rerank.metric.v1",
82
+ required_inputs=("Compatible reranking CandidateSpec and a writable output directory.",),
83
+ setup_instructions=(
84
+ "The six synthetic cases are bundled with the SDK; no external dataset, model, provider, or cloud is required.",
85
+ "Call run(candidate, 'rag.rerank.v1', output_dir), or run_demo(output_dir) for both packaged examples.",
86
+ "Local labels are owner-readable; results remain local_unverified and are not a validated benchmark.",
87
+ ),
88
+ run_api="sphynx.run",
89
+ ),
90
+ PackDescriptor(
91
+ pack_id=SABLE_RERANK_DEV_PACK_ID,
92
+ pack_release="v1.external",
93
+ title="Owner-held Sable passage reranking development",
94
+ track="rag_rerank",
95
+ interface_version=INTERFACE_VERSION,
96
+ case_schema_version=CASE_SCHEMA_VERSION,
97
+ request_schema_version=REQUEST_SCHEMA_VERSION,
98
+ response_schema_version=RESPONSE_SCHEMA_VERSION,
99
+ status="requires_local_data",
100
+ description="Pass the owner-held dataset to run_private_rerank; release identity binds its complete SHA-256.",
101
+ execution_policy=ExecutionPolicy(
102
+ policy_version="v1", trials=1, per_case_timeout_seconds=30.0,
103
+ resource_policy_id="local-unbounded.v1",
104
+ ),
105
+ metric_release="rag.rerank.metric.v1",
106
+ required_inputs=(
107
+ "Authorized owner-held frozen-pool dataset JSON and its expected SHA-256.",
108
+ "Compatible reranking CandidateSpec and a writable output directory.",
109
+ ),
110
+ setup_instructions=(
111
+ "Obtain permission to use the local customer bank; no private dataset is bundled or downloaded.",
112
+ "Call run_private_rerank(candidate, dataset_path, output_dir, expected_sha256=expected_sha256).",
113
+ "Retain the same dataset and expected SHA-256 for grade_private_rerank; check its label status before interpreting scores.",
114
+ "Install any model or provider dependencies required by your candidate separately.",
115
+ ),
116
+ run_api="sphynx.run_private_rerank",
117
+ ),
118
+ PackDescriptor(
119
+ pack_id="rag.retrieve.v1",
120
+ pack_release="v1.external",
121
+ title="Owner-held full retrieval architecture development",
122
+ track="rag_retrieve",
123
+ interface_version="sphynx.rag.retrieve.v1",
124
+ case_schema_version="sphynx.rag.retrieve.dataset.v1",
125
+ request_schema_version="sphynx.rag.retrieve.request.v1",
126
+ response_schema_version="sphynx.rag.retrieve.response.v1",
127
+ status="requires_local_data",
128
+ description="Full corpus retrieval, fusion and context selection; use run_retrieval or capture_retrieval with owner-held data. Candidate pools may differ.",
129
+ execution_policy=ExecutionPolicy(
130
+ policy_version="rag.retrieve.policy.v1", trials=1,
131
+ per_case_timeout_seconds=0.0, resource_policy_id="local-in-process-unbounded.v1",
132
+ ),
133
+ metric_release="rag.retrieve.metric.v1",
134
+ required_inputs=(
135
+ "Authorized owner-held retrieval dataset JSON containing the corpus, question cohort, and reference labels.",
136
+ "Candidate identity plus a trusted adapter callable or captured batch responses; a writable output directory.",
137
+ ),
138
+ setup_instructions=(
139
+ "Prepare authorized question-bank and corpus assets; no customer data, index, embedding model, or provider access is bundled.",
140
+ "Call run_retrieval(candidate, adapter, dataset_path, output_dir); use capture_retrieval(candidate, dataset_path, responses, output_dir) for batch captures.",
141
+ "Use expected_sha256 to check an approved dataset digest; retain the dataset for grade_retrieval.",
142
+ "The callable runs in process without an OS sandbox or timeout; planned answer metrics are not computed.",
143
+ ),
144
+ run_api="sphynx.run_retrieval",
145
+ ),
146
+ PackDescriptor(
147
+ pack_id="rag.invoke.v1",
148
+ pack_release="v1-external-banks",
149
+ title="Five-customer RAG invocation development",
150
+ track="rag_invoke",
151
+ interface_version="sphynx.rag.invoke.v1",
152
+ case_schema_version="sphynx.rag.invoke.request.v1",
153
+ request_schema_version="sphynx.rag.invoke.request.v1",
154
+ response_schema_version="sphynx.rag.invoke.response.v1",
155
+ status="requires_local_data",
156
+ description="Pinned 200/300 development and 90/80 regression banks; use run_invocation with owner-held local data.",
157
+ execution_policy=ExecutionPolicy(
158
+ policy_version="v1", trials=1, per_case_timeout_seconds=30.0,
159
+ resource_policy_id="local-unbounded.v1",
160
+ ),
161
+ case_count=None,
162
+ metric_release="rag.invoke.metric.v1",
163
+ required_inputs=(
164
+ "Authorized local bank directory with pinned manifest.json, model-input.jsonl, and gold.jsonl for a release listed by list_invocation_banks().",
165
+ "Invocation CandidateSpec using sphynx.rag.invoke.v1 and a writable output directory.",
166
+ ),
167
+ setup_instructions=(
168
+ "Use list_invocation_banks() to choose excerpt-free-200, development-300, narrow-policy-90, or decision-boundary-80; files are not bundled or downloaded.",
169
+ "Call run_invocation(candidate, data_dir, output_dir, bank_id=bank_id); the loader checks the manifest, pinned hashes, case counts, and labels.",
170
+ "Retain the same bank for grade_invocation; install candidate-specific provider or model dependencies separately.",
171
+ "Local labels are owner-readable; development routing scores remain local_unverified.",
172
+ "Development-300 includes supplied source-excerpt controls; regression banks are previously exercised, not blind holdouts.",
173
+ ),
174
+ run_api="sphynx.run_invocation",
175
+ ),
176
+ PackDescriptor(
177
+ pack_id="rag.question_robustness.v1", pack_release="v1.external",
178
+ title="Paired question wording robustness development", track="rag_question_robustness",
179
+ interface_version="sphynx.rag.question_robustness.v1",
180
+ case_schema_version="sphynx.rag.question_robustness.pilot.draft.v1",
181
+ request_schema_version="sphynx.rag.question_robustness.request.v1",
182
+ response_schema_version="sphynx.rag.question_robustness.response.v1",
183
+ status="requires_local_data",
184
+ description="Pinned owner-held paired pilot; pending human review remains provisional. Observed search and owner query judgments support separate grades.",
185
+ execution_policy=ExecutionPolicy("rag.question_robustness.policy.v1", 1, 0.0, "trusted-local-in-process.v1"),
186
+ case_count=60, metric_release="rag.question_robustness.metric.v1",
187
+ required_inputs=("Owner-held pilot directory, approved manifest SHA-256, trusted adapter and pinned candidate metadata.",),
188
+ setup_instructions=(
189
+ "Call run_question_robustness(candidate, adapter, data_dir, output_dir, mode='routing_only' or 'observed_pipeline').",
190
+ "Use ObservedSearch.search in pipeline adapters; candidate assertions alone do not establish search or evidence.",
191
+ "Call grade_question_robustness; supply separate capture-bound owner query judgments for semantic metrics.",
192
+ "Original labels remain provisional; adapters run in process without a timeout or OS sandbox. No cloud projection is installed.",
193
+ ), run_api="sphynx.run_question_robustness",
194
+ ),
195
+ PackDescriptor(
196
+ pack_id="rag.rerank.beir.v1", pack_release="v1.external",
197
+ title="Six-domain public BEIR reranking development", track="rag_rerank_public",
198
+ interface_version=INTERFACE_VERSION, case_schema_version="expanded-reranker.frozen-pools.v1",
199
+ request_schema_version=REQUEST_SCHEMA_VERSION, response_schema_version=RESPONSE_SCHEMA_VERSION,
200
+ status="requires_local_data",
201
+ description="180 public queries with frozen BM25 pools; select one 90-query original split. Seven Sable illustrations are excluded.",
202
+ execution_policy=ExecutionPolicy("rag.rerank.beir.policy.v1", 1, 30.0, "local-unbounded.v1"),
203
+ case_count=90, metric_release="rag.rerank.beir.metric.v1",
204
+ required_inputs=("Authorized external frozen-pools.json, expected SHA-256 and compatible reranking CandidateSpec.",),
205
+ setup_instructions=(
206
+ "Call run_public_rerank(candidate, dataset_path, output_dir, split='development' or 'confirmation').",
207
+ "Use grade_public_rerank, report_public_rerank and compare_public_rerank with the same dataset and split.",
208
+ "Native positive qrels use grade >0. Ranking quality is conditional on relevant passages being present in the BM25 pool.",
209
+ "Both original splits were previously opened and are exploratory; raw public datasets are not redistributed with the SDK.",
210
+ ), run_api="sphynx.run_public_rerank",
211
+ ),
212
+
213
+ )
214
+
215
+
216
+ def list_packs() -> tuple[PackDescriptor, ...]:
217
+ """Return descriptors installed with the package, independent of CWD."""
218
+
219
+ return _PACKS
220
+
221
+
222
+ def get_pack(pack_id: str) -> PackDescriptor:
223
+ """Return an installed descriptor or raise an actionable lookup error."""
224
+
225
+ for pack in _PACKS:
226
+ if pack.pack_id == pack_id:
227
+ return pack
228
+ available = ", ".join(pack.pack_id for pack in _PACKS) or "none"
229
+ raise ValueError(f"Unknown pack {pack_id!r}. Installed packs: {available}")
230
+
231
+
232
+ def check_candidate(candidate: CandidateSpec, pack_id: str) -> CandidateCheck:
233
+ """Check static interface compatibility; does not execute the candidate.
234
+
235
+ ``pack_runnable`` is separate because a valid candidate protocol can exist
236
+ before the pack's cases and grader are shipped.
237
+ """
238
+
239
+ if not isinstance(candidate, CandidateSpec):
240
+ raise TypeError("candidate must be CandidateSpec")
241
+ try:
242
+ pack = get_pack(pack_id)
243
+ except ValueError as exc:
244
+ return CandidateCheck(candidate.name, pack_id, False, False, (str(exc),), None)
245
+ reasons: list[str] = []
246
+ if candidate.interface_version != pack.interface_version:
247
+ reasons.append(
248
+ f"Candidate interface {candidate.interface_version!r} is incompatible with {pack.pack_id}; "
249
+ f"use {pack.interface_version!r}."
250
+ )
251
+ return CandidateCheck(
252
+ candidate_name=candidate.name,
253
+ pack_id=pack.pack_id,
254
+ compatible=not reasons,
255
+ pack_runnable=pack.runnable,
256
+ reasons=tuple(reasons),
257
+ pack=pack,
258
+ )