flowspec2 1.0.0__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.
- flowspec2/__init__.py +105 -0
- flowspec2/authoring/__init__.py +275 -0
- flowspec2/authoring/authoring-evidence-signature.schema.json +34 -0
- flowspec2/authoring/authoring-evidence.schema.json +455 -0
- flowspec2/authoring/authoring-operational-evidence.schema.json +160 -0
- flowspec2/authoring/benchmark.py +1409 -0
- flowspec2/authoring/corpus/await_correction.case.json +220 -0
- flowspec2/authoring/corpus/flowspec2.ctk.json +769 -0
- flowspec2/authoring/corpus/gated_derive.case.json +100 -0
- flowspec2/authoring/corpus/linear.case.json +55 -0
- flowspec2/authoring/corpus/manifest.json +31 -0
- flowspec2/authoring/corpus/subflow.case.json +66 -0
- flowspec2/authoring/corpus/terminal.case.json +94 -0
- flowspec2/authoring/corpus.py +307 -0
- flowspec2/authoring/ctk.py +836 -0
- flowspec2/authoring/detached_signature.py +120 -0
- flowspec2/authoring/evidence.py +311 -0
- flowspec2/authoring/evidence_signature.py +187 -0
- flowspec2/authoring/evidence_verification.py +229 -0
- flowspec2/authoring/gemini.py +215 -0
- flowspec2/authoring/operational-corpus.json +61 -0
- flowspec2/authoring/operational.py +956 -0
- flowspec2/authoring/operational_providers.py +167 -0
- flowspec2/authoring/presentation_review.py +1013 -0
- flowspec2/authoring/presentation_review_signature.py +305 -0
- flowspec2/authoring/projection.py +299 -0
- flowspec2/authoring/provider_prompt.py +83 -0
- flowspec2/backends/__init__.py +82 -0
- flowspec2/backends/http.py +189 -0
- flowspec2/checker.py +238 -0
- flowspec2/cli.py +789 -0
- flowspec2/cli_parser.py +345 -0
- flowspec2/clock.py +23 -0
- flowspec2/compat/__init__.py +47 -0
- flowspec2/compat/models.py +82 -0
- flowspec2/compat/open_workflow.py +616 -0
- flowspec2/compat/rasa.py +19 -0
- flowspec2/compat/rasa_export.py +876 -0
- flowspec2/compat/rasa_import.py +992 -0
- flowspec2/compat/rasa_shared.py +270 -0
- flowspec2/compat/schemas/open-workflow-conversation-1.schema.json +138 -0
- flowspec2/compat/schemas/vendor/open-workflow-1.0.3.LICENSE +201 -0
- flowspec2/compat/schemas/vendor/open-workflow-1.0.3.provenance.json +13 -0
- flowspec2/compat/schemas/vendor/open-workflow-1.0.3.workflow.yaml +1956 -0
- flowspec2/compat/tool_profiles.py +149 -0
- flowspec2/compat/yaml.py +147 -0
- flowspec2/compiler.py +957 -0
- flowspec2/compiler_contracts.py +17 -0
- flowspec2/compiler_resume_contracts.py +594 -0
- flowspec2/compiler_schema_relations.py +1830 -0
- flowspec2/compiler_tool_contracts.py +245 -0
- flowspec2/compiler_value_contracts.py +447 -0
- flowspec2/derive.py +32 -0
- flowspec2/diagnostics.py +94 -0
- flowspec2/domains.py +489 -0
- flowspec2/experimental/__init__.py +57 -0
- flowspec2/experimental/flowspec-3-draft.schema.json +923 -0
- flowspec2/experimental/v3-preview-loss-policy.json +161 -0
- flowspec2/experimental/v3_lowering.py +928 -0
- flowspec2/experimental/v3_preview.py +2460 -0
- flowspec2/flowspec-2.schema.json +635 -0
- flowspec2/interactive.py +300 -0
- flowspec2/ir.py +1459 -0
- flowspec2/json_codec.py +120 -0
- flowspec2/llm.py +366 -0
- flowspec2/models.py +121 -0
- flowspec2/nodes.py +1537 -0
- flowspec2/observability.py +151 -0
- flowspec2/predicates.py +89 -0
- flowspec2/profiles.py +118 -0
- flowspec2/py.typed +0 -0
- flowspec2/runtime.py +1059 -0
- flowspec2/schema.py +54 -0
- flowspec2/schema_contracts.py +203 -0
- flowspec2/semantic_derive_contracts.py +530 -0
- flowspec2/semantic_path_contracts.py +528 -0
- flowspec2/semantic_predicate_contracts.py +355 -0
- flowspec2/semantic_schema_contracts.py +137 -0
- flowspec2/semantic_source_contracts.py +21 -0
- flowspec2/semantic_state_contracts.py +450 -0
- flowspec2/semantic_support.py +40 -0
- flowspec2/semantics.py +410 -0
- flowspec2/state_migration.py +1034 -0
- flowspec2/subflows/__init__.py +1117 -0
- flowspec2/subflows/address.py +328 -0
- flowspec2/subflows/identification.py +1031 -0
- flowspec2/tools.py +1154 -0
- flowspec2-1.0.0.dist-info/METADATA +482 -0
- flowspec2-1.0.0.dist-info/RECORD +92 -0
- flowspec2-1.0.0.dist-info/WHEEL +4 -0
- flowspec2-1.0.0.dist-info/entry_points.txt +2 -0
- flowspec2-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,956 @@
|
|
|
1
|
+
"""Report-only evidence for model-mediated routing and extraction behavior."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
from collections.abc import Callable, Mapping
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from importlib.metadata import version as package_version
|
|
12
|
+
from importlib.resources import files
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Final, Literal, cast
|
|
15
|
+
|
|
16
|
+
import jsonschema
|
|
17
|
+
|
|
18
|
+
from flowspec2.domains import make_slot_model
|
|
19
|
+
from flowspec2.json_codec import StrictJsonError, strict_json_loads, validate_json_value
|
|
20
|
+
from flowspec2.llm import (
|
|
21
|
+
StructuredOutputRequest,
|
|
22
|
+
build_extraction_request,
|
|
23
|
+
build_route_request,
|
|
24
|
+
)
|
|
25
|
+
from flowspec2.models import AgentResponse
|
|
26
|
+
|
|
27
|
+
from .evidence import AuthoringProviderProvenance
|
|
28
|
+
from .evidence_verification import AuthoringEvidenceVerification, verify_authoring_evidence
|
|
29
|
+
|
|
30
|
+
AUTHORING_OPERATIONAL_EVIDENCE_FORMAT: Final[str] = "flowspec2/authoring-operational-evidence@2"
|
|
31
|
+
OPERATIONAL_EVIDENCE_CLASSIFICATION: Final[str] = "report_only"
|
|
32
|
+
OPERATIONAL_CORPUS_FORMAT: Final[str] = "flowspec2/operational-corpus@2"
|
|
33
|
+
OPERATIONAL_PROBE_FORMAT: Final[str] = "flowspec2/operational-probe@2"
|
|
34
|
+
REFERENCE_OPERATIONAL_CORPUS_IDENTIFIER: Final[str] = "flowspec2_reference_operational"
|
|
35
|
+
OPERATIONAL_PROMPT_FORMAT: Final[str] = "flowspec2/operational-structured-output@1"
|
|
36
|
+
|
|
37
|
+
_CORPUS_PATH: Final[Path] = Path(__file__).with_name("operational-corpus.json")
|
|
38
|
+
_EVIDENCE_SCHEMA_PATH: Final[Path] = Path(__file__).with_name(
|
|
39
|
+
"authoring-operational-evidence.schema.json"
|
|
40
|
+
)
|
|
41
|
+
_IDENTIFIER_PATTERN: Final[re.Pattern[str]] = re.compile(r"^[a-z0-9][a-z0-9_-]*$")
|
|
42
|
+
_DIGEST_PATTERN: Final[re.Pattern[str]] = re.compile(r"^[0-9a-f]{64}$")
|
|
43
|
+
_SECRET_KEY_PATTERN: Final[re.Pattern[str]] = re.compile(
|
|
44
|
+
r"(?:api[_-]?key|authorization|credential|password|secret|access[_-]?token)",
|
|
45
|
+
re.IGNORECASE,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
OperationalKind = Literal["route", "extract"]
|
|
49
|
+
OperationalSubjectMode = Literal["authored", "counterfactual"]
|
|
50
|
+
OperationalDiagnostic = Literal[
|
|
51
|
+
"invalid_json",
|
|
52
|
+
"schema_mismatch",
|
|
53
|
+
"semantic_mismatch",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _canonical_json(json_document: object) -> str:
|
|
58
|
+
return json.dumps(
|
|
59
|
+
json_document,
|
|
60
|
+
ensure_ascii=False,
|
|
61
|
+
allow_nan=False,
|
|
62
|
+
separators=(",", ":"),
|
|
63
|
+
sort_keys=True,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _sha256(serialized_contract: str) -> str:
|
|
68
|
+
return hashlib.sha256(serialized_contract.encode("utf-8")).hexdigest()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _request_contract(structured_request: StructuredOutputRequest) -> dict[str, object]:
|
|
72
|
+
return {
|
|
73
|
+
"system_instruction": structured_request.system,
|
|
74
|
+
"prompt": structured_request.prompt,
|
|
75
|
+
"response_schema": structured_request.response_schema,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _operational_prompt_contract() -> dict[str, object]:
|
|
80
|
+
route_request = build_route_request(
|
|
81
|
+
"reference citizen route input",
|
|
82
|
+
[
|
|
83
|
+
{
|
|
84
|
+
"flow": "reference_flow",
|
|
85
|
+
"route": {
|
|
86
|
+
"description": "Reference route description.",
|
|
87
|
+
"trigger_phrases": ["reference trigger phrase"],
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
],
|
|
91
|
+
)
|
|
92
|
+
extraction_request = build_extraction_request(
|
|
93
|
+
"reference citizen extraction input",
|
|
94
|
+
AgentResponse(
|
|
95
|
+
description="Reference extraction prompt.",
|
|
96
|
+
payload_schema={
|
|
97
|
+
"type": "object",
|
|
98
|
+
"additionalProperties": False,
|
|
99
|
+
"properties": {
|
|
100
|
+
"reference_slot": {
|
|
101
|
+
"description": "Reference extraction hint.",
|
|
102
|
+
"enum": ["reference_token"],
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"required": ["reference_slot"],
|
|
106
|
+
},
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
return {
|
|
110
|
+
"format": OPERATIONAL_PROMPT_FORMAT,
|
|
111
|
+
"request_fields": ["system_instruction", "prompt", "response_schema"],
|
|
112
|
+
"route_request": _request_contract(route_request),
|
|
113
|
+
"extraction_request": _request_contract(extraction_request),
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
OPERATIONAL_PROMPT_DIGEST: Final[str] = _sha256(_canonical_json(_operational_prompt_contract()))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _contains_secret_key(json_document: object) -> bool:
|
|
121
|
+
if isinstance(json_document, Mapping):
|
|
122
|
+
return any(
|
|
123
|
+
_SECRET_KEY_PATTERN.search(str(property_name)) is not None
|
|
124
|
+
or _contains_secret_key(property_value)
|
|
125
|
+
for property_name, property_value in json_document.items()
|
|
126
|
+
)
|
|
127
|
+
if isinstance(json_document, list):
|
|
128
|
+
return any(_contains_secret_key(element_value) for element_value in json_document)
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _decode_pointer_segment(pointer_segment: str) -> str:
|
|
133
|
+
return pointer_segment.replace("~1", "/").replace("~0", "~")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _resolve_pointer(json_document: object, json_pointer: str) -> object:
|
|
137
|
+
if not json_pointer.startswith("/"):
|
|
138
|
+
raise ValueError("operational subject pointer must be an absolute JSON Pointer")
|
|
139
|
+
current_value = json_document
|
|
140
|
+
for encoded_segment in json_pointer[1:].split("/"):
|
|
141
|
+
pointer_segment = _decode_pointer_segment(encoded_segment)
|
|
142
|
+
if isinstance(current_value, Mapping):
|
|
143
|
+
if pointer_segment not in current_value:
|
|
144
|
+
raise ValueError(f"operational subject pointer does not exist: {json_pointer}")
|
|
145
|
+
current_value = current_value[pointer_segment]
|
|
146
|
+
elif isinstance(current_value, list):
|
|
147
|
+
if not pointer_segment.isdigit() or int(pointer_segment) >= len(current_value):
|
|
148
|
+
raise ValueError(f"operational subject pointer does not exist: {json_pointer}")
|
|
149
|
+
current_value = current_value[int(pointer_segment)]
|
|
150
|
+
else:
|
|
151
|
+
raise ValueError(f"operational subject pointer does not exist: {json_pointer}")
|
|
152
|
+
return current_value
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _replace_pointer(json_document: object, json_pointer: str, replacement: object) -> None:
|
|
156
|
+
if not json_pointer.startswith("/"):
|
|
157
|
+
raise ValueError("operational subject pointer must be an absolute JSON Pointer")
|
|
158
|
+
encoded_segments = json_pointer[1:].split("/")
|
|
159
|
+
if not encoded_segments or encoded_segments == [""]:
|
|
160
|
+
raise ValueError("operational subject pointer cannot replace the document root")
|
|
161
|
+
parent_value = json_document
|
|
162
|
+
for encoded_segment in encoded_segments[:-1]:
|
|
163
|
+
pointer_segment = _decode_pointer_segment(encoded_segment)
|
|
164
|
+
if isinstance(parent_value, dict) and pointer_segment in parent_value:
|
|
165
|
+
parent_value = parent_value[pointer_segment]
|
|
166
|
+
elif (
|
|
167
|
+
isinstance(parent_value, list)
|
|
168
|
+
and pointer_segment.isdigit()
|
|
169
|
+
and int(pointer_segment) < len(parent_value)
|
|
170
|
+
):
|
|
171
|
+
parent_value = parent_value[int(pointer_segment)]
|
|
172
|
+
else:
|
|
173
|
+
raise ValueError(f"operational subject pointer does not exist: {json_pointer}")
|
|
174
|
+
final_segment = _decode_pointer_segment(encoded_segments[-1])
|
|
175
|
+
if isinstance(parent_value, dict) and final_segment in parent_value:
|
|
176
|
+
parent_value[final_segment] = replacement
|
|
177
|
+
return
|
|
178
|
+
if (
|
|
179
|
+
isinstance(parent_value, list)
|
|
180
|
+
and final_segment.isdigit()
|
|
181
|
+
and int(final_segment) < len(parent_value)
|
|
182
|
+
):
|
|
183
|
+
parent_value[int(final_segment)] = replacement
|
|
184
|
+
return
|
|
185
|
+
raise ValueError(f"operational subject pointer does not exist: {json_pointer}")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@dataclass(frozen=True)
|
|
189
|
+
class OperationalProbe:
|
|
190
|
+
"""One public model-mediated observation bound to an authoring case."""
|
|
191
|
+
|
|
192
|
+
identifier: str
|
|
193
|
+
pair_identifier: str
|
|
194
|
+
subject_mode: OperationalSubjectMode
|
|
195
|
+
operation: OperationalKind
|
|
196
|
+
authoring_case_identifier: str
|
|
197
|
+
subject_pointer: str
|
|
198
|
+
citizen_input: str
|
|
199
|
+
expected_json: str
|
|
200
|
+
counterfactual_json: str | None = None
|
|
201
|
+
|
|
202
|
+
def __post_init__(self) -> None:
|
|
203
|
+
if not _IDENTIFIER_PATTERN.fullmatch(self.identifier):
|
|
204
|
+
raise ValueError("operational probe identifier is invalid")
|
|
205
|
+
if not _IDENTIFIER_PATTERN.fullmatch(self.pair_identifier):
|
|
206
|
+
raise ValueError("operational probe pair identifier is invalid")
|
|
207
|
+
if self.subject_mode not in {"authored", "counterfactual"}:
|
|
208
|
+
raise ValueError("operational probe subject mode is unsupported")
|
|
209
|
+
if self.operation not in {"route", "extract"}:
|
|
210
|
+
raise ValueError("operational probe operation is unsupported")
|
|
211
|
+
if not _IDENTIFIER_PATTERN.fullmatch(self.authoring_case_identifier):
|
|
212
|
+
raise ValueError("operational probe authoring case identifier is invalid")
|
|
213
|
+
if not self.subject_pointer.startswith("/"):
|
|
214
|
+
raise ValueError("operational probe subject pointer must be a JSON Pointer")
|
|
215
|
+
if not self.citizen_input.strip():
|
|
216
|
+
raise ValueError("operational probe citizen input must be non-empty")
|
|
217
|
+
expected_output = strict_json_loads(self.expected_json)
|
|
218
|
+
validate_json_value(expected_output, boundary="operational expected output")
|
|
219
|
+
if self.expected_json != _canonical_json(expected_output):
|
|
220
|
+
raise ValueError("operational probe expected output must be canonical JSON")
|
|
221
|
+
if self.subject_mode == "authored" and self.counterfactual_json is not None:
|
|
222
|
+
raise ValueError("authored operational probes cannot replace their subject")
|
|
223
|
+
if self.subject_mode == "counterfactual" and self.counterfactual_json is None:
|
|
224
|
+
raise ValueError("counterfactual operational probes require a replacement subject")
|
|
225
|
+
if self.counterfactual_json is not None:
|
|
226
|
+
counterfactual_subject = strict_json_loads(self.counterfactual_json)
|
|
227
|
+
validate_json_value(
|
|
228
|
+
counterfactual_subject,
|
|
229
|
+
boundary="operational counterfactual subject",
|
|
230
|
+
)
|
|
231
|
+
if self.counterfactual_json != _canonical_json(counterfactual_subject):
|
|
232
|
+
raise ValueError("operational counterfactual subject must be canonical JSON")
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def expected_output(self) -> object:
|
|
236
|
+
return strict_json_loads(self.expected_json)
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def counterfactual_subject(self) -> object | None:
|
|
240
|
+
return (
|
|
241
|
+
None
|
|
242
|
+
if self.counterfactual_json is None
|
|
243
|
+
else strict_json_loads(self.counterfactual_json)
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
def to_dict(self) -> dict[str, object]:
|
|
247
|
+
return {
|
|
248
|
+
"identifier": self.identifier,
|
|
249
|
+
"pair_identifier": self.pair_identifier,
|
|
250
|
+
"subject_mode": self.subject_mode,
|
|
251
|
+
"operation": self.operation,
|
|
252
|
+
"authoring_case_identifier": self.authoring_case_identifier,
|
|
253
|
+
"subject_pointer": self.subject_pointer,
|
|
254
|
+
"citizen_input": self.citizen_input,
|
|
255
|
+
"expected": self.expected_output,
|
|
256
|
+
"counterfactual": self.counterfactual_subject,
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@dataclass(frozen=True)
|
|
261
|
+
class OperationalCorpus:
|
|
262
|
+
"""Closed packaged operational probes and their content identity."""
|
|
263
|
+
|
|
264
|
+
identifier: str
|
|
265
|
+
digest: str
|
|
266
|
+
probes: tuple[OperationalProbe, ...]
|
|
267
|
+
format_identifier: str = OPERATIONAL_CORPUS_FORMAT
|
|
268
|
+
probe_format_identifier: str = OPERATIONAL_PROBE_FORMAT
|
|
269
|
+
|
|
270
|
+
def __post_init__(self) -> None:
|
|
271
|
+
if self.format_identifier != OPERATIONAL_CORPUS_FORMAT:
|
|
272
|
+
raise ValueError("operational corpus format is unsupported")
|
|
273
|
+
if self.probe_format_identifier != OPERATIONAL_PROBE_FORMAT:
|
|
274
|
+
raise ValueError("operational probe format is unsupported")
|
|
275
|
+
if self.identifier != REFERENCE_OPERATIONAL_CORPUS_IDENTIFIER:
|
|
276
|
+
raise ValueError("operational corpus identifier is unsupported")
|
|
277
|
+
if not _DIGEST_PATTERN.fullmatch(self.digest):
|
|
278
|
+
raise ValueError("operational corpus digest must be lowercase SHA-256")
|
|
279
|
+
identifiers = tuple(probe.identifier for probe in self.probes)
|
|
280
|
+
if not identifiers or identifiers != tuple(sorted(identifiers)):
|
|
281
|
+
raise ValueError("operational probes must be non-empty and sorted")
|
|
282
|
+
if len(set(identifiers)) != len(identifiers):
|
|
283
|
+
raise ValueError("operational probe identifiers must be unique")
|
|
284
|
+
probes_by_pair: dict[str, list[OperationalProbe]] = {}
|
|
285
|
+
for probe in self.probes:
|
|
286
|
+
probes_by_pair.setdefault(probe.pair_identifier, []).append(probe)
|
|
287
|
+
for pair_identifier, paired_probes in probes_by_pair.items():
|
|
288
|
+
if len(paired_probes) != 2 or {probe.subject_mode for probe in paired_probes} != {
|
|
289
|
+
"authored",
|
|
290
|
+
"counterfactual",
|
|
291
|
+
}:
|
|
292
|
+
raise ValueError(
|
|
293
|
+
f"operational pair must contain authored and counterfactual probes: "
|
|
294
|
+
f"{pair_identifier}"
|
|
295
|
+
)
|
|
296
|
+
comparison_fields = {
|
|
297
|
+
(
|
|
298
|
+
probe.operation,
|
|
299
|
+
probe.authoring_case_identifier,
|
|
300
|
+
probe.subject_pointer,
|
|
301
|
+
probe.citizen_input,
|
|
302
|
+
)
|
|
303
|
+
for probe in paired_probes
|
|
304
|
+
}
|
|
305
|
+
if len(comparison_fields) != 1:
|
|
306
|
+
raise ValueError(f"operational pair inputs are inconsistent: {pair_identifier}")
|
|
307
|
+
if len({probe.expected_json for probe in paired_probes}) != 2:
|
|
308
|
+
raise ValueError(
|
|
309
|
+
f"operational pair must expect distinct outcomes: {pair_identifier}"
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
def metadata(self) -> dict[str, object]:
|
|
313
|
+
return {
|
|
314
|
+
"format": self.format_identifier,
|
|
315
|
+
"probe_format": self.probe_format_identifier,
|
|
316
|
+
"identifier": self.identifier,
|
|
317
|
+
"digest": self.digest,
|
|
318
|
+
"probe_identifiers": [probe.identifier for probe in self.probes],
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
_operational_corpus_cache: OperationalCorpus | None = None
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def load_reference_operational_corpus() -> OperationalCorpus:
|
|
326
|
+
"""Load the immutable public operational probe corpus."""
|
|
327
|
+
|
|
328
|
+
global _operational_corpus_cache
|
|
329
|
+
if _operational_corpus_cache is not None:
|
|
330
|
+
return _operational_corpus_cache
|
|
331
|
+
corpus_resource = files("flowspec2.authoring").joinpath("operational-corpus.json")
|
|
332
|
+
serialized_corpus = corpus_resource.read_text(encoding="utf-8")
|
|
333
|
+
decoded_corpus = strict_json_loads(serialized_corpus)
|
|
334
|
+
if not isinstance(decoded_corpus, dict):
|
|
335
|
+
raise ValueError("operational corpus must be a JSON object")
|
|
336
|
+
if set(decoded_corpus) != {"format", "case_format", "identifier", "probes"}:
|
|
337
|
+
raise ValueError("operational corpus keys do not match the closed contract")
|
|
338
|
+
if decoded_corpus["format"] != OPERATIONAL_CORPUS_FORMAT:
|
|
339
|
+
raise ValueError("operational corpus format is unsupported")
|
|
340
|
+
if decoded_corpus["case_format"] != OPERATIONAL_PROBE_FORMAT:
|
|
341
|
+
raise ValueError("operational probe format is unsupported")
|
|
342
|
+
raw_probes = decoded_corpus["probes"]
|
|
343
|
+
if not isinstance(raw_probes, list):
|
|
344
|
+
raise ValueError("operational corpus probes must be an array")
|
|
345
|
+
probes: list[OperationalProbe] = []
|
|
346
|
+
for raw_probe in raw_probes:
|
|
347
|
+
if not isinstance(raw_probe, dict) or set(raw_probe) != {
|
|
348
|
+
"identifier",
|
|
349
|
+
"pair_identifier",
|
|
350
|
+
"subject_mode",
|
|
351
|
+
"operation",
|
|
352
|
+
"authoring_case_identifier",
|
|
353
|
+
"subject_pointer",
|
|
354
|
+
"citizen_input",
|
|
355
|
+
"expected",
|
|
356
|
+
"counterfactual",
|
|
357
|
+
}:
|
|
358
|
+
raise ValueError("operational probe keys do not match the closed contract")
|
|
359
|
+
probes.append(
|
|
360
|
+
OperationalProbe(
|
|
361
|
+
identifier=cast(str, raw_probe["identifier"]),
|
|
362
|
+
pair_identifier=cast(str, raw_probe["pair_identifier"]),
|
|
363
|
+
subject_mode=cast(OperationalSubjectMode, raw_probe["subject_mode"]),
|
|
364
|
+
operation=cast(OperationalKind, raw_probe["operation"]),
|
|
365
|
+
authoring_case_identifier=cast(str, raw_probe["authoring_case_identifier"]),
|
|
366
|
+
subject_pointer=cast(str, raw_probe["subject_pointer"]),
|
|
367
|
+
citizen_input=cast(str, raw_probe["citizen_input"]),
|
|
368
|
+
expected_json=_canonical_json(raw_probe["expected"]),
|
|
369
|
+
counterfactual_json=(
|
|
370
|
+
None
|
|
371
|
+
if raw_probe["counterfactual"] is None
|
|
372
|
+
else _canonical_json(raw_probe["counterfactual"])
|
|
373
|
+
),
|
|
374
|
+
)
|
|
375
|
+
)
|
|
376
|
+
canonical_corpus = _canonical_json(decoded_corpus)
|
|
377
|
+
_operational_corpus_cache = OperationalCorpus(
|
|
378
|
+
identifier=cast(str, decoded_corpus["identifier"]),
|
|
379
|
+
digest=_sha256(canonical_corpus),
|
|
380
|
+
probes=tuple(probes),
|
|
381
|
+
)
|
|
382
|
+
return _operational_corpus_cache
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
@dataclass(frozen=True)
|
|
386
|
+
class OperationalProbeRequest:
|
|
387
|
+
"""Exact provider-neutral inputs for one operational probe."""
|
|
388
|
+
|
|
389
|
+
probe_identifier: str
|
|
390
|
+
operation: OperationalKind
|
|
391
|
+
system_instruction: str
|
|
392
|
+
prompt: str
|
|
393
|
+
response_schema_json: str
|
|
394
|
+
|
|
395
|
+
def __post_init__(self) -> None:
|
|
396
|
+
if not _IDENTIFIER_PATTERN.fullmatch(self.probe_identifier):
|
|
397
|
+
raise ValueError("operational request probe identifier is invalid")
|
|
398
|
+
if self.operation not in {"route", "extract"}:
|
|
399
|
+
raise ValueError("operational request operation is unsupported")
|
|
400
|
+
if not self.system_instruction or not self.prompt:
|
|
401
|
+
raise ValueError("operational request instructions must be non-empty")
|
|
402
|
+
response_schema = strict_json_loads(self.response_schema_json)
|
|
403
|
+
if not isinstance(response_schema, dict):
|
|
404
|
+
raise ValueError("operational response schema must be an object")
|
|
405
|
+
if self.response_schema_json != _canonical_json(response_schema):
|
|
406
|
+
raise ValueError("operational response schema must be canonical JSON")
|
|
407
|
+
jsonschema.Draft202012Validator.check_schema(response_schema)
|
|
408
|
+
|
|
409
|
+
@classmethod
|
|
410
|
+
def from_structured_request(
|
|
411
|
+
cls,
|
|
412
|
+
probe: OperationalProbe,
|
|
413
|
+
structured_request: StructuredOutputRequest,
|
|
414
|
+
) -> OperationalProbeRequest:
|
|
415
|
+
return cls(
|
|
416
|
+
probe_identifier=probe.identifier,
|
|
417
|
+
operation=probe.operation,
|
|
418
|
+
system_instruction=structured_request.system,
|
|
419
|
+
prompt=structured_request.prompt,
|
|
420
|
+
response_schema_json=_canonical_json(structured_request.response_schema),
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
def response_schema(self) -> dict[str, Any]:
|
|
424
|
+
return cast(dict[str, Any], strict_json_loads(self.response_schema_json))
|
|
425
|
+
|
|
426
|
+
def to_dict(self) -> dict[str, object]:
|
|
427
|
+
return {
|
|
428
|
+
"system_instruction": self.system_instruction,
|
|
429
|
+
"prompt": self.prompt,
|
|
430
|
+
"response_schema": self.response_schema(),
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
@dataclass(frozen=True)
|
|
435
|
+
class OperationalModelResponse:
|
|
436
|
+
"""Raw completed provider turn and optional effective model identity."""
|
|
437
|
+
|
|
438
|
+
raw_output: str
|
|
439
|
+
effective_model_version: str | None = None
|
|
440
|
+
|
|
441
|
+
def __post_init__(self) -> None:
|
|
442
|
+
if self.effective_model_version is not None:
|
|
443
|
+
normalized_model = self.effective_model_version.strip()
|
|
444
|
+
if not normalized_model:
|
|
445
|
+
raise ValueError("operational effective model version must be non-empty")
|
|
446
|
+
object.__setattr__(self, "effective_model_version", normalized_model)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
@dataclass(frozen=True)
|
|
450
|
+
class _PreparedProbe:
|
|
451
|
+
probe: OperationalProbe
|
|
452
|
+
correction_round: int
|
|
453
|
+
source_sha256: str
|
|
454
|
+
subject_sha256: str
|
|
455
|
+
request: OperationalProbeRequest
|
|
456
|
+
expected_output_json: str
|
|
457
|
+
|
|
458
|
+
@property
|
|
459
|
+
def expected_output(self) -> object:
|
|
460
|
+
return strict_json_loads(self.expected_output_json)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
@dataclass(frozen=True)
|
|
464
|
+
class OperationalCapture:
|
|
465
|
+
"""One exact request, raw model turn, and deterministic observation."""
|
|
466
|
+
|
|
467
|
+
probe_identifier: str
|
|
468
|
+
pair_identifier: str
|
|
469
|
+
subject_mode: OperationalSubjectMode
|
|
470
|
+
operation: OperationalKind
|
|
471
|
+
authoring_case_identifier: str
|
|
472
|
+
correction_round: int
|
|
473
|
+
source_sha256: str
|
|
474
|
+
subject_pointer: str
|
|
475
|
+
subject_sha256: str
|
|
476
|
+
request_json: str
|
|
477
|
+
expected_output_json: str
|
|
478
|
+
raw_output: str
|
|
479
|
+
raw_output_sha256: str
|
|
480
|
+
parsed_output_json: str | None
|
|
481
|
+
effective_model_version: str | None
|
|
482
|
+
matched: bool
|
|
483
|
+
diagnostic: OperationalDiagnostic | None
|
|
484
|
+
|
|
485
|
+
def to_dict(self) -> dict[str, object]:
|
|
486
|
+
return {
|
|
487
|
+
"probe_identifier": self.probe_identifier,
|
|
488
|
+
"pair_identifier": self.pair_identifier,
|
|
489
|
+
"subject_mode": self.subject_mode,
|
|
490
|
+
"operation": self.operation,
|
|
491
|
+
"authoring_case_identifier": self.authoring_case_identifier,
|
|
492
|
+
"correction_round": self.correction_round,
|
|
493
|
+
"source_sha256": self.source_sha256,
|
|
494
|
+
"subject_pointer": self.subject_pointer,
|
|
495
|
+
"subject_sha256": self.subject_sha256,
|
|
496
|
+
"request": strict_json_loads(self.request_json),
|
|
497
|
+
"expected_output": strict_json_loads(self.expected_output_json),
|
|
498
|
+
"raw_output": self.raw_output,
|
|
499
|
+
"raw_output_sha256": self.raw_output_sha256,
|
|
500
|
+
"parsed_output": (
|
|
501
|
+
strict_json_loads(self.parsed_output_json)
|
|
502
|
+
if self.parsed_output_json is not None
|
|
503
|
+
else None
|
|
504
|
+
),
|
|
505
|
+
"effective_model_version": self.effective_model_version,
|
|
506
|
+
"matched": self.matched,
|
|
507
|
+
"diagnostic": self.diagnostic,
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _completed_sources(
|
|
512
|
+
serialized_authoring_evidence: str,
|
|
513
|
+
verified_authoring_evidence: AuthoringEvidenceVerification,
|
|
514
|
+
) -> dict[str, dict[str, Any]]:
|
|
515
|
+
recomputed_verification = verify_authoring_evidence(
|
|
516
|
+
serialized_authoring_evidence,
|
|
517
|
+
expected_repository_revision=verified_authoring_evidence.repository_revision,
|
|
518
|
+
)
|
|
519
|
+
if recomputed_verification != verified_authoring_evidence:
|
|
520
|
+
raise ValueError("authoring evidence verification does not match its exact document")
|
|
521
|
+
evidence_document = strict_json_loads(serialized_authoring_evidence)
|
|
522
|
+
if not isinstance(evidence_document, dict):
|
|
523
|
+
raise ValueError("authoring evidence must be a JSON object")
|
|
524
|
+
if evidence_document.get("digest") != verified_authoring_evidence.digest:
|
|
525
|
+
raise ValueError("verified authoring evidence digest does not match its document")
|
|
526
|
+
captures = cast(list[dict[str, Any]], evidence_document["captures"])
|
|
527
|
+
captures_by_key = {
|
|
528
|
+
(capture["case_identifier"], capture["correction_round"]): capture for capture in captures
|
|
529
|
+
}
|
|
530
|
+
completed_sources: dict[str, dict[str, Any]] = {}
|
|
531
|
+
report = cast(dict[str, Any], evidence_document["report"])
|
|
532
|
+
for case_result in cast(list[dict[str, Any]], report["case_results"]):
|
|
533
|
+
if case_result["succeeded"] is not True:
|
|
534
|
+
continue
|
|
535
|
+
attempts = cast(list[dict[str, Any]], case_result["attempts"])
|
|
536
|
+
final_round = cast(int, attempts[-1]["correction_round"])
|
|
537
|
+
case_identifier = cast(str, case_result["case_identifier"])
|
|
538
|
+
capture = captures_by_key[(case_identifier, final_round)]
|
|
539
|
+
source_document = strict_json_loads(cast(str, capture["authored_source"]))
|
|
540
|
+
if not isinstance(source_document, dict):
|
|
541
|
+
raise ValueError("successful authored source must be a JSON object")
|
|
542
|
+
completed_sources[case_identifier] = {
|
|
543
|
+
"correction_round": final_round,
|
|
544
|
+
"source_sha256": capture["source_sha256"],
|
|
545
|
+
"source": source_document,
|
|
546
|
+
}
|
|
547
|
+
return completed_sources
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _catalog_contract(completed_sources: Mapping[str, Mapping[str, Any]]) -> dict[str, object]:
|
|
551
|
+
source_closure = [
|
|
552
|
+
{
|
|
553
|
+
"case_identifier": case_identifier,
|
|
554
|
+
"correction_round": completed_sources[case_identifier]["correction_round"],
|
|
555
|
+
"source_sha256": completed_sources[case_identifier]["source_sha256"],
|
|
556
|
+
}
|
|
557
|
+
for case_identifier in sorted(completed_sources)
|
|
558
|
+
]
|
|
559
|
+
return {
|
|
560
|
+
"source_closure": source_closure,
|
|
561
|
+
"digest": _sha256(_canonical_json(source_closure)),
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _extraction_agent_response(source_document: dict[str, Any], path_index: int) -> AgentResponse:
|
|
566
|
+
path = cast(list[dict[str, Any]], source_document["path"])
|
|
567
|
+
path_step = path[path_index]
|
|
568
|
+
slot_name = cast(str, path_step["slot"])
|
|
569
|
+
slots = cast(dict[str, dict[str, Any]], source_document["slots"])
|
|
570
|
+
slot_contract = slots[slot_name]
|
|
571
|
+
prompt_contract = cast(dict[str, Any], path_step["prompt"])
|
|
572
|
+
slot_model = make_slot_model(
|
|
573
|
+
slot_name,
|
|
574
|
+
cast(str, slot_contract["domain"]),
|
|
575
|
+
cast(dict[str, Any], source_document["domains"]),
|
|
576
|
+
nullable=cast(bool, slot_contract.get("nullable", False)),
|
|
577
|
+
extract_hint=cast(str | None, prompt_contract.get("extract_hint")),
|
|
578
|
+
)
|
|
579
|
+
return AgentResponse(
|
|
580
|
+
description=cast(str, prompt_contract["text"]),
|
|
581
|
+
payload_schema=slot_model.model_json_schema(),
|
|
582
|
+
interactive=cast(dict[str, Any] | None, path_step.get("interactive")),
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def _prepare_probes(
|
|
587
|
+
serialized_authoring_evidence: str,
|
|
588
|
+
verified_authoring_evidence: AuthoringEvidenceVerification,
|
|
589
|
+
) -> tuple[tuple[_PreparedProbe, ...], dict[str, object]]:
|
|
590
|
+
completed_sources = _completed_sources(
|
|
591
|
+
serialized_authoring_evidence,
|
|
592
|
+
verified_authoring_evidence,
|
|
593
|
+
)
|
|
594
|
+
corpus = load_reference_operational_corpus()
|
|
595
|
+
prepared_probes: list[_PreparedProbe] = []
|
|
596
|
+
for probe in corpus.probes:
|
|
597
|
+
source_contract = completed_sources.get(probe.authoring_case_identifier)
|
|
598
|
+
if source_contract is None:
|
|
599
|
+
raise ValueError(
|
|
600
|
+
f"operational probe requires successful authoring case: "
|
|
601
|
+
f"{probe.authoring_case_identifier}"
|
|
602
|
+
)
|
|
603
|
+
source_document = cast(dict[str, Any], source_contract["source"])
|
|
604
|
+
effective_source_document = copy.deepcopy(source_document)
|
|
605
|
+
if probe.subject_mode == "counterfactual":
|
|
606
|
+
_replace_pointer(
|
|
607
|
+
effective_source_document,
|
|
608
|
+
probe.subject_pointer,
|
|
609
|
+
probe.counterfactual_subject,
|
|
610
|
+
)
|
|
611
|
+
subject_value = _resolve_pointer(effective_source_document, probe.subject_pointer)
|
|
612
|
+
expected_output: object
|
|
613
|
+
if probe.operation == "route":
|
|
614
|
+
if probe.subject_pointer != "/route/trigger_phrases":
|
|
615
|
+
raise ValueError("route probe must bind route.trigger_phrases")
|
|
616
|
+
catalog_flows = [
|
|
617
|
+
(
|
|
618
|
+
effective_source_document
|
|
619
|
+
if case_identifier == probe.authoring_case_identifier
|
|
620
|
+
else cast(dict[str, Any], completed_sources[case_identifier]["source"])
|
|
621
|
+
)
|
|
622
|
+
for case_identifier in sorted(completed_sources)
|
|
623
|
+
]
|
|
624
|
+
structured_request = build_route_request(probe.citizen_input, catalog_flows)
|
|
625
|
+
expected_output = {
|
|
626
|
+
"service": effective_source_document["flow"]
|
|
627
|
+
if cast(dict[str, Any], probe.expected_output)["selected"] is True
|
|
628
|
+
else None
|
|
629
|
+
}
|
|
630
|
+
else:
|
|
631
|
+
pointer_segments = probe.subject_pointer.strip("/").split("/")
|
|
632
|
+
if (
|
|
633
|
+
len(pointer_segments) != 4
|
|
634
|
+
or pointer_segments[0] != "path"
|
|
635
|
+
or not pointer_segments[1].isdigit()
|
|
636
|
+
or pointer_segments[2:] != ["prompt", "extract_hint"]
|
|
637
|
+
):
|
|
638
|
+
raise ValueError("extract probe must bind a path prompt.extract_hint")
|
|
639
|
+
agent_response = _extraction_agent_response(
|
|
640
|
+
effective_source_document,
|
|
641
|
+
int(pointer_segments[1]),
|
|
642
|
+
)
|
|
643
|
+
structured_request = build_extraction_request(probe.citizen_input, agent_response)
|
|
644
|
+
expected_output = probe.expected_output
|
|
645
|
+
prepared_probes.append(
|
|
646
|
+
_PreparedProbe(
|
|
647
|
+
probe=probe,
|
|
648
|
+
correction_round=cast(int, source_contract["correction_round"]),
|
|
649
|
+
source_sha256=cast(str, source_contract["source_sha256"]),
|
|
650
|
+
subject_sha256=_sha256(_canonical_json(subject_value)),
|
|
651
|
+
request=OperationalProbeRequest.from_structured_request(
|
|
652
|
+
probe,
|
|
653
|
+
structured_request,
|
|
654
|
+
),
|
|
655
|
+
expected_output_json=_canonical_json(expected_output),
|
|
656
|
+
)
|
|
657
|
+
)
|
|
658
|
+
return tuple(prepared_probes), _catalog_contract(completed_sources)
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def _capture(
|
|
662
|
+
prepared_probe: _PreparedProbe,
|
|
663
|
+
model_response: OperationalModelResponse,
|
|
664
|
+
) -> OperationalCapture:
|
|
665
|
+
parsed_output: object | None = None
|
|
666
|
+
diagnostic: OperationalDiagnostic | None = None
|
|
667
|
+
try:
|
|
668
|
+
parsed_output = strict_json_loads(model_response.raw_output)
|
|
669
|
+
except (json.JSONDecodeError, StrictJsonError):
|
|
670
|
+
diagnostic = "invalid_json"
|
|
671
|
+
if diagnostic is None and not jsonschema.Draft202012Validator(
|
|
672
|
+
prepared_probe.request.response_schema()
|
|
673
|
+
).is_valid(cast(Any, parsed_output)):
|
|
674
|
+
diagnostic = "schema_mismatch"
|
|
675
|
+
if diagnostic is None and parsed_output != prepared_probe.expected_output:
|
|
676
|
+
diagnostic = "semantic_mismatch"
|
|
677
|
+
matched = diagnostic is None
|
|
678
|
+
return OperationalCapture(
|
|
679
|
+
probe_identifier=prepared_probe.probe.identifier,
|
|
680
|
+
pair_identifier=prepared_probe.probe.pair_identifier,
|
|
681
|
+
subject_mode=prepared_probe.probe.subject_mode,
|
|
682
|
+
operation=prepared_probe.probe.operation,
|
|
683
|
+
authoring_case_identifier=prepared_probe.probe.authoring_case_identifier,
|
|
684
|
+
correction_round=prepared_probe.correction_round,
|
|
685
|
+
source_sha256=prepared_probe.source_sha256,
|
|
686
|
+
subject_pointer=prepared_probe.probe.subject_pointer,
|
|
687
|
+
subject_sha256=prepared_probe.subject_sha256,
|
|
688
|
+
request_json=_canonical_json(prepared_probe.request.to_dict()),
|
|
689
|
+
expected_output_json=prepared_probe.expected_output_json,
|
|
690
|
+
raw_output=model_response.raw_output,
|
|
691
|
+
raw_output_sha256=_sha256(model_response.raw_output),
|
|
692
|
+
parsed_output_json=(
|
|
693
|
+
_canonical_json(parsed_output) if diagnostic != "invalid_json" else None
|
|
694
|
+
),
|
|
695
|
+
effective_model_version=model_response.effective_model_version,
|
|
696
|
+
matched=matched,
|
|
697
|
+
diagnostic=diagnostic,
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
@dataclass(frozen=True)
|
|
702
|
+
class AuthoringOperationalEvidence:
|
|
703
|
+
"""Canonical report-only operational evidence bound to authoring evidence."""
|
|
704
|
+
|
|
705
|
+
package_version: str
|
|
706
|
+
repository_revision: str
|
|
707
|
+
benchmark_evidence_digest: str
|
|
708
|
+
corpus: OperationalCorpus
|
|
709
|
+
provider: AuthoringProviderProvenance
|
|
710
|
+
catalog_json: str
|
|
711
|
+
captures: tuple[OperationalCapture, ...]
|
|
712
|
+
|
|
713
|
+
def __post_init__(self) -> None:
|
|
714
|
+
if not self.package_version or not self.repository_revision:
|
|
715
|
+
raise ValueError("operational evidence package and revision must be non-empty")
|
|
716
|
+
if not _DIGEST_PATTERN.fullmatch(self.benchmark_evidence_digest):
|
|
717
|
+
raise ValueError("operational evidence authoring digest is invalid")
|
|
718
|
+
catalog = strict_json_loads(self.catalog_json)
|
|
719
|
+
if not isinstance(catalog, dict) or self.catalog_json != _canonical_json(catalog):
|
|
720
|
+
raise ValueError("operational evidence catalog must be canonical JSON")
|
|
721
|
+
capture_identifiers = tuple(capture.probe_identifier for capture in self.captures)
|
|
722
|
+
expected_identifiers = tuple(probe.identifier for probe in self.corpus.probes)
|
|
723
|
+
if capture_identifiers != expected_identifiers:
|
|
724
|
+
raise ValueError("operational evidence capture closure does not match its corpus")
|
|
725
|
+
|
|
726
|
+
@property
|
|
727
|
+
def matched_probes(self) -> int:
|
|
728
|
+
return sum(capture.matched for capture in self.captures)
|
|
729
|
+
|
|
730
|
+
@property
|
|
731
|
+
def total_probes(self) -> int:
|
|
732
|
+
return len(self.captures)
|
|
733
|
+
|
|
734
|
+
@property
|
|
735
|
+
def all_matched(self) -> bool:
|
|
736
|
+
return self.matched_probes == self.total_probes
|
|
737
|
+
|
|
738
|
+
def _unsigned_dict(self) -> dict[str, object]:
|
|
739
|
+
return {
|
|
740
|
+
"format": AUTHORING_OPERATIONAL_EVIDENCE_FORMAT,
|
|
741
|
+
"classification": OPERATIONAL_EVIDENCE_CLASSIFICATION,
|
|
742
|
+
"package_version": self.package_version,
|
|
743
|
+
"repository_revision": self.repository_revision,
|
|
744
|
+
"benchmark_evidence_digest": self.benchmark_evidence_digest,
|
|
745
|
+
"corpus": self.corpus.metadata(),
|
|
746
|
+
"provider": self.provider.to_dict(),
|
|
747
|
+
"catalog": strict_json_loads(self.catalog_json),
|
|
748
|
+
"captures": [capture.to_dict() for capture in self.captures],
|
|
749
|
+
"matched_probes": self.matched_probes,
|
|
750
|
+
"total_probes": self.total_probes,
|
|
751
|
+
"all_matched": self.all_matched,
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
@property
|
|
755
|
+
def digest(self) -> str:
|
|
756
|
+
return _sha256(_canonical_json(self._unsigned_dict()))
|
|
757
|
+
|
|
758
|
+
def to_dict(self) -> dict[str, object]:
|
|
759
|
+
evidence_document = self._unsigned_dict()
|
|
760
|
+
evidence_document["digest"] = self.digest
|
|
761
|
+
return evidence_document
|
|
762
|
+
|
|
763
|
+
def to_json(self) -> str:
|
|
764
|
+
return _canonical_json(self.to_dict())
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def run_authoring_operational_evidence(
|
|
768
|
+
serialized_authoring_evidence: str,
|
|
769
|
+
verified_authoring_evidence: AuthoringEvidenceVerification,
|
|
770
|
+
*,
|
|
771
|
+
provider: AuthoringProviderProvenance,
|
|
772
|
+
executor: Callable[[OperationalProbeRequest], OperationalModelResponse],
|
|
773
|
+
) -> AuthoringOperationalEvidence:
|
|
774
|
+
"""Run every packaged probe and retain completed mismatches as evidence."""
|
|
775
|
+
|
|
776
|
+
prepared_probes, catalog = _prepare_probes(
|
|
777
|
+
serialized_authoring_evidence,
|
|
778
|
+
verified_authoring_evidence,
|
|
779
|
+
)
|
|
780
|
+
captures = tuple(
|
|
781
|
+
_capture(prepared_probe, executor(prepared_probe.request))
|
|
782
|
+
for prepared_probe in prepared_probes
|
|
783
|
+
)
|
|
784
|
+
return AuthoringOperationalEvidence(
|
|
785
|
+
package_version=verified_authoring_evidence.package_version,
|
|
786
|
+
repository_revision=verified_authoring_evidence.repository_revision,
|
|
787
|
+
benchmark_evidence_digest=verified_authoring_evidence.digest,
|
|
788
|
+
corpus=load_reference_operational_corpus(),
|
|
789
|
+
provider=provider,
|
|
790
|
+
catalog_json=_canonical_json(catalog),
|
|
791
|
+
captures=captures,
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
@dataclass(frozen=True)
|
|
796
|
+
class AuthoringOperationalEvidenceVerification:
|
|
797
|
+
"""Offline integrity and replay summary for operational evidence."""
|
|
798
|
+
|
|
799
|
+
digest: str
|
|
800
|
+
benchmark_evidence_digest: str
|
|
801
|
+
repository_revision: str
|
|
802
|
+
provider_identifier: str
|
|
803
|
+
requested_model: str
|
|
804
|
+
effective_model_versions: tuple[str, ...]
|
|
805
|
+
matched_probes: int
|
|
806
|
+
total_probes: int
|
|
807
|
+
all_matched: bool
|
|
808
|
+
classification: str = OPERATIONAL_EVIDENCE_CLASSIFICATION
|
|
809
|
+
|
|
810
|
+
def to_dict(self) -> dict[str, object]:
|
|
811
|
+
return {
|
|
812
|
+
"digest": self.digest,
|
|
813
|
+
"benchmark_evidence_digest": self.benchmark_evidence_digest,
|
|
814
|
+
"repository_revision": self.repository_revision,
|
|
815
|
+
"provider_identifier": self.provider_identifier,
|
|
816
|
+
"requested_model": self.requested_model,
|
|
817
|
+
"effective_model_versions": list(self.effective_model_versions),
|
|
818
|
+
"matched_probes": self.matched_probes,
|
|
819
|
+
"total_probes": self.total_probes,
|
|
820
|
+
"all_matched": self.all_matched,
|
|
821
|
+
"classification": self.classification,
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
_operational_evidence_schema_cache: dict[str, Any] | None = None
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
def _operational_evidence_schema() -> dict[str, Any]:
|
|
829
|
+
global _operational_evidence_schema_cache
|
|
830
|
+
if _operational_evidence_schema_cache is None:
|
|
831
|
+
decoded_schema = strict_json_loads(_EVIDENCE_SCHEMA_PATH.read_text(encoding="utf-8"))
|
|
832
|
+
if not isinstance(decoded_schema, dict):
|
|
833
|
+
raise ValueError("operational evidence schema must be a JSON object")
|
|
834
|
+
jsonschema.Draft202012Validator.check_schema(decoded_schema)
|
|
835
|
+
_operational_evidence_schema_cache = cast(dict[str, Any], decoded_schema)
|
|
836
|
+
return _operational_evidence_schema_cache
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
def authoring_operational_evidence_schema() -> dict[str, Any]:
|
|
840
|
+
"""Return an owned closed schema for operational evidence."""
|
|
841
|
+
|
|
842
|
+
return copy.deepcopy(_operational_evidence_schema())
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
def _provider_from_document(provider_document: dict[str, Any]) -> AuthoringProviderProvenance:
|
|
846
|
+
if _contains_secret_key(provider_document.get("generation_configuration")):
|
|
847
|
+
raise ValueError("operational provider configuration contains a secret field")
|
|
848
|
+
return AuthoringProviderProvenance.from_configuration(
|
|
849
|
+
identifier=cast(str, provider_document["identifier"]),
|
|
850
|
+
model=cast(str, provider_document["model"]),
|
|
851
|
+
sdk=cast(str, provider_document["sdk"]),
|
|
852
|
+
sdk_version=cast(str, provider_document["sdk_version"]),
|
|
853
|
+
prompt_format=cast(str, provider_document["prompt_format"]),
|
|
854
|
+
prompt_digest=cast(str, provider_document["prompt_digest"]),
|
|
855
|
+
generation_configuration=cast(
|
|
856
|
+
dict[str, object], provider_document["generation_configuration"]
|
|
857
|
+
),
|
|
858
|
+
)
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def verify_authoring_operational_evidence(
|
|
862
|
+
serialized_operational_evidence: str,
|
|
863
|
+
serialized_authoring_evidence: str,
|
|
864
|
+
verified_authoring_evidence: AuthoringEvidenceVerification,
|
|
865
|
+
) -> AuthoringOperationalEvidenceVerification:
|
|
866
|
+
"""Verify exact closure and replay captured model outputs without network access."""
|
|
867
|
+
|
|
868
|
+
decoded_evidence = strict_json_loads(serialized_operational_evidence)
|
|
869
|
+
jsonschema.Draft202012Validator(_operational_evidence_schema()).validate(decoded_evidence)
|
|
870
|
+
if not isinstance(decoded_evidence, dict):
|
|
871
|
+
raise AssertionError("operational evidence schema accepted a non-object")
|
|
872
|
+
evidence_document = cast(dict[str, Any], decoded_evidence)
|
|
873
|
+
if serialized_operational_evidence.strip() != _canonical_json(evidence_document):
|
|
874
|
+
raise ValueError("operational evidence must use canonical compact JSON")
|
|
875
|
+
unsigned_document = dict(evidence_document)
|
|
876
|
+
recorded_digest = cast(str, unsigned_document.pop("digest"))
|
|
877
|
+
if recorded_digest != _sha256(_canonical_json(unsigned_document)):
|
|
878
|
+
raise ValueError("operational evidence digest does not match its content")
|
|
879
|
+
if evidence_document["package_version"] != package_version("flowspec2"):
|
|
880
|
+
raise ValueError("operational evidence package version does not match verifier")
|
|
881
|
+
if evidence_document["benchmark_evidence_digest"] != verified_authoring_evidence.digest:
|
|
882
|
+
raise ValueError("operational evidence is bound to different authoring evidence")
|
|
883
|
+
if evidence_document["repository_revision"] != verified_authoring_evidence.repository_revision:
|
|
884
|
+
raise ValueError("operational evidence repository revision is inconsistent")
|
|
885
|
+
corpus = load_reference_operational_corpus()
|
|
886
|
+
if evidence_document["corpus"] != corpus.metadata():
|
|
887
|
+
raise ValueError("operational evidence corpus does not match installed corpus")
|
|
888
|
+
prepared_probes, expected_catalog = _prepare_probes(
|
|
889
|
+
serialized_authoring_evidence,
|
|
890
|
+
verified_authoring_evidence,
|
|
891
|
+
)
|
|
892
|
+
if evidence_document["catalog"] != expected_catalog:
|
|
893
|
+
raise ValueError("operational evidence source catalog does not match authoring evidence")
|
|
894
|
+
provider_document = cast(dict[str, Any], evidence_document["provider"])
|
|
895
|
+
provider = _provider_from_document(provider_document)
|
|
896
|
+
if (
|
|
897
|
+
provider.prompt_format != OPERATIONAL_PROMPT_FORMAT
|
|
898
|
+
or provider.prompt_digest != OPERATIONAL_PROMPT_DIGEST
|
|
899
|
+
):
|
|
900
|
+
raise ValueError("operational provider prompt contract is unsupported")
|
|
901
|
+
capture_documents = cast(list[dict[str, Any]], evidence_document["captures"])
|
|
902
|
+
if len(capture_documents) != len(prepared_probes):
|
|
903
|
+
raise ValueError("operational evidence capture count is inconsistent")
|
|
904
|
+
replayed_captures: list[OperationalCapture] = []
|
|
905
|
+
for capture_document, prepared_probe in zip(
|
|
906
|
+
capture_documents,
|
|
907
|
+
prepared_probes,
|
|
908
|
+
strict=True,
|
|
909
|
+
):
|
|
910
|
+
replayed_capture = _capture(
|
|
911
|
+
prepared_probe,
|
|
912
|
+
OperationalModelResponse(
|
|
913
|
+
raw_output=cast(str, capture_document["raw_output"]),
|
|
914
|
+
effective_model_version=cast(
|
|
915
|
+
str | None, capture_document["effective_model_version"]
|
|
916
|
+
),
|
|
917
|
+
),
|
|
918
|
+
)
|
|
919
|
+
if replayed_capture.to_dict() != capture_document:
|
|
920
|
+
raise ValueError(
|
|
921
|
+
f"operational capture does not match offline replay: "
|
|
922
|
+
f"{prepared_probe.probe.identifier}"
|
|
923
|
+
)
|
|
924
|
+
replayed_captures.append(replayed_capture)
|
|
925
|
+
if provider.identifier == "google_gemini" and any(
|
|
926
|
+
capture.effective_model_version is None for capture in replayed_captures
|
|
927
|
+
):
|
|
928
|
+
raise ValueError("Gemini operational evidence requires effective model versions")
|
|
929
|
+
matched_probes = sum(capture.matched for capture in replayed_captures)
|
|
930
|
+
total_probes = len(replayed_captures)
|
|
931
|
+
if (
|
|
932
|
+
evidence_document["matched_probes"] != matched_probes
|
|
933
|
+
or evidence_document["total_probes"] != total_probes
|
|
934
|
+
or evidence_document["all_matched"] != (matched_probes == total_probes)
|
|
935
|
+
):
|
|
936
|
+
raise ValueError("operational evidence summary does not match replay")
|
|
937
|
+
effective_models = tuple(
|
|
938
|
+
sorted(
|
|
939
|
+
{
|
|
940
|
+
capture.effective_model_version
|
|
941
|
+
for capture in replayed_captures
|
|
942
|
+
if capture.effective_model_version is not None
|
|
943
|
+
}
|
|
944
|
+
)
|
|
945
|
+
)
|
|
946
|
+
return AuthoringOperationalEvidenceVerification(
|
|
947
|
+
digest=recorded_digest,
|
|
948
|
+
benchmark_evidence_digest=verified_authoring_evidence.digest,
|
|
949
|
+
repository_revision=verified_authoring_evidence.repository_revision,
|
|
950
|
+
provider_identifier=provider.identifier,
|
|
951
|
+
requested_model=provider.model,
|
|
952
|
+
effective_model_versions=effective_models,
|
|
953
|
+
matched_probes=matched_probes,
|
|
954
|
+
total_probes=total_probes,
|
|
955
|
+
all_matched=matched_probes == total_probes,
|
|
956
|
+
)
|