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,29 @@
1
+ """Named research protocols for the separate, local rag.papers.v1 suite.
2
+
3
+ Import specific paper names to make a call's evaluation approach visible.
4
+ These SDK 0.5.0 submodule APIs preserve the existing root exports and do not imply
5
+ official-framework parity, human calibration, or hosted-suite admission.
6
+ """
7
+ from .contracts import (PaperEvaluationError, make_case, make_trace, judgment,
8
+ candidate_input, validate_case, validate_trace, validate_judgment)
9
+ from .datasets import (PaperDataset, prepare_paper_dataset, prepare_official_paper_dataset,
10
+ prepare_canonical_paper_dataset, load_paper_dataset)
11
+ from .registry import describe_paper_methods
12
+ from .scoring import (evaluate_beir, evaluate_hotpotqa, evaluate_musique,
13
+ evaluate_qasper, evaluate_rgb, evaluate_crag)
14
+ from .semantic import (evaluate_sufficient_context, evaluate_ragas, evaluate_alce,
15
+ evaluate_ares, evaluate_factscore, evaluate_ragchecker,
16
+ evaluate_lost_in_the_middle, evaluate_trec_rag_2025)
17
+ from .judges import JsonJudgeAdapter, JudgeSession, run_paper_judge, prepare_ares_training, train_ares_judge
18
+ from .candidates import candidate_trace
19
+ from .runner import (run_paper_suite, load_paper_run, compare_paper_runs, capture_paper_candidate)
20
+ from .experiments import plan_lost_in_the_middle, plan_rgb, plan_musique, prepare_intervention_dataset
21
+ from .sable import (prepare_sable_paper_dataset, evaluate_sable_evidence,
22
+ bind_reflex_case_history, trace_from_level3, trace_from_full_rag_capture)
23
+ from .trec import prepare_trec_rag_2025_dataset, trace_from_trec_rag_2025
24
+ from .schemas import get_paper_schema
25
+
26
+ __all__ = [name for name in globals() if name.startswith(('evaluate_', 'prepare_', 'validate_', 'make_', 'plan_'))] + [
27
+ 'PaperEvaluationError','PaperDataset','judgment','candidate_input','describe_paper_methods','load_paper_dataset',
28
+ 'JsonJudgeAdapter','JudgeSession','run_paper_judge','run_paper_suite','load_paper_run','candidate_trace','train_ares_judge',
29
+ 'compare_paper_runs','capture_paper_candidate','bind_reflex_case_history','trace_from_level3','trace_from_full_rag_capture','trace_from_trec_rag_2025','get_paper_schema']
@@ -0,0 +1,45 @@
1
+ """A distinct invocation: python -m sphynx.rag_papers --help."""
2
+ from __future__ import annotations
3
+ import argparse
4
+ import json
5
+ from pathlib import Path
6
+
7
+ from .contracts import PaperEvaluationError, parse
8
+ from .datasets import prepare_paper_dataset, prepare_official_paper_dataset, load_paper_dataset
9
+ from .registry import describe_paper_methods
10
+ from .runner import run_paper_suite
11
+
12
+
13
+ def main(argv=None):
14
+ parser=argparse.ArgumentParser(description='Source-only rag.papers.v1; offline intake and grading, no implicit model execution')
15
+ sub=parser.add_subparsers(dest='command',required=True)
16
+ sub.add_parser('methods')
17
+ native=sub.add_parser('prepare')
18
+ native.add_argument('paper', choices=('beir','hotpotqa','musique','qasper','alce','rgb','crag','lost_in_the_middle'))
19
+ native.add_argument('source');native.add_argument('output');native.add_argument('--sha256',required=True)
20
+ native.add_argument('--source-url');native.add_argument('--split',default='development');native.add_argument('--scenario')
21
+ bridge=sub.add_parser('bridge-official');bridge.add_argument('source');bridge.add_argument('output');bridge.add_argument('--manifest-sha256',required=True)
22
+ grade=sub.add_parser('grade');grade.add_argument('dataset');grade.add_argument('traces');grade.add_argument('output')
23
+ grade.add_argument('--manifest-sha256',required=True);grade.add_argument('--methods',nargs='+',required=True)
24
+ grade.add_argument('--candidate',required=True);grade.add_argument('--source-sha256',required=True)
25
+ grade.add_argument('--judgments')
26
+ args=parser.parse_args(argv)
27
+ def lines(path):
28
+ file=Path(path)
29
+ if file.stat().st_size>200_000_000:raise PaperEvaluationError('Evidence file exceeds limit')
30
+ return [parse(line) for line in file.read_bytes().splitlines() if line.strip()]
31
+ try:
32
+ if args.command=='methods':out=describe_paper_methods()
33
+ elif args.command=='prepare':out=prepare_paper_dataset(args.paper,args.source,args.output,expected_sha256=args.sha256,
34
+ source_url=args.source_url,split=args.split,scenario=args.scenario)
35
+ elif args.command=='bridge-official':out=prepare_official_paper_dataset(args.source,args.output,expected_manifest_sha256=args.manifest_sha256)
36
+ else:
37
+ dataset=load_paper_dataset(args.dataset,expected_manifest_sha256=args.manifest_sha256)
38
+ out=run_paper_suite(dataset,lines(args.traces),args.output,methods=args.methods,
39
+ judgments=[] if not args.judgments else lines(args.judgments),candidate_id=args.candidate,source_sha256=args.source_sha256)
40
+ print(json.dumps(out,indent=2,allow_nan=False));return 0
41
+ except (ValueError,OSError,KeyError,TypeError):
42
+ parser.exit(2,'Paper evaluation failed: invalid inputs, identities or output path; inspect the pinned input contract.\n')
43
+
44
+
45
+ if __name__=='__main__':raise SystemExit(main())
@@ -0,0 +1,19 @@
1
+ """Candidate-side trace construction using only the projected public input."""
2
+ from .contracts import STAGES, TRACE_SCHEMA, digest, exact, require, validate_trace
3
+
4
+
5
+ def candidate_trace(payload, *, candidate_id, source_sha256, text='', ranked=None,
6
+ prompt=None, status='ok', abstained=False, attempt_id='attempt-1'):
7
+ """Build a return value inside a trusted candidate without receiving gold."""
8
+ exact(payload,('case_id','question','conversation_history','retrieval_query','context','corpus_snapshot_id','documents'), 'candidate input')
9
+ permitted={d['id'] for d in payload['documents']}
10
+ trace=dict(schema_version=TRACE_SCHEMA,case_id=payload['case_id'],attempt_id=attempt_id,
11
+ input_sha256=digest(payload),candidate=dict(id=candidate_id,source_sha256=source_sha256),
12
+ rewritten_query=payload['retrieval_query'],native={},stages=dict.fromkeys(STAGES),
13
+ response=dict(status=status,text=text,abstained=abstained,support_ids=None,support_facts=None),
14
+ operational=dict(tokens=None,cost_usd=None))
15
+ trace['stages'].update(retrieved=ranked,prompt=prompt)
16
+ validate_trace(trace)
17
+ for stage in trace['stages'].values():
18
+ require(stage is None or set(stage)<=permitted,'Candidate trace uses unavailable documents')
19
+ return trace
@@ -0,0 +1,282 @@
1
+ """Paper evaluation wire contracts. Native gold remains separate from inputs."""
2
+ from __future__ import annotations
3
+
4
+ import copy
5
+ import hashlib
6
+ import json
7
+ import math
8
+ from pathlib import Path
9
+
10
+ CASE_SCHEMA = 'sphynx.rag.case.v1'
11
+ TRACE_SCHEMA = 'sphynx.rag.trace.v1'
12
+ JUDGMENT_SCHEMA = 'sphynx.rag.judgment.v1'
13
+ RESULT_SCHEMA = 'sphynx.rag.paper-result.v1'
14
+ STAGES = ('indexed', 'retrieved', 'reranked', 'selected', 'prompt')
15
+ REFERENCE_FIELDS = ('answers', 'qrels', 'support_ids', 'support_facts', 'answerable',
16
+ 'expected_action', 'required_atoms', 'atom_support',
17
+ 'required_relations', 'relation_support', 'annotation_sets', 'native')
18
+ MAX_BYTES = 200_000_000
19
+
20
+
21
+ class PaperEvaluationError(ValueError):
22
+ """A paper protocol's data, identity or execution boundary is invalid."""
23
+
24
+
25
+ def require(condition, message):
26
+ if not condition:
27
+ raise PaperEvaluationError(message)
28
+
29
+
30
+ def canonical(value):
31
+ try:
32
+ return json.dumps(value, ensure_ascii=False, sort_keys=True,
33
+ separators=(',', ':'), allow_nan=False).encode('utf-8')
34
+ except (ValueError, TypeError, RecursionError):
35
+ raise PaperEvaluationError('Expected bounded finite JSON') from None
36
+
37
+
38
+ def digest(value):
39
+ return hashlib.sha256(canonical(value)).hexdigest()
40
+
41
+
42
+ def parse(raw):
43
+ require(len(raw) <= MAX_BYTES, 'JSON exceeds the local byte limit')
44
+ def pairs(items):
45
+ out = {}
46
+ for key, value in items:
47
+ require(key not in out, 'Duplicate JSON object key')
48
+ out[key] = value
49
+ return out
50
+ try:
51
+ result = json.loads(raw, object_pairs_hook=pairs,
52
+ parse_constant=lambda _: require(False, 'Nonfinite JSON value'))
53
+ canonical(result)
54
+ return result
55
+ except (ValueError, UnicodeError, RecursionError):
56
+ raise PaperEvaluationError('Invalid finite UTF-8 JSON') from None
57
+
58
+
59
+ def exact(value, fields, where):
60
+ require(type(value) is dict and set(value) == set(fields), where + ' fields differ')
61
+
62
+
63
+ def identity(value):
64
+ require(type(value) is str and 0 < len(value) <= 256
65
+ and not any(ord(c) < 32 for c in value), 'Invalid record identity')
66
+ return value
67
+
68
+
69
+ def checksum(value):
70
+ require(type(value) is str and len(value) == 64
71
+ and all(c in '0123456789abcdef' for c in value), 'Expected lowercase SHA256')
72
+ return value
73
+
74
+
75
+ def ids(values):
76
+ require(type(values) is list and len(values) <= 1_000_000, 'Expected bounded ID list')
77
+ for value in values:
78
+ identity(value)
79
+ require(len(set(values)) == len(values), 'Duplicate record identity')
80
+ return set(values)
81
+
82
+
83
+ def facts(values):
84
+ require(values is None or type(values) is list and all(type(row) is list and len(row)==2
85
+ and type(row[0]) is str and type(row[1]) is int and row[1]>=0 for row in values), 'Invalid title/sentence-index supporting facts')
86
+
87
+
88
+ def make_case(case_id, question, document_ids, *, references=None, history=(), query=None,
89
+ snapshot='unspecified', origin='synthetic', family='unspecified', split='development',
90
+ cluster_id=None, native_id=None, source_id='caller-supplied', context=None):
91
+ refs = dict.fromkeys(REFERENCE_FIELDS)
92
+ refs['native'] = {}
93
+ if references:
94
+ require(set(references) <= set(refs), 'Unknown reference fields')
95
+ refs.update(copy.deepcopy(references))
96
+ value = dict(schema_version=CASE_SCHEMA, id=case_id, question=question,
97
+ conversation_history=copy.deepcopy(list(history)), retrieval_query=query,
98
+ context=copy.deepcopy(context or {}),
99
+ corpus_snapshot_id=snapshot, document_ids=list(document_ids), references=refs,
100
+ metadata=dict(origin=origin, scenario_family=family, split=split,
101
+ cluster_id=cluster_id or case_id, source_id=source_id,
102
+ native_id=native_id or case_id))
103
+ return validate_case(value)
104
+
105
+
106
+ def validate_case(value, corpus=None):
107
+ exact(value, ('schema_version', 'id', 'question', 'conversation_history', 'retrieval_query',
108
+ 'context', 'corpus_snapshot_id', 'document_ids', 'references', 'metadata'), 'case')
109
+ require(value['schema_version'] == CASE_SCHEMA, 'Unsupported RAG case schema')
110
+ identity(value['id']); identity(value['corpus_snapshot_id'])
111
+ require(type(value['question']) is str and 0 < len(value['question']) <= 100_000,
112
+ 'Original question is required')
113
+ require(value['retrieval_query'] is None or type(value['retrieval_query']) is str,
114
+ 'Retrieval query must be text or unknown')
115
+ require(type(value['context']) is dict and set(value['context']) <= {'as_of', 'locale'}
116
+ and all(type(v) is str for v in value['context'].values()), 'Invalid public task context')
117
+ require(type(value['conversation_history']) is list and len(value['conversation_history']) <= 1000,
118
+ 'Invalid conversation history')
119
+ for turn in value['conversation_history']:
120
+ exact(turn, ('role', 'content'), 'history turn')
121
+ require(turn['role'] in ('system', 'user', 'assistant', 'tool')
122
+ and type(turn['content']) is str, 'Invalid history turn')
123
+ documents = ids(value['document_ids'])
124
+ if corpus is not None:
125
+ require(all(d in corpus for d in documents), 'Case references absent corpus documents')
126
+ meta = value['metadata']
127
+ exact(meta, ('origin', 'scenario_family', 'split', 'cluster_id', 'source_id', 'native_id'), 'metadata')
128
+ require(meta['origin'] in ('synthetic', 'public', 'owner-held', 'recorded'), 'Invalid origin')
129
+ for field in meta:
130
+ identity(meta[field])
131
+ refs = value['references']; exact(refs, REFERENCE_FIELDS, 'references')
132
+ # A known source may be absent from a particular candidate's context. Gold
133
+ # identities bind the shared corpus, not only documents delivered to it.
134
+ reference_universe = corpus
135
+ for field in ('answerable',):
136
+ require(refs[field] is None or type(refs[field]) is bool, 'Invalid answerability label')
137
+ require(refs['expected_action'] in (None, 'answer', 'abstain', 'clarify', 'no_rag'),
138
+ 'Invalid expected action')
139
+ for field in ('answers', 'support_ids', 'required_atoms', 'required_relations'):
140
+ require(refs[field] is None or type(refs[field]) is list
141
+ and all(type(x) is str for x in refs[field]), 'Invalid reference text/ID list')
142
+ for field in ('support_ids',):
143
+ if refs[field] is not None:
144
+ support = ids(refs[field])
145
+ require(reference_universe is None or all(d in reference_universe for d in support), 'Gold support outside frozen corpus')
146
+ if refs['qrels'] is not None:
147
+ require(type(refs['qrels']) is dict
148
+ and all(type(v) is int and 0 <= v <= 100 for v in refs['qrels'].values()),
149
+ 'Invalid native qrels')
150
+ keys=ids(list(refs['qrels']))
151
+ require(reference_universe is None or all(d in reference_universe for d in keys), 'Qrel outside frozen corpus')
152
+ require(type(refs['native']) is dict, 'Native annotations must be an object')
153
+ for field in ('atom_support', 'relation_support'):
154
+ if refs[field] is not None:
155
+ require(type(refs[field]) is dict, 'Invalid evidence mapping')
156
+ for required, support in refs[field].items():
157
+ identity(required)
158
+ support_ids=ids(support)
159
+ require(reference_universe is None or all(d in reference_universe for d in support_ids), 'Evidence support outside frozen corpus')
160
+ for field in ('support_facts', 'annotation_sets'):
161
+ require(refs[field] is None or type(refs[field]) is list, 'Invalid annotation list')
162
+ facts(refs['support_facts'])
163
+ if refs['annotation_sets'] is not None:
164
+ for annotation in refs['annotation_sets']:
165
+ exact(annotation,('answer','evidence_ids'),'answer annotation')
166
+ require(type(annotation['answer']) is str,'Invalid annotated answer')
167
+ if annotation['evidence_ids'] is not None:
168
+ evidence=ids(annotation['evidence_ids'])
169
+ require(reference_universe is None or all(d in reference_universe for d in evidence),'Annotated evidence outside corpus')
170
+ canonical(value)
171
+ return value
172
+
173
+
174
+ def candidate_input(case, corpus):
175
+ validate_case(case, corpus)
176
+ # This projection is deliberately built field by field, including corpus
177
+ # rows. Native annotations and accidental extra corpus fields never enter it.
178
+ return copy.deepcopy(dict(case_id=case['id'], question=case['question'],
179
+ conversation_history=case['conversation_history'], retrieval_query=case['retrieval_query'],
180
+ context=case['context'],
181
+ corpus_snapshot_id=case['corpus_snapshot_id'], documents=[
182
+ dict(id=did, text=corpus[did]['text'], title=corpus[did].get('title', ''))
183
+ for did in case['document_ids']]))
184
+
185
+
186
+ def make_trace(case, corpus, *, text='', ranked=None, prompt=None, status='ok', abstained=False,
187
+ attempt_id='attempt-1', candidate_id='fixture', source_digest=None,
188
+ support_ids=None, support_facts=None):
189
+ value = dict(schema_version=TRACE_SCHEMA, case_id=case['id'], attempt_id=attempt_id,
190
+ rewritten_query=case['retrieval_query'], native={},
191
+ input_sha256=digest(candidate_input(case, corpus)), candidate=dict(id=candidate_id,
192
+ source_sha256=source_digest or digest({'fixture': candidate_id})),
193
+ stages=dict.fromkeys(STAGES), response=dict(status=status, text=text, abstained=abstained,
194
+ support_ids=support_ids, support_facts=support_facts), operational=dict(tokens=None, cost_usd=None))
195
+ value['stages']['retrieved'] = ranked
196
+ value['stages']['prompt'] = prompt
197
+ return validate_trace(value, case, corpus)
198
+
199
+
200
+ def validate_trace(value, case=None, corpus=None):
201
+ exact(value, ('schema_version', 'case_id', 'attempt_id', 'input_sha256', 'candidate', 'rewritten_query', 'native',
202
+ 'stages', 'response', 'operational'), 'trace')
203
+ require(value['schema_version'] == TRACE_SCHEMA, 'Unsupported RAG trace schema')
204
+ identity(value['case_id']); identity(value['attempt_id']); checksum(value['input_sha256'])
205
+ require(value['rewritten_query'] is None or type(value['rewritten_query']) is str, 'Invalid captured rewritten query')
206
+ require(type(value['native']) is dict, 'Native trace capture must be an object')
207
+ exact(value['candidate'], ('id', 'source_sha256'), 'candidate')
208
+ identity(value['candidate']['id']); checksum(value['candidate']['source_sha256'])
209
+ exact(value['stages'], STAGES, 'trace stages')
210
+ previous = None
211
+ for stage in STAGES:
212
+ current = value['stages'][stage]
213
+ if current is None:
214
+ continue
215
+ current = ids(current)
216
+ if previous is not None:
217
+ require(current <= previous, 'Later stage invents documents')
218
+ if case is not None:
219
+ require(current <= set(case['document_ids']), 'Trace uses unavailable documents')
220
+ previous = current
221
+ response = value['response']
222
+ exact(response, ('status', 'text', 'abstained', 'support_ids', 'support_facts'), 'response')
223
+ require(response['status'] in ('ok', 'error', 'timeout', 'missing'), 'Unknown response status')
224
+ require(type(response['text']) is str and type(response['abstained']) is bool, 'Invalid response')
225
+ if response['support_ids'] is not None:
226
+ support = ids(response['support_ids'])
227
+ if case is not None:
228
+ require(support <= set(case['document_ids']), 'Predicted support outside case')
229
+ require(response['support_facts'] is None or type(response['support_facts']) is list,
230
+ 'Invalid predicted supporting facts')
231
+ facts(response['support_facts'])
232
+ exact(value['operational'], ('tokens', 'cost_usd'), 'operations')
233
+ for item in value['operational'].values():
234
+ require(item is None or type(item) in (int, float) and math.isfinite(item) and item >= 0,
235
+ 'Invalid recorded usage')
236
+ if case is not None:
237
+ require(value['case_id'] == case['id'], 'Trace case identity differs')
238
+ if corpus is not None:
239
+ require(value['input_sha256'] == digest(candidate_input(case, corpus)), 'Trace inputs changed')
240
+ canonical(value)
241
+ return value
242
+
243
+
244
+ def judgment(case, trace, task, labels, *, origin='fixture', model='fixture', rubric='v1'):
245
+ value = dict(schema_version=JUDGMENT_SCHEMA, case_id=case['id'], trace_sha256=digest(trace),
246
+ input_sha256=trace['input_sha256'], task=task, labels=copy.deepcopy(labels),
247
+ provenance=dict(origin=origin, model=model, rubric=rubric), status='judged',
248
+ usage=dict(tokens=None, cost_usd=None))
249
+ validate_judgment(value, case, trace, task)
250
+ return value
251
+
252
+
253
+ def validate_judgment(value, case=None, trace=None, task=None):
254
+ exact(value, ('schema_version', 'case_id', 'trace_sha256', 'input_sha256', 'task',
255
+ 'labels', 'provenance', 'status', 'usage'), 'judgment')
256
+ require(value['schema_version'] == JUDGMENT_SCHEMA, 'Unknown judgment schema')
257
+ identity(value['case_id']); checksum(value['trace_sha256']); checksum(value['input_sha256'])
258
+ require((case is None) == (trace is None), 'Supply both case and trace to bind a review')
259
+ if case is not None:
260
+ require(value['case_id'] == case['id'] and value['trace_sha256'] == digest(trace)
261
+ and value['input_sha256'] == trace['input_sha256'], 'Judgment does not bind the exact case/trace inputs')
262
+ require(task is None or value['task'] == task, 'Judgment task differs')
263
+ identity(value['task']); require(type(value['labels']) is dict, 'Judgment labels must be an object')
264
+ exact(value['provenance'], ('origin', 'model', 'rubric'), 'judgment provenance')
265
+ require(value['provenance']['origin'] in ('fixture', 'model', 'human'), 'Invalid review origin')
266
+ for field in ('model', 'rubric'):
267
+ identity(value['provenance'][field])
268
+ require(value['status'] in ('judged', 'abstained', 'error', 'timeout'), 'Invalid judgment status')
269
+ exact(value['usage'], ('tokens', 'cost_usd'), 'judgment usage')
270
+ for item in value['usage'].values():
271
+ require(item is None or type(item) in (int, float) and math.isfinite(item) and item >= 0,
272
+ 'Invalid judgment usage')
273
+ canonical(value)
274
+ return value
275
+
276
+
277
+ def save_json(value, path):
278
+ target = Path(path)
279
+ target.parent.mkdir(parents=True, exist_ok=True)
280
+ with target.open('xb') as handle:
281
+ handle.write(canonical(value) + b'\n')
282
+ return hashlib.sha256(target.read_bytes()).hexdigest()