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.
- sable_sphynx-0.7.0.dev0.dist-info/METADATA +152 -0
- sable_sphynx-0.7.0.dev0.dist-info/RECORD +174 -0
- sable_sphynx-0.7.0.dev0.dist-info/WHEEL +5 -0
- sable_sphynx-0.7.0.dev0.dist-info/top_level.txt +1 -0
- sphynx/__init__.py +469 -0
- sphynx/__init__.pyi +376 -0
- sphynx/_analytical_client.py +77 -0
- sphynx/_analytical_values.py +252 -0
- sphynx/_audit_client.py +173 -0
- sphynx/_comparison_adapters.py +113 -0
- sphynx/_componentized_full_rag.py +34 -0
- sphynx/_composed_pipeline.py +109 -0
- sphynx/_full_rag_grading.py +218 -0
- sphynx/_full_rag_trace.py +221 -0
- sphynx/_local_eval.py +268 -0
- sphynx/_pipeline_core.py +243 -0
- sphynx/_pr_client.py +237 -0
- sphynx/_rag_diagnostics_calibration.py +285 -0
- sphynx/_rag_diagnostics_catalog.py +39 -0
- sphynx/_rag_diagnostics_common.py +26 -0
- sphynx/_rag_diagnostics_contracts.py +186 -0
- sphynx/_rag_diagnostics_engine.py +243 -0
- sphynx/_rag_diagnostics_importers.py +242 -0
- sphynx/_rag_diagnostics_ranking.py +81 -0
- sphynx/_rag_diagnostics_semantic.py +325 -0
- sphynx/_rag_ranking.py +149 -0
- sphynx/_speech_benchmark_scoring.py +140 -0
- sphynx/_speech_benchmark_sources.py +275 -0
- sphynx/_speech_complete_scoring.py +218 -0
- sphynx/_speech_complete_source.py +118 -0
- sphynx/_speech_kido.py +294 -0
- sphynx/_speech_scoring.py +77 -0
- sphynx/_track_capture.py +213 -0
- sphynx/analytical_views.py +515 -0
- sphynx/assessment_contracts.py +378 -0
- sphynx/assessment_selection.py +57 -0
- sphynx/audit_access.py +245 -0
- sphynx/audit_contracts.py +204 -0
- sphynx/audit_requests.py +180 -0
- sphynx/authoring.py +188 -0
- sphynx/benchmarks.py +568 -0
- sphynx/candidates.py +62 -0
- sphynx/catalog.py +272 -0
- sphynx/chunking.py +372 -0
- sphynx/clickhouse_reader.py +169 -0
- sphynx/client.py +486 -0
- sphynx/comparison_core.py +149 -0
- sphynx/comparison_views.py +223 -0
- sphynx/components.py +1661 -0
- sphynx/contracts.py +514 -0
- sphynx/customer_rerank.py +299 -0
- sphynx/customer_retrieval.py +146 -0
- sphynx/data/benchmark-releases.v1.json +164 -0
- sphynx/data/public-benchmark-release.v1.json +354 -0
- sphynx/data/rag-diagnostics-synthetic.v1.json +6982 -0
- sphynx/data/rag-paper-references.v1.json +176 -0
- sphynx/data/rag-paper-schemas.v1.json +1069 -0
- sphynx/data/rag-rerank-v1.json +507 -0
- sphynx/dataset_catalog_data.py +1214 -0
- sphynx/dataset_readiness.py +138 -0
- sphynx/dataset_v3.py +999 -0
- sphynx/demo.py +130 -0
- sphynx/demo_candidates/__init__.py +1 -0
- sphynx/demo_candidates/dense.py +11 -0
- sphynx/demo_candidates/lexical.py +21 -0
- sphynx/diagnostics.py +207 -0
- sphynx/documentation.py +737 -0
- sphynx/documentation_candidate.py +939 -0
- sphynx/evals.py +538 -0
- sphynx/evaluation_protocols.py +124 -0
- sphynx/evaluator_bindings.py +370 -0
- sphynx/evidence.py +352 -0
- sphynx/execution_capabilities.py +109 -0
- sphynx/fast_search.py +285 -0
- sphynx/full_rag.py +341 -0
- sphynx/hosted/__init__.py +18 -0
- sphynx/hosted/benchmark_intake.py +218 -0
- sphynx/hosted/contracts.py +361 -0
- sphynx/hosted/model_admission.py +130 -0
- sphynx/hosted/model_artifacts.py +298 -0
- sphynx/hosted/model_contracts.py +63 -0
- sphynx/hosted/model_gcs.py +181 -0
- sphynx/hosted/model_history.py +84 -0
- sphynx/hosted/model_session.py +233 -0
- sphynx/hosted/public_suite.py +74 -0
- sphynx/hosted/registry.py +97 -0
- sphynx/hosted/revisions.py +126 -0
- sphynx/hosted/sandbox.py +105 -0
- sphynx/hosted/submission.py +287 -0
- sphynx/hosted_workflow.py +451 -0
- sphynx/http_transport.py +86 -0
- sphynx/integrated_rag.py +352 -0
- sphynx/integrated_workflow.py +552 -0
- sphynx/integration_contracts.py +474 -0
- sphynx/invocation.py +791 -0
- sphynx/invocation_candidates.py +392 -0
- sphynx/invocation_catalog_data.py +40 -0
- sphynx/learned_catalog_data.py +30 -0
- sphynx/learned_rerank.py +267 -0
- sphynx/legacy_full_rag.py +67 -0
- sphynx/lifecycle.py +885 -0
- sphynx/lifecycle_workflow.py +490 -0
- sphynx/metric_views.py +474 -0
- sphynx/metrics.py +282 -0
- sphynx/model_adapters.py +438 -0
- sphynx/model_catalog_data.py +30 -0
- sphynx/official_benchmarks.py +385 -0
- sphynx/packs.py +258 -0
- sphynx/pipeline.py +313 -0
- sphynx/pipeline_workflow.py +551 -0
- sphynx/pr_evaluations.py +560 -0
- sphynx/progress.py +89 -0
- sphynx/public_bundle.py +173 -0
- sphynx/public_eval.py +62 -0
- sphynx/public_rerank.py +355 -0
- sphynx/py.typed +0 -0
- sphynx/pytest_plugin.py +62 -0
- sphynx/question_robustness.py +437 -0
- sphynx/rag.py +329 -0
- sphynx/rag_diagnostics.py +309 -0
- sphynx/rag_papers/__init__.py +29 -0
- sphynx/rag_papers/__main__.py +45 -0
- sphynx/rag_papers/candidates.py +19 -0
- sphynx/rag_papers/contracts.py +282 -0
- sphynx/rag_papers/datasets.py +291 -0
- sphynx/rag_papers/experiments.py +116 -0
- sphynx/rag_papers/judges.py +308 -0
- sphynx/rag_papers/registry.py +33 -0
- sphynx/rag_papers/runner.py +176 -0
- sphynx/rag_papers/sable.py +135 -0
- sphynx/rag_papers/schemas.py +12 -0
- sphynx/rag_papers/scoring.py +249 -0
- sphynx/rag_papers/semantic.py +288 -0
- sphynx/rag_papers/trec.py +80 -0
- sphynx/recorded_scores.py +352 -0
- sphynx/records.py +124 -0
- sphynx/reflex_traces.py +193 -0
- sphynx/report_rendering.py +71 -0
- sphynx/rerank_family_contracts.py +150 -0
- sphynx/rerank_workflow.py +424 -0
- sphynx/research_candidate.py +190 -0
- sphynx/results.py +852 -0
- sphynx/retrieval.py +750 -0
- sphynx/retrieval_candidates.py +295 -0
- sphynx/retrieval_catalog_data.py +19 -0
- sphynx/retrieval_recordings.py +125 -0
- sphynx/reviewer_audit.py +266 -0
- sphynx/routing_policies.py +375 -0
- sphynx/runner.py +544 -0
- sphynx/sable_rerank.py +489 -0
- sphynx/score_reliability.py +393 -0
- sphynx/speech_benchmarks.py +204 -0
- sphynx/speech_campaigns.py +378 -0
- sphynx/speech_complete.py +236 -0
- sphynx/speech_contracts.py +225 -0
- sphynx/speech_evidence.py +429 -0
- sphynx/speech_listening.py +115 -0
- sphynx/speech_multilingual.py +71 -0
- sphynx/speech_tts.py +479 -0
- sphynx/sql/001_sync_v1.sql +30 -0
- sphynx/stage_contracts.py +199 -0
- sphynx/statistical_catalog_data.py +53 -0
- sphynx/statistical_rerank.py +232 -0
- sphynx/sync.py +797 -0
- sphynx/workbench.py +378 -0
- sphynx/workbench_catalog_data.py +13 -0
- sphynx/workbench_tracks.py +78 -0
- sphynx/workflow_async.py +197 -0
- sphynx/workflow_contracts.py +594 -0
- sphynx/workflow_decisions.py +211 -0
- sphynx/workflow_defaults.py +16 -0
- sphynx/workflow_extensions.py +212 -0
- sphynx/workflow_registry.py +513 -0
- sphynx/workflows.py +792 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Pinned native dataset intake and shared-corpus bundles, without inference."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import bz2
|
|
5
|
+
import copy
|
|
6
|
+
import hashlib
|
|
7
|
+
import io
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import zipfile
|
|
12
|
+
|
|
13
|
+
from .contracts import (MAX_BYTES, canonical, checksum, digest, identity, make_case,
|
|
14
|
+
parse, require, validate_case)
|
|
15
|
+
|
|
16
|
+
BUNDLE_SCHEMA = 'sphynx.rag.paper-dataset.v1'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _read(path, expected_sha256):
|
|
20
|
+
path = Path(path)
|
|
21
|
+
require(path.is_file() and not path.is_symlink() and path.stat().st_size <= MAX_BYTES,
|
|
22
|
+
'Expected a bounded regular source file')
|
|
23
|
+
raw = path.read_bytes()
|
|
24
|
+
require(hashlib.sha256(raw).hexdigest() == checksum(expected_sha256), 'Source checksum differs')
|
|
25
|
+
return raw
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _records(raw):
|
|
29
|
+
if raw[:3] == b'BZh':
|
|
30
|
+
decompressor = bz2.BZ2Decompressor()
|
|
31
|
+
raw = decompressor.decompress(raw, max_length=MAX_BYTES+1)
|
|
32
|
+
require(decompressor.eof and len(raw) <= MAX_BYTES and not decompressor.unused_data,
|
|
33
|
+
'Compressed source exceeds bounds or contains concatenated streams')
|
|
34
|
+
try:
|
|
35
|
+
return parse(raw)
|
|
36
|
+
except ValueError:
|
|
37
|
+
return [parse(line) for line in raw.splitlines() if line.strip()]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _document(corpus, text, *, title='', key=None):
|
|
41
|
+
require(type(text) is str and type(title) is str, 'Document must have exact text/title')
|
|
42
|
+
key = identity(key or 'doc-'+digest({'text':text,'title':title}))
|
|
43
|
+
row = dict(id=key, title=title, text=text)
|
|
44
|
+
require(key not in corpus or corpus[key] == row, 'Conflicting document bytes under one ID')
|
|
45
|
+
corpus[key] = row
|
|
46
|
+
return key
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _write_bundle(corpus, cases, output_dir, provenance, source=None):
|
|
50
|
+
output = Path(output_dir)
|
|
51
|
+
require(not output.exists(), 'Choose a new dataset output directory')
|
|
52
|
+
output.mkdir(parents=True, exist_ok=False)
|
|
53
|
+
files = {}
|
|
54
|
+
def write(name, rows):
|
|
55
|
+
with (output/name).open('xb') as handle:
|
|
56
|
+
if isinstance(rows, bytes):
|
|
57
|
+
handle.write(rows)
|
|
58
|
+
else:
|
|
59
|
+
for row in rows:
|
|
60
|
+
handle.write(canonical(row)+b'\n')
|
|
61
|
+
raw=(output/name).read_bytes()
|
|
62
|
+
require(len(raw)<=MAX_BYTES, 'Dataset file exceeds local byte limit')
|
|
63
|
+
files[name]=dict(sha256=hashlib.sha256(raw).hexdigest(),bytes=len(raw))
|
|
64
|
+
write('corpus.jsonl', corpus.values())
|
|
65
|
+
case_ids=set(); count=0
|
|
66
|
+
def checked():
|
|
67
|
+
nonlocal count
|
|
68
|
+
for case in cases:
|
|
69
|
+
validate_case(case, corpus)
|
|
70
|
+
require(case['id'] not in case_ids, 'Duplicate case ID')
|
|
71
|
+
case_ids.add(case['id']);count+=1
|
|
72
|
+
yield case
|
|
73
|
+
write('cases.jsonl', checked())
|
|
74
|
+
require(count>0, 'Empty benchmark population')
|
|
75
|
+
if source is not None:
|
|
76
|
+
write('source.bin', source)
|
|
77
|
+
manifest=dict(schema_version=BUNDLE_SCHEMA, files=files,
|
|
78
|
+
counts=dict(documents=len(corpus),cases=count), provenance=provenance,
|
|
79
|
+
execution=dict(models_run=False,hosted_admitted=False),
|
|
80
|
+
reference_policy='Native labels are protected annotations, not independent judgments of candidate outputs.')
|
|
81
|
+
(output/'manifest.json').write_bytes(canonical(manifest)+b'\n')
|
|
82
|
+
return dict(path=str(output.resolve()), manifest_sha256=hashlib.sha256((output/'manifest.json').read_bytes()).hexdigest(),
|
|
83
|
+
**manifest['counts'])
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class PaperDataset:
|
|
87
|
+
"""Verified shared corpus with a streaming, complete case population."""
|
|
88
|
+
def __init__(self, root, manifest, corpus):
|
|
89
|
+
self.path,self.manifest,self.corpus=root,manifest,corpus
|
|
90
|
+
self._identity = digest(dict(manifest=manifest, corpus=corpus))
|
|
91
|
+
self._manifest_sha256=hashlib.sha256((root/'manifest.json').read_bytes()).hexdigest()
|
|
92
|
+
|
|
93
|
+
def cases(self):
|
|
94
|
+
_read(self.path/'manifest.json',self._manifest_sha256)
|
|
95
|
+
_read(self.path/'corpus.jsonl',self.manifest['files']['corpus.jsonl']['sha256'])
|
|
96
|
+
require(digest(dict(manifest=self.manifest, corpus=self.corpus)) == self._identity,
|
|
97
|
+
'Loaded dataset was mutated; reopen the pinned dataset')
|
|
98
|
+
# Reverify on each pass, including when callers change a file after load.
|
|
99
|
+
raw=_read(self.path/'cases.jsonl',self.manifest['files']['cases.jsonl']['sha256'])
|
|
100
|
+
seen=set()
|
|
101
|
+
for line in raw.splitlines():
|
|
102
|
+
if not line.strip():continue
|
|
103
|
+
case=validate_case(parse(line),self.corpus)
|
|
104
|
+
require(case['id'] not in seen, 'Duplicate saved case ID')
|
|
105
|
+
seen.add(case['id']);yield case
|
|
106
|
+
require(len(seen)==self.manifest['counts']['cases'], 'Saved case census differs')
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def load_paper_dataset(path, *, expected_manifest_sha256):
|
|
110
|
+
root=Path(path)
|
|
111
|
+
manifest=parse(_read(root/'manifest.json',expected_manifest_sha256))
|
|
112
|
+
require(manifest.get('schema_version')==BUNDLE_SCHEMA, 'Unknown paper dataset schema')
|
|
113
|
+
require(set(manifest.get('files',{})) in ({'corpus.jsonl','cases.jsonl'},
|
|
114
|
+
{'corpus.jsonl','cases.jsonl','source.bin'}), 'Unexpected dataset file closure')
|
|
115
|
+
contents={}
|
|
116
|
+
for name,receipt in manifest['files'].items():
|
|
117
|
+
raw=_read(root/name,receipt['sha256'])
|
|
118
|
+
require(len(raw)==receipt['bytes'], 'Saved byte census differs')
|
|
119
|
+
if name!='source.bin':contents[name]=raw
|
|
120
|
+
corpus={}
|
|
121
|
+
for line in contents['corpus.jsonl'].splitlines():
|
|
122
|
+
if line.strip():
|
|
123
|
+
row=parse(line)
|
|
124
|
+
require(type(row) is dict and set(row)=={'id','title','text'}, 'Saved document fields differ')
|
|
125
|
+
require(row['id'] not in corpus,'Duplicate saved document ID')
|
|
126
|
+
_document(corpus,row['text'],title=row['title'],key=row['id'])
|
|
127
|
+
require(len(corpus)==manifest['counts']['documents'],'Saved corpus census differs')
|
|
128
|
+
dataset=PaperDataset(root,manifest,corpus)
|
|
129
|
+
# Consume validation without retaining a second copy of all cases.
|
|
130
|
+
for _ in dataset.cases():pass
|
|
131
|
+
return dataset
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def prepare_canonical_paper_dataset(cases, corpus, output_dir, *, expected_input_sha256, source_id):
|
|
135
|
+
"""Freeze explicit canonical objects, with their corpus and case bytes pinned.
|
|
136
|
+
|
|
137
|
+
This records a caller's declared local provenance; it grants no data rights
|
|
138
|
+
and does not promote model or synthetic annotations into human reviews.
|
|
139
|
+
"""
|
|
140
|
+
require(type(cases) is list and type(corpus) is dict,'Canonical cases and keyed corpus required')
|
|
141
|
+
require(digest(dict(cases=cases,corpus=corpus))==checksum(expected_input_sha256),'Canonical input checksum differs')
|
|
142
|
+
identity(source_id)
|
|
143
|
+
return _write_bundle(copy.deepcopy(corpus),copy.deepcopy(cases),output_dir,
|
|
144
|
+
dict(kind='canonical-objects',source_id=source_id,input_sha256=expected_input_sha256))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def prepare_official_paper_dataset(source_path, output_dir, *, expected_manifest_sha256):
|
|
148
|
+
"""Bridge the existing complete SciFact/Hotpot bundles without rerunning intake."""
|
|
149
|
+
from ..official_benchmarks import load_official_benchmark
|
|
150
|
+
old=load_official_benchmark(source_path,expected_manifest_sha256=expected_manifest_sha256)
|
|
151
|
+
corpus={d:dict(id=d,title=r.get('title',''),text=r['text']) for d,r in old.corpus.items()}
|
|
152
|
+
def cases():
|
|
153
|
+
for qid,query in old.queries.items():
|
|
154
|
+
native=old.annotations.get(qid,{})
|
|
155
|
+
refs={'qrels':old.qrels[qid], 'native':copy.deepcopy(native)}
|
|
156
|
+
if native:
|
|
157
|
+
refs['answers']=[native['answer']] if type(native.get('answer')) is str else None
|
|
158
|
+
refs['support_facts']=native.get('supporting_facts') if not native.get('validation_issues') else None
|
|
159
|
+
yield make_case(qid,query['text'],query.get('context_ids',list(corpus)),references=refs,
|
|
160
|
+
snapshot=old.manifest['source_sha256'],origin='public',family=old.manifest['dataset_id'],
|
|
161
|
+
split=old.manifest['upstream']['split'],source_id=old.manifest['dataset_id'])
|
|
162
|
+
return _write_bundle(corpus,cases(),output_dir,dict(kind='existing-verified-official-bundle',
|
|
163
|
+
source_manifest_sha256=expected_manifest_sha256,dataset_id=old.manifest['dataset_id']))
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def prepare_paper_dataset(paper, source_path, output_dir, *, expected_sha256,
|
|
167
|
+
source_url=None, split='development', scenario=None):
|
|
168
|
+
"""Read explicit native JSON/JSONL/BZ2 or BEIR ZIP; preserve all native rows.
|
|
169
|
+
|
|
170
|
+
A caller pin establishes exact bytes, not independently authenticated origin,
|
|
171
|
+
license rights or customer authorization. No directory discovery/network.
|
|
172
|
+
"""
|
|
173
|
+
raw=_read(source_path,expected_sha256)
|
|
174
|
+
corpus={}; cases=[]
|
|
175
|
+
def case(cid,question,docs,refs,**extra):
|
|
176
|
+
cases.append(make_case(identity(str(cid)),question,docs,references=refs,
|
|
177
|
+
snapshot=expected_sha256,origin='public',family=scenario or paper,split=split,
|
|
178
|
+
source_id=paper,**extra))
|
|
179
|
+
if paper=='beir':
|
|
180
|
+
with zipfile.ZipFile(io.BytesIO(raw)) as archive:
|
|
181
|
+
require(sum(i.file_size for i in archive.infolist())<=MAX_BYTES,'Expanded BEIR ZIP exceeds bound')
|
|
182
|
+
roots=[n[:-len('corpus.jsonl')] for n in archive.namelist() if n.endswith('corpus.jsonl')]
|
|
183
|
+
require(len(roots)==1,'BEIR archive needs exactly one corpus')
|
|
184
|
+
prefix=roots[0]
|
|
185
|
+
require(len(archive.namelist())==len(set(archive.namelist())),'Duplicate ZIP member')
|
|
186
|
+
documents=[parse(x) for x in archive.read(prefix+'corpus.jsonl').splitlines() if x.strip()]
|
|
187
|
+
queries=[parse(x) for x in archive.read(prefix+'queries.jsonl').splitlines() if x.strip()]
|
|
188
|
+
for row in documents:_document(corpus,row['text'],title=row.get('title',''),key=row['_id'])
|
|
189
|
+
qrels={}
|
|
190
|
+
for line in archive.read(prefix+'qrels/'+split+'.tsv').decode('utf-8').splitlines():
|
|
191
|
+
if not line.strip() or line.startswith('query-id\t'):continue
|
|
192
|
+
q,d,g=line.split('\t'); require(d in corpus,'BEIR qrel document absent')
|
|
193
|
+
require(d not in qrels.setdefault(q,{}),'Duplicate native qrel')
|
|
194
|
+
qrels[q][d]=int(g)
|
|
195
|
+
querymap={q['_id']:q for q in queries};require(len(querymap)==len(queries),'Duplicate native query')
|
|
196
|
+
require(set(qrels)<=set(querymap),'Qrel query absent')
|
|
197
|
+
for qid,refs in qrels.items():case(qid,querymap[qid]['text'],list(corpus),{'qrels':refs,'native':{'query':querymap[qid]}})
|
|
198
|
+
else:
|
|
199
|
+
data=_records(raw)
|
|
200
|
+
if paper=='qasper':
|
|
201
|
+
require(type(data) is dict,'QASPER source must map paper IDs')
|
|
202
|
+
for pid,paperrow in data.items():
|
|
203
|
+
docs=[]; by_text={}
|
|
204
|
+
for section in paperrow['full_text']:
|
|
205
|
+
for text in section['paragraphs']:
|
|
206
|
+
did=_document(corpus,text,title=section.get('section_name',''),key=pid+':'+digest([text,section.get('section_name','')]))
|
|
207
|
+
if did not in docs:docs.append(did)
|
|
208
|
+
by_text[text]=did
|
|
209
|
+
for qa in paperrow['qas']:
|
|
210
|
+
annotations=[]
|
|
211
|
+
for row in qa['answers']:
|
|
212
|
+
a=row['answer']
|
|
213
|
+
if a['unanswerable']:answer='Unanswerable'; evidence=[]
|
|
214
|
+
else:
|
|
215
|
+
answer=', '.join(a['extractive_spans']) if a.get('extractive_spans') else a.get('free_form_answer')
|
|
216
|
+
if not answer:
|
|
217
|
+
require(type(a.get('yes_no')) is bool,'QASPER answer has no recognized form')
|
|
218
|
+
answer='Yes' if a['yes_no'] else 'No'
|
|
219
|
+
evidence=[e for e in a['evidence'] if 'FLOAT SELECTED' not in e]
|
|
220
|
+
annotations.append(dict(answer=answer,evidence_ids=[by_text[e] for e in evidence]
|
|
221
|
+
if all(e in by_text for e in evidence) else None))
|
|
222
|
+
case(qa['question_id'],qa['question'],docs,{'annotation_sets':annotations,'native':qa},cluster_id=pid)
|
|
223
|
+
else:
|
|
224
|
+
if type(data) is dict:data=[data] # One JSONL row is also a valid JSON object.
|
|
225
|
+
require(type(data) is list,'Native source must be a JSON array or JSONL records')
|
|
226
|
+
for index,row in enumerate(data):
|
|
227
|
+
cid=str(row.get('id',row.get('_id',row.get('interaction_id',index))))
|
|
228
|
+
if paper=='hotpotqa':
|
|
229
|
+
docs=[];titles={}; issues=[]
|
|
230
|
+
for title,sentences in row['context']:
|
|
231
|
+
did=_document(corpus,''.join(sentences),title=title,key=cid+':'+digest(title))
|
|
232
|
+
docs.append(did);titles[title]=(did,len(sentences))
|
|
233
|
+
for title,sentence in row['supporting_facts']:
|
|
234
|
+
if title not in titles or type(sentence) is not int or not 0<=sentence<titles[title][1]:issues.append([title,sentence])
|
|
235
|
+
support=None if issues else row['supporting_facts']
|
|
236
|
+
case(cid,row['question'],docs,{'answers':[row['answer']],'support_facts':support,
|
|
237
|
+
'support_ids':None if issues else list(dict.fromkeys(titles[t][0] for t,_ in support)),
|
|
238
|
+
'native':row})
|
|
239
|
+
elif paper=='musique':
|
|
240
|
+
docs=[];support=[];by_index={}
|
|
241
|
+
for p in row['paragraphs']:
|
|
242
|
+
did=_document(corpus,p['paragraph_text'],title=p.get('title',''),key=cid+':'+str(p['idx']))
|
|
243
|
+
docs.append(did)
|
|
244
|
+
by_index[p['idx']]=did
|
|
245
|
+
if p['is_supporting']:support.append(did)
|
|
246
|
+
answers=list(dict.fromkeys([row['answer']]+row.get('answer_aliases',[])))
|
|
247
|
+
hops=row.get('question_decomposition',[])
|
|
248
|
+
known=bool(hops) and all(h.get('paragraph_support_idx') in by_index for h in hops)
|
|
249
|
+
atom_support={str(h['id']):[by_index[h['paragraph_support_idx']]] for h in hops} if known else None
|
|
250
|
+
relations={}
|
|
251
|
+
if known:
|
|
252
|
+
for hop in hops:
|
|
253
|
+
for dependency in re.findall(r'#(\d+)',hop['question']):
|
|
254
|
+
require(dependency in atom_support,'MuSiQue dependency points to absent hop')
|
|
255
|
+
relations[dependency+'->'+str(hop['id'])]=list(dict.fromkeys(atom_support[dependency]+atom_support[str(hop['id'])]))
|
|
256
|
+
case(cid,row['question'],docs,{'answers':answers,'support_ids':support,
|
|
257
|
+
'answerable':row['answerable'],'native':row,
|
|
258
|
+
'required_atoms':list(atom_support) if known else None,'atom_support':atom_support,
|
|
259
|
+
'required_relations':list(relations) if known else None,'relation_support':relations if known else None})
|
|
260
|
+
elif paper=='alce':
|
|
261
|
+
docs=[_document(corpus,d.get('text',d.get('sent','')),title=d.get('title',''),key=cid+':'+str(i+1)) for i,d in enumerate(row['docs'])]
|
|
262
|
+
native=copy.deepcopy(row)
|
|
263
|
+
if row.get('qa_pairs'):native['asqa_short_answers']=[q['short_answers'] for q in row['qa_pairs']]
|
|
264
|
+
if row.get('answers'):native['qampari_answers']=row['answers']
|
|
265
|
+
case(cid,row['question'],docs,{'native':native})
|
|
266
|
+
elif paper=='rgb':
|
|
267
|
+
positives=row.get('positive',[]);negatives=row.get('negative',[])
|
|
268
|
+
require(type(positives) is list and type(negatives) is list,'RGB context lists required')
|
|
269
|
+
positive_groups=positives if positives and type(positives[0]) is list else [positives]
|
|
270
|
+
positive_ids=[[_document(corpus,p,key=cid+':p:'+str(g)+':'+str(i)) for i,p in enumerate(group)]
|
|
271
|
+
for g,group in enumerate(positive_groups)]
|
|
272
|
+
negative_ids=[_document(corpus,p,key=cid+':n:'+str(i)) for i,p in enumerate(negatives)]
|
|
273
|
+
wrong_ids=[_document(corpus,p,key=cid+':w:'+str(i)) for i,p in enumerate(row.get('positive_wrong',[]))]
|
|
274
|
+
docs=[d for group in positive_ids for d in group]+negative_ids+wrong_ids
|
|
275
|
+
answers=row['answer']; groups=[[answers]] if type(answers) is str else [
|
|
276
|
+
a if type(a) is list else [a] for a in answers]
|
|
277
|
+
native={**row,'answer_groups':groups,'positive_ids':[d for group in positive_ids for d in group],
|
|
278
|
+
'positive_groups':positive_ids,'negative_ids':negative_ids,'wrong_ids':wrong_ids}
|
|
279
|
+
case(cid,row.get('query',row.get('question')),docs,{'native':native})
|
|
280
|
+
elif paper=='crag':
|
|
281
|
+
docs=[_document(corpus,d.get('page_result',d.get('page_snippet','')),title=d.get('page_name',''),key=cid+':'+str(i)) for i,d in enumerate(row['search_results'])]
|
|
282
|
+
case(cid,row['query'],docs,{'answers':[row['answer']],'native':row},context={'as_of':row.get('query_time','')})
|
|
283
|
+
elif paper=='lost_in_the_middle':
|
|
284
|
+
docs=[_document(corpus,d['text'],title=d.get('title',''),key=cid+':'+str(i)) for i,d in enumerate(row['ctxs'])]
|
|
285
|
+
support=[did for did,d in zip(docs,row['ctxs']) if d.get('hasanswer') is True]
|
|
286
|
+
case(cid,row['question'],docs,{'answers':row['answers'],'support_ids':support,'native':row})
|
|
287
|
+
else:
|
|
288
|
+
require(False,'No native loader for this paper; use canonical case/judgment inputs')
|
|
289
|
+
provenance=dict(paper=paper,source_sha256=expected_sha256,source_url=source_url,split=split,
|
|
290
|
+
origin_basis='caller-pinned native bytes; preserve independent publisher/license receipt')
|
|
291
|
+
return _write_bundle(corpus,cases,output_dir,provenance,source=raw)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Frozen controlled interventions, separate from ordinary benchmark grading."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import copy
|
|
4
|
+
import math
|
|
5
|
+
import random
|
|
6
|
+
|
|
7
|
+
from .contracts import candidate_input, digest, require, validate_case
|
|
8
|
+
from .datasets import _write_bundle
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def plan_lost_in_the_middle(case, corpus, *, evidence_id, positions, seed,
|
|
12
|
+
render_prompt, count_tokens, tokenizer_revision, max_tokens):
|
|
13
|
+
"""Move one evidence document through the same frozen distractor population.
|
|
14
|
+
|
|
15
|
+
The caller supplies its actual prompt renderer and tokenizer. Every prompt
|
|
16
|
+
must fit the same declared budget; no text is truncated or resampled. Exact
|
|
17
|
+
text hashes, token counts and randomized execution order are retained.
|
|
18
|
+
"""
|
|
19
|
+
validate_case(case,corpus)
|
|
20
|
+
require(evidence_id in case['document_ids'] and type(positions) is list and positions
|
|
21
|
+
and len(set(positions))==len(positions) and all(type(p) is int and 0<=p<len(case['document_ids']) for p in positions),
|
|
22
|
+
'Invalid evidence ID or zero-based positions')
|
|
23
|
+
require(type(seed) is int and callable(render_prompt) and callable(count_tokens)
|
|
24
|
+
and type(tokenizer_revision) is str and tokenizer_revision
|
|
25
|
+
and type(max_tokens) is int and max_tokens>0, 'Explicit prompt/tokenizer and budget required')
|
|
26
|
+
others=[d for d in case['document_ids'] if d!=evidence_id]
|
|
27
|
+
random.Random(seed).shuffle(others)
|
|
28
|
+
trials=[]
|
|
29
|
+
for position in positions:
|
|
30
|
+
c=copy.deepcopy(case); order=list(others);order.insert(position,evidence_id)
|
|
31
|
+
c['id']='litm-'+digest([case['id'],position,seed])[:32]; c['document_ids']=order
|
|
32
|
+
c['metadata']['scenario_family']='lost_in_the_middle.position'
|
|
33
|
+
prompt=render_prompt(candidate_input(c,corpus))
|
|
34
|
+
require(type(prompt) is str,'Prompt renderer must return exact text')
|
|
35
|
+
tokens=count_tokens(prompt)
|
|
36
|
+
require(type(tokens) is int and 0<tokens<=max_tokens,'Position trial exceeds token budget')
|
|
37
|
+
trials.append(dict(case=validate_case(c,corpus),position=position,prompt_sha256=digest(prompt),tokens=tokens))
|
|
38
|
+
random.Random(seed+1).shuffle(trials)
|
|
39
|
+
return dict(schema_version='sphynx.rag.position-plan.v1', method='lost_in_the_middle',
|
|
40
|
+
source_case_sha256=digest(case), corpus_sha256=digest({d:corpus[d] for d in case['document_ids']}),
|
|
41
|
+
evidence_id=evidence_id,seed=seed,tokenizer_revision=tokenizer_revision,max_tokens=max_tokens,
|
|
42
|
+
fixed_distractor_order=others,trials=trials,models_run=False)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def plan_rgb(case, corpus, *, testbed, passage_count=5, noise_rate=0, correct_rate=0, seed=0):
|
|
46
|
+
"""Freeze RGB testbed selections and shuffle seed, failing on missing inputs.
|
|
47
|
+
|
|
48
|
+
Selection conventions are explicit Sphynx variants. The original scripts
|
|
49
|
+
also contain fallback sampling behavior; this planner refuses to silently
|
|
50
|
+
change a requested experimental condition when a pool is too small.
|
|
51
|
+
"""
|
|
52
|
+
validate_case(case,corpus)
|
|
53
|
+
require(testbed in ('noise','rejection','integration','counterfactual'), 'Unknown RGB testbed')
|
|
54
|
+
require(type(passage_count) is int and passage_count>0 and type(seed) is int,'Invalid RGB trial size/seed')
|
|
55
|
+
for rate in (noise_rate,correct_rate):require(type(rate) in (int,float) and math.isfinite(rate) and 0<=rate<=1,'Invalid RGB rate')
|
|
56
|
+
n=case['references']['native']; rng=random.Random(seed)
|
|
57
|
+
positives=n.get('positive_ids',[]); negatives=n.get('negative_ids',[])
|
|
58
|
+
negative_count=passage_count if testbed=='rejection' else math.ceil(passage_count*noise_rate)
|
|
59
|
+
require(negative_count<=len(negatives),'Insufficient frozen negative documents')
|
|
60
|
+
selected=list(negatives[:negative_count]); positive_count=passage_count-negative_count
|
|
61
|
+
if testbed=='integration':
|
|
62
|
+
groups=copy.deepcopy(n.get('positive_groups',[]))
|
|
63
|
+
require(groups and all(groups) and positive_count>=len(groups),'Integration needs evidence from every group')
|
|
64
|
+
for group in groups:rng.shuffle(group)
|
|
65
|
+
round_robin=[g[i] for i in range(max(map(len,groups))) for g in groups if i<len(g)]
|
|
66
|
+
require(positive_count<=len(round_robin),'Insufficient integration evidence')
|
|
67
|
+
selected+=round_robin[:positive_count]
|
|
68
|
+
elif testbed=='counterfactual':
|
|
69
|
+
correct_count=math.ceil(passage_count*correct_rate); wrong_count=positive_count-correct_count
|
|
70
|
+
wrong=n.get('wrong_ids',[])
|
|
71
|
+
require(0<=wrong_count and len(wrong)==len(positives) and len(wrong)>=positive_count,
|
|
72
|
+
'Counterfactual evidence must align with native positive documents')
|
|
73
|
+
indices=rng.sample(range(len(positives)),positive_count)
|
|
74
|
+
selected += [wrong[i] for i in indices[:wrong_count]]+[positives[i] for i in indices[wrong_count:]]
|
|
75
|
+
else:
|
|
76
|
+
require(positive_count<=len(positives),'Insufficient frozen positive documents')
|
|
77
|
+
selected+=positives[:positive_count]
|
|
78
|
+
require(len(set(selected))==passage_count and set(selected)<=set(case['document_ids']),'RGB selection is incomplete')
|
|
79
|
+
rng.shuffle(selected); c=copy.deepcopy(case)
|
|
80
|
+
c['id']='rgb-'+digest([case['id'],testbed,selected,seed])[:32];c['document_ids']=selected
|
|
81
|
+
c['metadata']['scenario_family']='rgb.'+testbed
|
|
82
|
+
return dict(schema_version='sphynx.rag.rgb-plan.v1', method='rgb', source_case_sha256=digest(case),
|
|
83
|
+
testbed=testbed,passage_count=passage_count,noise_rate=1 if testbed=='rejection' else noise_rate,
|
|
84
|
+
correct_rate=correct_rate,seed=seed,case=validate_case(c,corpus),models_run=False)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def prepare_intervention_dataset(plan, corpus, output_dir):
|
|
88
|
+
"""Materialize a plan for capture_paper_candidate followed by run_paper_suite."""
|
|
89
|
+
require(plan.get('schema_version') in ('sphynx.rag.position-plan.v1','sphynx.rag.rgb-plan.v1','sphynx.rag.musique-plan.v1'),'Unknown intervention plan')
|
|
90
|
+
cases=[t['case'] for t in plan['trials']] if 'trials' in plan else [plan['case']]
|
|
91
|
+
return _write_bundle(corpus,cases,output_dir,dict(kind='controlled-intervention',plan=copy.deepcopy(plan),plan_sha256=digest(plan)))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def plan_musique(case, corpus, *, seed=0):
|
|
95
|
+
"""Execute full-context and leave-one-hop-evidence-out connected-QA trials.
|
|
96
|
+
|
|
97
|
+
Removed annotated evidence does not prove remaining context insufficient:
|
|
98
|
+
redundant support and model prior knowledge need separate interpretation.
|
|
99
|
+
Source answerability and original gold stay intact across interventions.
|
|
100
|
+
"""
|
|
101
|
+
validate_case(case,corpus)
|
|
102
|
+
support=case['references']['atom_support'];required=case['references']['required_atoms']
|
|
103
|
+
require(type(seed) is int and required and support and all(a in support and support[a] for a in required),
|
|
104
|
+
'Frozen required-hop evidence annotations are needed')
|
|
105
|
+
trials=[]
|
|
106
|
+
for atom in [None]+required:
|
|
107
|
+
c=copy.deepcopy(case)
|
|
108
|
+
removed=[] if atom is None else support[atom]
|
|
109
|
+
c['document_ids']=[d for d in case['document_ids'] if d not in removed]
|
|
110
|
+
c['id']='musique-'+digest([case['id'],atom,seed])[:32]
|
|
111
|
+
c['metadata']['scenario_family']='musique.connected.full' if atom is None else 'musique.connected.ablation'
|
|
112
|
+
if atom is not None:c['references']['expected_action']=None
|
|
113
|
+
trials.append(dict(case=validate_case(c,corpus),removed_hop=atom,removed_ids=removed))
|
|
114
|
+
random.Random(seed).shuffle(trials)
|
|
115
|
+
return dict(schema_version='sphynx.rag.musique-plan.v1',method='musique',source_case_sha256=digest(case),
|
|
116
|
+
seed=seed,trials=trials,models_run=False,insufficiency_inferred_from_removal=False)
|