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,229 @@
|
|
|
1
|
+
"""Strict offline verification and deterministic replay of authoring evidence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from importlib.metadata import version as package_version
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Final, cast
|
|
14
|
+
|
|
15
|
+
import jsonschema
|
|
16
|
+
|
|
17
|
+
from flowspec2.json_codec import strict_json_loads
|
|
18
|
+
from flowspec2.profiles import reference_profile
|
|
19
|
+
|
|
20
|
+
from .benchmark import replay_authoring_benchmark
|
|
21
|
+
from .corpus import load_reference_authoring_corpus
|
|
22
|
+
from .evidence import AUTHORING_EVIDENCE_FORMAT, SOURCE_ADAPTER_CONTRACT
|
|
23
|
+
|
|
24
|
+
_EVIDENCE_SCHEMA_PATH: Final[Path] = Path(__file__).with_name("authoring-evidence.schema.json")
|
|
25
|
+
_SECRET_KEY_PATTERN: Final[re.Pattern[str]] = re.compile(
|
|
26
|
+
r"(?:api[_-]?key|authorization|credential|password|secret|access[_-]?token)",
|
|
27
|
+
re.IGNORECASE,
|
|
28
|
+
)
|
|
29
|
+
_evidence_schema_cache: dict[str, Any] | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _canonical_json(json_document: object) -> str:
|
|
33
|
+
return json.dumps(
|
|
34
|
+
json_document,
|
|
35
|
+
ensure_ascii=False,
|
|
36
|
+
allow_nan=False,
|
|
37
|
+
separators=(",", ":"),
|
|
38
|
+
sort_keys=True,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _sha256(serialized_contract: str) -> str:
|
|
43
|
+
return hashlib.sha256(serialized_contract.encode("utf-8")).hexdigest()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _contains_secret_key(json_document: object) -> bool:
|
|
47
|
+
if isinstance(json_document, Mapping):
|
|
48
|
+
return any(
|
|
49
|
+
_SECRET_KEY_PATTERN.search(str(property_name)) is not None
|
|
50
|
+
or _contains_secret_key(property_value)
|
|
51
|
+
for property_name, property_value in json_document.items()
|
|
52
|
+
)
|
|
53
|
+
if isinstance(json_document, list):
|
|
54
|
+
return any(_contains_secret_key(element_value) for element_value in json_document)
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _evidence_schema() -> dict[str, Any]:
|
|
59
|
+
global _evidence_schema_cache
|
|
60
|
+
cached_schema = _evidence_schema_cache
|
|
61
|
+
if cached_schema is None:
|
|
62
|
+
decoded_schema = strict_json_loads(_EVIDENCE_SCHEMA_PATH.read_text(encoding="utf-8"))
|
|
63
|
+
if not isinstance(decoded_schema, dict):
|
|
64
|
+
raise ValueError("authoring evidence schema must be a JSON object")
|
|
65
|
+
jsonschema.Draft202012Validator.check_schema(decoded_schema)
|
|
66
|
+
cached_schema = cast(dict[str, Any], decoded_schema)
|
|
67
|
+
_evidence_schema_cache = cached_schema
|
|
68
|
+
return cached_schema
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def authoring_evidence_schema() -> dict[str, Any]:
|
|
72
|
+
"""Return an owned copy of the closed evidence schema."""
|
|
73
|
+
|
|
74
|
+
return copy.deepcopy(_evidence_schema())
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class AuthoringEvidenceVerification:
|
|
79
|
+
"""Verified deterministic identity and replay summary for one artifact."""
|
|
80
|
+
|
|
81
|
+
digest: str
|
|
82
|
+
benchmark_identifier: str
|
|
83
|
+
repository_revision: str
|
|
84
|
+
package_version: str
|
|
85
|
+
provider_identifier: str
|
|
86
|
+
requested_model: str
|
|
87
|
+
effective_model_versions: tuple[str, ...]
|
|
88
|
+
successful_cases: int
|
|
89
|
+
total_cases: int
|
|
90
|
+
total_attempts: int
|
|
91
|
+
|
|
92
|
+
def to_dict(self) -> dict[str, object]:
|
|
93
|
+
"""Return the stable machine-readable verification summary."""
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
"digest": self.digest,
|
|
97
|
+
"benchmark_identifier": self.benchmark_identifier,
|
|
98
|
+
"repository_revision": self.repository_revision,
|
|
99
|
+
"package_version": self.package_version,
|
|
100
|
+
"provider_identifier": self.provider_identifier,
|
|
101
|
+
"requested_model": self.requested_model,
|
|
102
|
+
"effective_model_versions": list(self.effective_model_versions),
|
|
103
|
+
"successful_cases": self.successful_cases,
|
|
104
|
+
"total_cases": self.total_cases,
|
|
105
|
+
"total_attempts": self.total_attempts,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def verify_authoring_evidence(
|
|
110
|
+
serialized_evidence: str,
|
|
111
|
+
*,
|
|
112
|
+
expected_repository_revision: str | None = None,
|
|
113
|
+
) -> AuthoringEvidenceVerification:
|
|
114
|
+
"""Validate provenance and replay every exact capture against packaged contracts."""
|
|
115
|
+
|
|
116
|
+
decoded_evidence = strict_json_loads(serialized_evidence)
|
|
117
|
+
jsonschema.Draft202012Validator(_evidence_schema()).validate(decoded_evidence)
|
|
118
|
+
if not isinstance(decoded_evidence, dict):
|
|
119
|
+
raise AssertionError("the evidence schema accepted a non-object document")
|
|
120
|
+
evidence_document = cast(dict[str, Any], decoded_evidence)
|
|
121
|
+
if serialized_evidence.strip() != _canonical_json(evidence_document):
|
|
122
|
+
raise ValueError("authoring evidence must use canonical compact JSON")
|
|
123
|
+
if evidence_document["format"] != AUTHORING_EVIDENCE_FORMAT:
|
|
124
|
+
raise ValueError("authoring evidence format is unsupported")
|
|
125
|
+
|
|
126
|
+
unsigned_document = dict(evidence_document)
|
|
127
|
+
recorded_digest = cast(str, unsigned_document.pop("digest"))
|
|
128
|
+
computed_digest = _sha256(_canonical_json(unsigned_document))
|
|
129
|
+
if recorded_digest != computed_digest:
|
|
130
|
+
raise ValueError("authoring evidence digest does not match its content")
|
|
131
|
+
|
|
132
|
+
installed_package_version = package_version("flowspec2")
|
|
133
|
+
recorded_package_version = cast(str, evidence_document["package_version"])
|
|
134
|
+
if recorded_package_version != installed_package_version:
|
|
135
|
+
raise ValueError("authoring evidence package version does not match the installed verifier")
|
|
136
|
+
repository_revision = cast(str, evidence_document["repository_revision"])
|
|
137
|
+
if (
|
|
138
|
+
expected_repository_revision is not None
|
|
139
|
+
and repository_revision != expected_repository_revision
|
|
140
|
+
):
|
|
141
|
+
raise ValueError("authoring evidence repository revision does not match the expected value")
|
|
142
|
+
|
|
143
|
+
reference_corpus = load_reference_authoring_corpus()
|
|
144
|
+
if evidence_document["corpus"] != reference_corpus.metadata():
|
|
145
|
+
raise ValueError("authoring evidence corpus does not match the installed corpus")
|
|
146
|
+
flow_profile = reference_profile()
|
|
147
|
+
expected_profile = {
|
|
148
|
+
"identifier": flow_profile.identifier,
|
|
149
|
+
"digest": flow_profile.digest,
|
|
150
|
+
}
|
|
151
|
+
if evidence_document["profile"] != expected_profile:
|
|
152
|
+
raise ValueError("authoring evidence profile does not match the installed profile")
|
|
153
|
+
if evidence_document["source_adapter"] != SOURCE_ADAPTER_CONTRACT:
|
|
154
|
+
raise ValueError("authoring evidence source adapter contract is unsupported")
|
|
155
|
+
|
|
156
|
+
provider_document = cast(dict[str, Any], evidence_document["provider"])
|
|
157
|
+
if _contains_secret_key(provider_document["generation_configuration"]):
|
|
158
|
+
raise ValueError("authoring evidence provider configuration contains a secret field")
|
|
159
|
+
capture_documents = cast(list[dict[str, Any]], evidence_document["captures"])
|
|
160
|
+
if provider_document["identifier"] == "google_gemini" and any(
|
|
161
|
+
capture_document["effective_model_version"] is None
|
|
162
|
+
for capture_document in capture_documents
|
|
163
|
+
):
|
|
164
|
+
raise ValueError("Gemini authoring evidence requires effective model versions")
|
|
165
|
+
|
|
166
|
+
report_document = cast(dict[str, Any], evidence_document["report"])
|
|
167
|
+
benchmark_document = cast(dict[str, int], evidence_document["benchmark"])
|
|
168
|
+
case_result_documents = cast(list[dict[str, Any]], report_document["case_results"])
|
|
169
|
+
if any(
|
|
170
|
+
len(cast(list[dict[str, Any]], case_result_document["attempts"]))
|
|
171
|
+
> benchmark_document["max_correction_rounds"] + 1
|
|
172
|
+
for case_result_document in case_result_documents
|
|
173
|
+
):
|
|
174
|
+
raise ValueError("authoring evidence report exceeds its correction-round limit")
|
|
175
|
+
expected_capture_keys = tuple(
|
|
176
|
+
(case_result_document["case_identifier"], attempt_document["correction_round"])
|
|
177
|
+
for case_result_document in case_result_documents
|
|
178
|
+
for attempt_document in cast(list[dict[str, Any]], case_result_document["attempts"])
|
|
179
|
+
)
|
|
180
|
+
actual_capture_keys = tuple(
|
|
181
|
+
(capture_document["case_identifier"], capture_document["correction_round"])
|
|
182
|
+
for capture_document in capture_documents
|
|
183
|
+
)
|
|
184
|
+
if actual_capture_keys != expected_capture_keys:
|
|
185
|
+
raise ValueError("authoring evidence captures do not align with report attempts")
|
|
186
|
+
for capture_document in capture_documents:
|
|
187
|
+
authored_source = cast(str, capture_document["authored_source"])
|
|
188
|
+
if capture_document["source_sha256"] != _sha256(authored_source):
|
|
189
|
+
raise ValueError("authoring evidence capture source digest does not match its source")
|
|
190
|
+
|
|
191
|
+
captured_sources: dict[str, list[str]] = {
|
|
192
|
+
benchmark_case.identifier: [] for benchmark_case in reference_corpus.cases
|
|
193
|
+
}
|
|
194
|
+
for capture_document in capture_documents:
|
|
195
|
+
case_identifier = cast(str, capture_document["case_identifier"])
|
|
196
|
+
if case_identifier not in captured_sources:
|
|
197
|
+
raise ValueError("authoring evidence capture case is not in the installed corpus")
|
|
198
|
+
captured_sources[case_identifier].append(cast(str, capture_document["authored_source"]))
|
|
199
|
+
|
|
200
|
+
replayed_report = replay_authoring_benchmark(
|
|
201
|
+
cast(str, report_document["benchmark_identifier"]),
|
|
202
|
+
reference_corpus.cases,
|
|
203
|
+
captured_sources,
|
|
204
|
+
profile=flow_profile,
|
|
205
|
+
)
|
|
206
|
+
if replayed_report.to_dict() != report_document:
|
|
207
|
+
raise ValueError("authoring evidence report does not match deterministic replay")
|
|
208
|
+
|
|
209
|
+
effective_model_versions = tuple(
|
|
210
|
+
sorted(
|
|
211
|
+
{
|
|
212
|
+
cast(str, capture_document["effective_model_version"])
|
|
213
|
+
for capture_document in capture_documents
|
|
214
|
+
if capture_document["effective_model_version"] is not None
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
return AuthoringEvidenceVerification(
|
|
219
|
+
digest=recorded_digest,
|
|
220
|
+
benchmark_identifier=replayed_report.benchmark_identifier,
|
|
221
|
+
repository_revision=repository_revision,
|
|
222
|
+
package_version=recorded_package_version,
|
|
223
|
+
provider_identifier=cast(str, provider_document["identifier"]),
|
|
224
|
+
requested_model=cast(str, provider_document["model"]),
|
|
225
|
+
effective_model_versions=effective_model_versions,
|
|
226
|
+
successful_cases=replayed_report.successful_cases,
|
|
227
|
+
total_cases=replayed_report.total_cases,
|
|
228
|
+
total_attempts=replayed_report.total_attempts,
|
|
229
|
+
)
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Optional Gemini transport for the provider-neutral authoring benchmark."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
8
|
+
from typing import Final, Protocol, cast
|
|
9
|
+
|
|
10
|
+
from jsonschema import Draft202012Validator
|
|
11
|
+
|
|
12
|
+
from flowspec2.json_codec import StrictJsonError, strict_json_loads
|
|
13
|
+
|
|
14
|
+
from .benchmark import AuthoredSource, AuthoringRequest
|
|
15
|
+
from .evidence import AuthoringProviderProvenance
|
|
16
|
+
from .projection import authoring_projection
|
|
17
|
+
from .provider_prompt import (
|
|
18
|
+
AUTHORING_SYSTEM_INSTRUCTION,
|
|
19
|
+
authoring_prompt,
|
|
20
|
+
authoring_prompt_digest,
|
|
21
|
+
canonical_json,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
DEFAULT_GEMINI_AUTHOR_MODEL: Final[str] = "gemini-2.5-flash"
|
|
25
|
+
DEFAULT_GEMINI_AUTHOR_TIMEOUT_MILLISECONDS: Final[int] = 120_000
|
|
26
|
+
GEMINI_AUTHOR_PROMPT_FORMAT: Final[str] = "flowspec2/gemini-author-prompt@3"
|
|
27
|
+
|
|
28
|
+
_PROVIDER_IDENTIFIER: Final[str] = "google_gemini"
|
|
29
|
+
_SDK_NAME: Final[str] = "google-genai"
|
|
30
|
+
_SYSTEM_INSTRUCTION: Final[str] = AUTHORING_SYSTEM_INSTRUCTION
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _canonical_json(json_document: object) -> str:
|
|
34
|
+
return canonical_json(json_document)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
GEMINI_AUTHOR_PROMPT_DIGEST: Final[str] = authoring_prompt_digest(GEMINI_AUTHOR_PROMPT_FORMAT)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class _GeminiModels(Protocol):
|
|
41
|
+
def generate_content(
|
|
42
|
+
self,
|
|
43
|
+
*,
|
|
44
|
+
model: str,
|
|
45
|
+
contents: str,
|
|
46
|
+
config: dict[str, object],
|
|
47
|
+
) -> object: ...
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class _GeminiClient(Protocol):
|
|
51
|
+
@property
|
|
52
|
+
def models(self) -> _GeminiModels: ...
|
|
53
|
+
|
|
54
|
+
def close(self) -> None: ...
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class GeminiAuthorError(RuntimeError):
|
|
58
|
+
"""The optional provider boundary could not return a usable source envelope."""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _authoring_prompt(authoring_request: AuthoringRequest) -> str:
|
|
62
|
+
return authoring_prompt(authoring_request, GEMINI_AUTHOR_PROMPT_FORMAT)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _default_client(api_key: str | None) -> _GeminiClient:
|
|
66
|
+
resolved_api_key = api_key or os.environ.get("GEMINI_API_KEY")
|
|
67
|
+
if not resolved_api_key:
|
|
68
|
+
raise GeminiAuthorError(
|
|
69
|
+
"Gemini authoring requires GEMINI_API_KEY after explicit network opt-in."
|
|
70
|
+
)
|
|
71
|
+
try:
|
|
72
|
+
from google import genai
|
|
73
|
+
except ImportError as import_error:
|
|
74
|
+
raise GeminiAuthorError(
|
|
75
|
+
"Gemini authoring requires the optional flowspec2[llm] dependency."
|
|
76
|
+
) from import_error
|
|
77
|
+
try:
|
|
78
|
+
return cast(
|
|
79
|
+
_GeminiClient,
|
|
80
|
+
genai.Client(
|
|
81
|
+
api_key=resolved_api_key,
|
|
82
|
+
http_options={"timeout": DEFAULT_GEMINI_AUTHOR_TIMEOUT_MILLISECONDS},
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
except Exception as client_error:
|
|
86
|
+
raise GeminiAuthorError("Gemini author client initialization failed") from client_error
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _sdk_version() -> str:
|
|
90
|
+
try:
|
|
91
|
+
return version(_SDK_NAME)
|
|
92
|
+
except PackageNotFoundError:
|
|
93
|
+
return "injected-client"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class GeminiAuthor:
|
|
97
|
+
"""Generate benchmark sources through a closed Gemini projection envelope."""
|
|
98
|
+
|
|
99
|
+
def __init__(
|
|
100
|
+
self,
|
|
101
|
+
model: str = DEFAULT_GEMINI_AUTHOR_MODEL,
|
|
102
|
+
*,
|
|
103
|
+
api_key: str | None = None,
|
|
104
|
+
client: _GeminiClient | None = None,
|
|
105
|
+
) -> None:
|
|
106
|
+
if not model.strip():
|
|
107
|
+
raise ValueError("Gemini author model must be non-empty")
|
|
108
|
+
if api_key is not None and client is not None:
|
|
109
|
+
raise ValueError("Gemini author cannot combine an API key with an injected client")
|
|
110
|
+
self.model = model
|
|
111
|
+
self._client = client if client is not None else _default_client(api_key)
|
|
112
|
+
self._owns_client = client is None
|
|
113
|
+
self._closed = False
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def generation_configuration(self) -> dict[str, object]:
|
|
117
|
+
projection_artifact = authoring_projection()
|
|
118
|
+
return {
|
|
119
|
+
"response_mime_type": "application/json",
|
|
120
|
+
"projection_schema_digest": projection_artifact.schema_digest,
|
|
121
|
+
"seed": 0,
|
|
122
|
+
"temperature": 0.0,
|
|
123
|
+
"timeout_milliseconds": DEFAULT_GEMINI_AUTHOR_TIMEOUT_MILLISECONDS,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
def provenance(self) -> AuthoringProviderProvenance:
|
|
127
|
+
return AuthoringProviderProvenance.from_configuration(
|
|
128
|
+
identifier=_PROVIDER_IDENTIFIER,
|
|
129
|
+
model=self.model,
|
|
130
|
+
sdk=_SDK_NAME,
|
|
131
|
+
sdk_version=_sdk_version(),
|
|
132
|
+
prompt_format=GEMINI_AUTHOR_PROMPT_FORMAT,
|
|
133
|
+
prompt_digest=GEMINI_AUTHOR_PROMPT_DIGEST,
|
|
134
|
+
generation_configuration=self.generation_configuration,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def __call__(self, authoring_request: AuthoringRequest) -> AuthoredSource:
|
|
138
|
+
if self._closed:
|
|
139
|
+
raise GeminiAuthorError("Gemini author is closed")
|
|
140
|
+
if authoring_request.format_identifier != "flowspec/2":
|
|
141
|
+
raise GeminiAuthorError("Gemini author supports only the flowspec/2 source adapter")
|
|
142
|
+
projection_artifact = authoring_projection()
|
|
143
|
+
provider_configuration: dict[str, object] = {
|
|
144
|
+
"system_instruction": _SYSTEM_INSTRUCTION,
|
|
145
|
+
"response_mime_type": "application/json",
|
|
146
|
+
"response_json_schema": projection_artifact.schema(),
|
|
147
|
+
"temperature": 0.0,
|
|
148
|
+
"seed": 0,
|
|
149
|
+
}
|
|
150
|
+
try:
|
|
151
|
+
provider_response = self._client.models.generate_content(
|
|
152
|
+
model=self.model,
|
|
153
|
+
contents=_authoring_prompt(authoring_request),
|
|
154
|
+
config=provider_configuration,
|
|
155
|
+
)
|
|
156
|
+
except Exception as provider_error:
|
|
157
|
+
status_code = getattr(
|
|
158
|
+
provider_error,
|
|
159
|
+
"status_code",
|
|
160
|
+
getattr(provider_error, "code", None),
|
|
161
|
+
)
|
|
162
|
+
status_suffix = f", status={status_code}" if isinstance(status_code, int) else ""
|
|
163
|
+
raise GeminiAuthorError(
|
|
164
|
+
"Gemini author request failed for "
|
|
165
|
+
f"{authoring_request.task.identifier!r} correction round "
|
|
166
|
+
f"{authoring_request.correction_round} "
|
|
167
|
+
f"({type(provider_error).__name__}{status_suffix})."
|
|
168
|
+
) from provider_error
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
response_text = getattr(provider_response, "text", None)
|
|
172
|
+
effective_model_version = getattr(provider_response, "model_version", None)
|
|
173
|
+
except Exception as response_error:
|
|
174
|
+
raise GeminiAuthorError("Gemini author response could not be read") from response_error
|
|
175
|
+
if not isinstance(response_text, str) or not response_text:
|
|
176
|
+
raise GeminiAuthorError("Gemini author returned an empty projection envelope")
|
|
177
|
+
if not isinstance(effective_model_version, str) or not effective_model_version.strip():
|
|
178
|
+
raise GeminiAuthorError("Gemini author response omitted its effective model version")
|
|
179
|
+
try:
|
|
180
|
+
projected_document = strict_json_loads(response_text)
|
|
181
|
+
except (json.JSONDecodeError, StrictJsonError) as decoding_error:
|
|
182
|
+
raise GeminiAuthorError(
|
|
183
|
+
"Gemini author returned an invalid projection envelope"
|
|
184
|
+
) from decoding_error
|
|
185
|
+
envelope_errors = tuple(
|
|
186
|
+
Draft202012Validator(projection_artifact.schema()).iter_errors(projected_document)
|
|
187
|
+
)
|
|
188
|
+
if envelope_errors:
|
|
189
|
+
raise GeminiAuthorError("Gemini author returned an incompatible projection envelope")
|
|
190
|
+
if not isinstance(projected_document, dict):
|
|
191
|
+
raise AssertionError("projection schema accepted a non-object response")
|
|
192
|
+
return AuthoredSource(
|
|
193
|
+
source=cast(str, projected_document["flow_document_json"]),
|
|
194
|
+
effective_model_version=effective_model_version,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def close(self) -> None:
|
|
198
|
+
if self._closed:
|
|
199
|
+
return
|
|
200
|
+
if self._owns_client:
|
|
201
|
+
try:
|
|
202
|
+
self._client.close()
|
|
203
|
+
except Exception as close_error:
|
|
204
|
+
raise GeminiAuthorError(
|
|
205
|
+
"Gemini author client could not close cleanly"
|
|
206
|
+
) from close_error
|
|
207
|
+
self._closed = True
|
|
208
|
+
|
|
209
|
+
def __enter__(self) -> GeminiAuthor:
|
|
210
|
+
if self._closed:
|
|
211
|
+
raise GeminiAuthorError("Gemini author is closed")
|
|
212
|
+
return self
|
|
213
|
+
|
|
214
|
+
def __exit__(self, _exception_type: object, _exception: object, _traceback: object) -> None:
|
|
215
|
+
self.close()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"format": "flowspec2/operational-corpus@2",
|
|
3
|
+
"case_format": "flowspec2/operational-probe@2",
|
|
4
|
+
"identifier": "flowspec2_reference_operational",
|
|
5
|
+
"probes": [
|
|
6
|
+
{
|
|
7
|
+
"identifier": "linear-extract-authored",
|
|
8
|
+
"pair_identifier": "linear-extract-hint",
|
|
9
|
+
"subject_mode": "authored",
|
|
10
|
+
"operation": "extract",
|
|
11
|
+
"authoring_case_identifier": "linear_collection",
|
|
12
|
+
"subject_pointer": "/path/0/prompt/extract_hint",
|
|
13
|
+
"citizen_input": "ALPHA=[broken streetlight]; ZETA=[playground swing is broken]",
|
|
14
|
+
"expected": {
|
|
15
|
+
"request_description": "playground swing is broken"
|
|
16
|
+
},
|
|
17
|
+
"counterfactual": null
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"identifier": "linear-extract-counterfactual",
|
|
21
|
+
"pair_identifier": "linear-extract-hint",
|
|
22
|
+
"subject_mode": "counterfactual",
|
|
23
|
+
"operation": "extract",
|
|
24
|
+
"authoring_case_identifier": "linear_collection",
|
|
25
|
+
"subject_pointer": "/path/0/prompt/extract_hint",
|
|
26
|
+
"citizen_input": "ALPHA=[broken streetlight]; ZETA=[playground swing is broken]",
|
|
27
|
+
"expected": {
|
|
28
|
+
"request_description": "broken streetlight"
|
|
29
|
+
},
|
|
30
|
+
"counterfactual": "Set request_description to only the text inside `ALPHA=[...]`; ignore `ZETA=[...]`."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"identifier": "linear-route-authored",
|
|
34
|
+
"pair_identifier": "linear-route-triggers",
|
|
35
|
+
"subject_mode": "authored",
|
|
36
|
+
"operation": "route",
|
|
37
|
+
"authoring_case_identifier": "linear_collection",
|
|
38
|
+
"subject_pointer": "/route/trigger_phrases",
|
|
39
|
+
"citizen_input": "cobalt lantern protocol",
|
|
40
|
+
"expected": {
|
|
41
|
+
"selected": true
|
|
42
|
+
},
|
|
43
|
+
"counterfactual": null
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"identifier": "linear-route-counterfactual",
|
|
47
|
+
"pair_identifier": "linear-route-triggers",
|
|
48
|
+
"subject_mode": "counterfactual",
|
|
49
|
+
"operation": "route",
|
|
50
|
+
"authoring_case_identifier": "linear_collection",
|
|
51
|
+
"subject_pointer": "/route/trigger_phrases",
|
|
52
|
+
"citizen_input": "cobalt lantern protocol",
|
|
53
|
+
"expected": {
|
|
54
|
+
"selected": false
|
|
55
|
+
},
|
|
56
|
+
"counterfactual": [
|
|
57
|
+
"amber compass protocol"
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|