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,616 @@
|
|
|
1
|
+
"""Lossless flowspec2 profile for Open Workflow Specification 1.0.3."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
from copy import deepcopy
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Mapping, Never
|
|
9
|
+
|
|
10
|
+
import jsonschema
|
|
11
|
+
|
|
12
|
+
from flowspec2.compat.models import (
|
|
13
|
+
CompatibilityDiagnostic,
|
|
14
|
+
CompatibilityError,
|
|
15
|
+
CompatibilityReport,
|
|
16
|
+
ConversionOutcome,
|
|
17
|
+
)
|
|
18
|
+
from flowspec2.compat.tool_profiles import (
|
|
19
|
+
compatibility_tool_names,
|
|
20
|
+
synthetic_compatibility_tool_definition,
|
|
21
|
+
)
|
|
22
|
+
from flowspec2.compat.yaml import load_yaml_mapping
|
|
23
|
+
from flowspec2.compiler import compile_flow
|
|
24
|
+
from flowspec2.json_codec import strict_json_loads
|
|
25
|
+
from flowspec2.schema import schema as flowspec_schema
|
|
26
|
+
from flowspec2.tools import ToolRegistry, default_tool_registry
|
|
27
|
+
|
|
28
|
+
OPEN_WORKFLOW_PROFILE_ID = (
|
|
29
|
+
"https://wllsena.github.io/flowspec2/profiles/open-workflow-conversation-1"
|
|
30
|
+
)
|
|
31
|
+
OPEN_WORKFLOW_SCHEMA_VERSION = "1.0.3"
|
|
32
|
+
|
|
33
|
+
_OPEN_WORKFLOW_DSL_VERSION = OPEN_WORKFLOW_SCHEMA_VERSION
|
|
34
|
+
_OPEN_WORKFLOW_FORMAT = "open-workflow/1.0.3"
|
|
35
|
+
_FLOWSPEC_FORMAT = "flowspec/2"
|
|
36
|
+
_DEFAULT_NAMESPACE = "flowspec2"
|
|
37
|
+
_OFFICIAL_SCHEMA_COMMIT = "9b5b1da29e9d4fff2358580241e11aab22704a16"
|
|
38
|
+
_PROFILE_SCHEMA_PATH = (
|
|
39
|
+
Path(__file__).with_name("schemas") / "open-workflow-conversation-1.schema.json"
|
|
40
|
+
)
|
|
41
|
+
_VENDOR_SCHEMA_DIRECTORY = Path(__file__).with_name("schemas") / "vendor"
|
|
42
|
+
_OFFICIAL_SCHEMA_PATH = _VENDOR_SCHEMA_DIRECTORY / "open-workflow-1.0.3.workflow.yaml"
|
|
43
|
+
_OFFICIAL_SCHEMA_LICENSE_PATH = _VENDOR_SCHEMA_DIRECTORY / "open-workflow-1.0.3.LICENSE"
|
|
44
|
+
_OFFICIAL_SCHEMA_PROVENANCE_PATH = _VENDOR_SCHEMA_DIRECTORY / "open-workflow-1.0.3.provenance.json"
|
|
45
|
+
_STEP_KINDS = (
|
|
46
|
+
"slot",
|
|
47
|
+
"confirm",
|
|
48
|
+
"derive",
|
|
49
|
+
"terminal",
|
|
50
|
+
"use",
|
|
51
|
+
"await_external",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
_profile_schema_cache: dict[str, Any] | None = None
|
|
55
|
+
_official_schema_cache: dict[str, Any] | None = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def _compatibility_profile_tool(**_inputs: Any) -> dict[str, Any]:
|
|
59
|
+
"""Stand in for a preserved tool while the adapter checks compilation only."""
|
|
60
|
+
|
|
61
|
+
return {}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _compatibility_tool_registry(flow_document: Mapping[str, Any]) -> ToolRegistry:
|
|
65
|
+
"""Register only tools named by the preserved artifact under review."""
|
|
66
|
+
|
|
67
|
+
registry = ToolRegistry()
|
|
68
|
+
known_definitions = default_tool_registry().definitions
|
|
69
|
+
for tool_name in compatibility_tool_names(flow_document):
|
|
70
|
+
registry.register(
|
|
71
|
+
tool_name,
|
|
72
|
+
_compatibility_profile_tool,
|
|
73
|
+
definition=known_definitions.get(tool_name)
|
|
74
|
+
or synthetic_compatibility_tool_definition(
|
|
75
|
+
tool_name,
|
|
76
|
+
flow_document,
|
|
77
|
+
version="open-workflow-profile",
|
|
78
|
+
description=(
|
|
79
|
+
"Synthetic compile-only contract derived from the preserved "
|
|
80
|
+
"Open Workflow profile."
|
|
81
|
+
),
|
|
82
|
+
),
|
|
83
|
+
)
|
|
84
|
+
return registry
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _load_json_mapping(document_path: Path) -> dict[str, Any]:
|
|
88
|
+
loaded_document = strict_json_loads(document_path.read_text(encoding="utf-8"))
|
|
89
|
+
if not isinstance(loaded_document, dict):
|
|
90
|
+
raise RuntimeError(f"expected a JSON object in {document_path}")
|
|
91
|
+
return dict(loaded_document)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _verify_vendor_artifact(
|
|
95
|
+
artifact_path: Path,
|
|
96
|
+
*,
|
|
97
|
+
expected_sha256: object,
|
|
98
|
+
) -> None:
|
|
99
|
+
if not isinstance(expected_sha256, str):
|
|
100
|
+
raise RuntimeError(f"missing SHA-256 provenance for {artifact_path.name}")
|
|
101
|
+
actual_sha256 = hashlib.sha256(artifact_path.read_bytes()).hexdigest()
|
|
102
|
+
if actual_sha256 != expected_sha256:
|
|
103
|
+
raise RuntimeError(
|
|
104
|
+
f"vendored Open Workflow artifact failed integrity verification: {artifact_path.name}"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _official_schema() -> dict[str, Any]:
|
|
109
|
+
global _official_schema_cache
|
|
110
|
+
cached_schema = _official_schema_cache
|
|
111
|
+
if cached_schema is None:
|
|
112
|
+
provenance = _load_json_mapping(_OFFICIAL_SCHEMA_PROVENANCE_PATH)
|
|
113
|
+
if provenance.get("version") != OPEN_WORKFLOW_SCHEMA_VERSION:
|
|
114
|
+
raise RuntimeError("vendored Open Workflow schema version does not match provenance")
|
|
115
|
+
if provenance.get("source_commit") != _OFFICIAL_SCHEMA_COMMIT:
|
|
116
|
+
raise RuntimeError("vendored Open Workflow schema commit does not match provenance")
|
|
117
|
+
_verify_vendor_artifact(
|
|
118
|
+
_OFFICIAL_SCHEMA_PATH,
|
|
119
|
+
expected_sha256=provenance.get("schema_sha256"),
|
|
120
|
+
)
|
|
121
|
+
_verify_vendor_artifact(
|
|
122
|
+
_OFFICIAL_SCHEMA_LICENSE_PATH,
|
|
123
|
+
expected_sha256=provenance.get("license_sha256"),
|
|
124
|
+
)
|
|
125
|
+
cached_schema = load_yaml_mapping(_OFFICIAL_SCHEMA_PATH)
|
|
126
|
+
jsonschema.Draft202012Validator.check_schema(cached_schema)
|
|
127
|
+
_official_schema_cache = cached_schema
|
|
128
|
+
return cached_schema
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def official_open_workflow_schema() -> dict[str, Any]:
|
|
132
|
+
"""Return a defensive copy of the verified official Open Workflow schema."""
|
|
133
|
+
|
|
134
|
+
return deepcopy(_official_schema())
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _profile_schema() -> dict[str, Any]:
|
|
138
|
+
global _profile_schema_cache
|
|
139
|
+
cached_schema = _profile_schema_cache
|
|
140
|
+
if cached_schema is None:
|
|
141
|
+
cached_schema = strict_json_loads(_PROFILE_SCHEMA_PATH.read_text(encoding="utf-8"))
|
|
142
|
+
jsonschema.Draft202012Validator.check_schema(cached_schema)
|
|
143
|
+
_profile_schema_cache = cached_schema
|
|
144
|
+
return cached_schema
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _json_path(error: jsonschema.ValidationError) -> str:
|
|
148
|
+
source_path = "$"
|
|
149
|
+
for path_component in error.absolute_path:
|
|
150
|
+
if isinstance(path_component, int):
|
|
151
|
+
source_path += f"[{path_component}]"
|
|
152
|
+
else:
|
|
153
|
+
source_path += f".{path_component}"
|
|
154
|
+
return source_path
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _raise_conversion_diagnostics(
|
|
158
|
+
*,
|
|
159
|
+
source_format: str,
|
|
160
|
+
target_format: str,
|
|
161
|
+
diagnostics: tuple[CompatibilityDiagnostic, ...],
|
|
162
|
+
) -> Never:
|
|
163
|
+
report = CompatibilityReport(
|
|
164
|
+
source_format=source_format,
|
|
165
|
+
target_format=target_format,
|
|
166
|
+
diagnostics=diagnostics,
|
|
167
|
+
)
|
|
168
|
+
raise CompatibilityError(report, allow_lossy=False)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _raise_conversion_error(
|
|
172
|
+
*,
|
|
173
|
+
source_format: str,
|
|
174
|
+
target_format: str,
|
|
175
|
+
code: str,
|
|
176
|
+
source_path: str,
|
|
177
|
+
message: str,
|
|
178
|
+
) -> Never:
|
|
179
|
+
_raise_conversion_diagnostics(
|
|
180
|
+
source_format=source_format,
|
|
181
|
+
target_format=target_format,
|
|
182
|
+
diagnostics=(
|
|
183
|
+
CompatibilityDiagnostic(
|
|
184
|
+
severity="error",
|
|
185
|
+
code=code,
|
|
186
|
+
source_path=source_path,
|
|
187
|
+
message=message,
|
|
188
|
+
),
|
|
189
|
+
),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _step_kind(step: Mapping[str, Any]) -> str | None:
|
|
194
|
+
declared_kinds = tuple(step_kind for step_kind in _STEP_KINDS if step_kind in step)
|
|
195
|
+
return declared_kinds[0] if len(declared_kinds) == 1 else None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _task_name(path_index: int, step_kind: str) -> str:
|
|
199
|
+
return f"path-{path_index}-{step_kind}"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _normalize_flow_name(flow_id: str) -> str:
|
|
203
|
+
return flow_id.replace("_", "-")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _valid_entry_schema(entry_schema: Mapping[str, Any]) -> bool:
|
|
207
|
+
try:
|
|
208
|
+
jsonschema.Draft202012Validator.check_schema(dict(entry_schema))
|
|
209
|
+
except jsonschema.SchemaError:
|
|
210
|
+
return False
|
|
211
|
+
return True
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _profile_validation_error(
|
|
215
|
+
message: str,
|
|
216
|
+
*path: str | int,
|
|
217
|
+
) -> jsonschema.ValidationError:
|
|
218
|
+
return jsonschema.ValidationError(message, path=path)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _validation_error_sort_key(
|
|
222
|
+
error: jsonschema.ValidationError,
|
|
223
|
+
) -> tuple[tuple[tuple[int, int | str], ...], str]:
|
|
224
|
+
path_components: list[tuple[int, int | str]] = []
|
|
225
|
+
for path_component in error.absolute_path:
|
|
226
|
+
if isinstance(path_component, int):
|
|
227
|
+
path_components.append((0, path_component))
|
|
228
|
+
else:
|
|
229
|
+
path_components.append((1, str(path_component)))
|
|
230
|
+
return tuple(path_components), error.message
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _sorted_validation_errors(
|
|
234
|
+
errors: tuple[jsonschema.ValidationError, ...],
|
|
235
|
+
) -> tuple[jsonschema.ValidationError, ...]:
|
|
236
|
+
return tuple(sorted(errors, key=_validation_error_sort_key))
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _official_validation_errors(
|
|
240
|
+
workflow_document: Mapping[str, Any],
|
|
241
|
+
) -> tuple[jsonschema.ValidationError, ...]:
|
|
242
|
+
errors = tuple(
|
|
243
|
+
jsonschema.Draft202012Validator(_official_schema()).iter_errors(workflow_document)
|
|
244
|
+
)
|
|
245
|
+
return _sorted_validation_errors(errors)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _merge_official_validation_errors(
|
|
249
|
+
profile_errors: tuple[jsonschema.ValidationError, ...],
|
|
250
|
+
official_errors: tuple[jsonschema.ValidationError, ...],
|
|
251
|
+
) -> tuple[jsonschema.ValidationError, ...]:
|
|
252
|
+
profile_error_keys = {(tuple(error.absolute_path), error.message) for error in profile_errors}
|
|
253
|
+
additional_official_errors = tuple(
|
|
254
|
+
error
|
|
255
|
+
for error in official_errors
|
|
256
|
+
if (tuple(error.absolute_path), error.message) not in profile_error_keys
|
|
257
|
+
)
|
|
258
|
+
return _sorted_validation_errors(profile_errors + additional_official_errors)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _flowspec_validation_errors(
|
|
262
|
+
flow_document: Mapping[str, Any],
|
|
263
|
+
) -> tuple[jsonschema.ValidationError, ...]:
|
|
264
|
+
errors = tuple(jsonschema.Draft202012Validator(flowspec_schema()).iter_errors(flow_document))
|
|
265
|
+
return _sorted_validation_errors(errors)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _compilation_error(flow_document: Mapping[str, Any]) -> str | None:
|
|
269
|
+
try:
|
|
270
|
+
compile_flow(
|
|
271
|
+
deepcopy(dict(flow_document)),
|
|
272
|
+
tools=_compatibility_tool_registry(flow_document),
|
|
273
|
+
)
|
|
274
|
+
except (KeyError, StopIteration, TypeError, ValueError) as error:
|
|
275
|
+
return str(error) or error.__class__.__name__
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _profile_semantic_errors(
|
|
280
|
+
workflow_document: Mapping[str, Any],
|
|
281
|
+
semantic_errors: list[jsonschema.ValidationError] | None = None,
|
|
282
|
+
) -> tuple[jsonschema.ValidationError, ...]:
|
|
283
|
+
semantic_errors = semantic_errors if semantic_errors is not None else []
|
|
284
|
+
document = workflow_document["document"]
|
|
285
|
+
flowspec_metadata = document["metadata"]["flowspec2"]
|
|
286
|
+
original_flow_id = flowspec_metadata["original_flow_id"]
|
|
287
|
+
sections = flowspec_metadata["sections"]
|
|
288
|
+
|
|
289
|
+
if document["name"] != _normalize_flow_name(original_flow_id):
|
|
290
|
+
semantic_errors.append(
|
|
291
|
+
_profile_validation_error(
|
|
292
|
+
"document.name must be the underscore-to-hyphen normalization of "
|
|
293
|
+
"document.metadata.flowspec2.original_flow_id",
|
|
294
|
+
"document",
|
|
295
|
+
"name",
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
if document["version"] != sections["version"]:
|
|
299
|
+
semantic_errors.append(
|
|
300
|
+
_profile_validation_error(
|
|
301
|
+
"document.version must match the preserved flowspec2 version",
|
|
302
|
+
"document",
|
|
303
|
+
"version",
|
|
304
|
+
)
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
for path_index, task_item in enumerate(workflow_document["do"]):
|
|
308
|
+
task_name, task_definition = next(iter(task_item.items()))
|
|
309
|
+
declared_path_index = task_definition["metadata"]["path_index"]
|
|
310
|
+
if declared_path_index != path_index:
|
|
311
|
+
semantic_errors.append(
|
|
312
|
+
_profile_validation_error(
|
|
313
|
+
"task metadata.path_index must equal its position in the sequential task list",
|
|
314
|
+
"do",
|
|
315
|
+
path_index,
|
|
316
|
+
task_name,
|
|
317
|
+
"metadata",
|
|
318
|
+
"path_index",
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
call_name = task_definition["call"]
|
|
323
|
+
call_step_kind = call_name.removeprefix("flowspec2.")
|
|
324
|
+
preserved_step = task_definition["with"]["step"]
|
|
325
|
+
preserved_step_kind = _step_kind(preserved_step)
|
|
326
|
+
if preserved_step_kind != call_step_kind:
|
|
327
|
+
semantic_errors.append(
|
|
328
|
+
_profile_validation_error(
|
|
329
|
+
"custom call must match the single flowspec2 kind in with.step",
|
|
330
|
+
"do",
|
|
331
|
+
path_index,
|
|
332
|
+
task_name,
|
|
333
|
+
"call",
|
|
334
|
+
)
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
expected_task_name = _task_name(path_index, call_step_kind)
|
|
338
|
+
if task_name != expected_task_name:
|
|
339
|
+
semantic_errors.append(
|
|
340
|
+
_profile_validation_error(
|
|
341
|
+
f"task name must be {expected_task_name!r}",
|
|
342
|
+
"do",
|
|
343
|
+
path_index,
|
|
344
|
+
)
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
input_definition = workflow_document.get("input")
|
|
348
|
+
preserved_entry_schema = sections["route"].get("entry_args_schema")
|
|
349
|
+
if (
|
|
350
|
+
input_definition is None
|
|
351
|
+
and isinstance(preserved_entry_schema, Mapping)
|
|
352
|
+
and _valid_entry_schema(preserved_entry_schema)
|
|
353
|
+
):
|
|
354
|
+
semantic_errors.append(
|
|
355
|
+
_profile_validation_error(
|
|
356
|
+
"input is required when the preserved route.entry_args_schema is a valid JSON Schema",
|
|
357
|
+
"input",
|
|
358
|
+
)
|
|
359
|
+
)
|
|
360
|
+
if input_definition is not None:
|
|
361
|
+
input_schema = input_definition["schema"]["document"]
|
|
362
|
+
try:
|
|
363
|
+
jsonschema.Draft202012Validator.check_schema(input_schema)
|
|
364
|
+
except jsonschema.SchemaError as error:
|
|
365
|
+
semantic_errors.append(
|
|
366
|
+
_profile_validation_error(
|
|
367
|
+
f"input.schema.document is not a valid JSON Schema: {error.message}",
|
|
368
|
+
"input",
|
|
369
|
+
"schema",
|
|
370
|
+
"document",
|
|
371
|
+
)
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
if input_schema != preserved_entry_schema:
|
|
375
|
+
semantic_errors.append(
|
|
376
|
+
_profile_validation_error(
|
|
377
|
+
"input.schema.document must match the preserved route.entry_args_schema",
|
|
378
|
+
"input",
|
|
379
|
+
"schema",
|
|
380
|
+
"document",
|
|
381
|
+
)
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
return _sorted_validation_errors(tuple(semantic_errors))
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _profile_validation_errors(
|
|
388
|
+
workflow_document: Mapping[str, Any],
|
|
389
|
+
) -> tuple[jsonschema.ValidationError, ...]:
|
|
390
|
+
profile_structural_errors = tuple(
|
|
391
|
+
jsonschema.Draft202012Validator(_profile_schema()).iter_errors(workflow_document)
|
|
392
|
+
)
|
|
393
|
+
official_errors = _official_validation_errors(workflow_document)
|
|
394
|
+
semantic_errors: list[jsonschema.ValidationError] = []
|
|
395
|
+
try:
|
|
396
|
+
_profile_semantic_errors(workflow_document, semantic_errors)
|
|
397
|
+
except (AttributeError, IndexError, KeyError, StopIteration, TypeError):
|
|
398
|
+
# Invalid container shapes can make semantic paths unreadable.
|
|
399
|
+
profile_errors = _sorted_validation_errors(
|
|
400
|
+
profile_structural_errors + tuple(semantic_errors)
|
|
401
|
+
)
|
|
402
|
+
return _merge_official_validation_errors(profile_errors, official_errors)
|
|
403
|
+
profile_errors = _sorted_validation_errors(profile_structural_errors + tuple(semantic_errors))
|
|
404
|
+
return _merge_official_validation_errors(profile_errors, official_errors)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _validation_diagnostics(
|
|
408
|
+
errors: tuple[jsonschema.ValidationError, ...],
|
|
409
|
+
*,
|
|
410
|
+
code: str,
|
|
411
|
+
) -> tuple[CompatibilityDiagnostic, ...]:
|
|
412
|
+
return tuple(
|
|
413
|
+
CompatibilityDiagnostic(
|
|
414
|
+
severity="error",
|
|
415
|
+
code=code,
|
|
416
|
+
source_path=_json_path(error),
|
|
417
|
+
message=error.message,
|
|
418
|
+
)
|
|
419
|
+
for error in errors
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _raise_validation_errors(
|
|
424
|
+
errors: tuple[jsonschema.ValidationError, ...],
|
|
425
|
+
*,
|
|
426
|
+
source_format: str,
|
|
427
|
+
target_format: str,
|
|
428
|
+
code: str,
|
|
429
|
+
) -> Never:
|
|
430
|
+
_raise_conversion_diagnostics(
|
|
431
|
+
source_format=source_format,
|
|
432
|
+
target_format=target_format,
|
|
433
|
+
diagnostics=_validation_diagnostics(errors, code=code),
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def validate_open_workflow_profile(
|
|
438
|
+
workflow_document: Mapping[str, Any],
|
|
439
|
+
) -> None:
|
|
440
|
+
"""Validate the official schema and the exact flowspec2 conversational profile."""
|
|
441
|
+
|
|
442
|
+
validation_errors = _profile_validation_errors(workflow_document)
|
|
443
|
+
if validation_errors:
|
|
444
|
+
raise validation_errors[0]
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def validate_official_open_workflow(
|
|
448
|
+
workflow_document: Mapping[str, Any],
|
|
449
|
+
) -> None:
|
|
450
|
+
"""Validate a document only against the verified official Open Workflow schema."""
|
|
451
|
+
|
|
452
|
+
validation_errors = _official_validation_errors(workflow_document)
|
|
453
|
+
if validation_errors:
|
|
454
|
+
raise validation_errors[0]
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def export_open_workflow(
|
|
458
|
+
flow_document: Mapping[str, Any],
|
|
459
|
+
*,
|
|
460
|
+
namespace: str = _DEFAULT_NAMESPACE,
|
|
461
|
+
) -> ConversionOutcome[dict[str, Any]]:
|
|
462
|
+
"""Export a valid flowspec2 document as the lossless Open Workflow profile."""
|
|
463
|
+
|
|
464
|
+
source_flow = deepcopy(dict(flow_document))
|
|
465
|
+
source_errors = _flowspec_validation_errors(source_flow)
|
|
466
|
+
if source_errors:
|
|
467
|
+
_raise_validation_errors(
|
|
468
|
+
source_errors,
|
|
469
|
+
source_format=_FLOWSPEC_FORMAT,
|
|
470
|
+
target_format=_OPEN_WORKFLOW_FORMAT,
|
|
471
|
+
code="open_workflow.invalid_flowspec",
|
|
472
|
+
)
|
|
473
|
+
if compilation_error := _compilation_error(source_flow):
|
|
474
|
+
_raise_conversion_error(
|
|
475
|
+
source_format=_FLOWSPEC_FORMAT,
|
|
476
|
+
target_format=_OPEN_WORKFLOW_FORMAT,
|
|
477
|
+
code="open_workflow.non_executable_flowspec",
|
|
478
|
+
source_path="$",
|
|
479
|
+
message=f"flowspec2 compiler rejected the source document: {compilation_error}",
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
diagnostics: list[CompatibilityDiagnostic] = []
|
|
483
|
+
path_tasks: list[dict[str, Any]] = []
|
|
484
|
+
for path_index, path_step in enumerate(source_flow["path"]):
|
|
485
|
+
step_kind = _step_kind(path_step)
|
|
486
|
+
if step_kind is None:
|
|
487
|
+
_raise_conversion_error(
|
|
488
|
+
source_format=_FLOWSPEC_FORMAT,
|
|
489
|
+
target_format=_OPEN_WORKFLOW_FORMAT,
|
|
490
|
+
code="open_workflow.invalid_path_step",
|
|
491
|
+
source_path=f"$.path[{path_index}]",
|
|
492
|
+
message="path step must declare exactly one supported flowspec2 step kind",
|
|
493
|
+
)
|
|
494
|
+
task_name = _task_name(path_index, step_kind)
|
|
495
|
+
path_tasks.append(
|
|
496
|
+
{
|
|
497
|
+
task_name: {
|
|
498
|
+
"call": f"flowspec2.{step_kind}",
|
|
499
|
+
"with": {"step": deepcopy(path_step)},
|
|
500
|
+
"metadata": {"path_index": path_index},
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
preserved_sections = {
|
|
506
|
+
section_name: deepcopy(section_value)
|
|
507
|
+
for section_name, section_value in source_flow.items()
|
|
508
|
+
if section_name not in {"flow", "path"}
|
|
509
|
+
}
|
|
510
|
+
workflow_document: dict[str, Any] = {
|
|
511
|
+
"document": {
|
|
512
|
+
"dsl": _OPEN_WORKFLOW_DSL_VERSION,
|
|
513
|
+
"namespace": namespace,
|
|
514
|
+
"name": _normalize_flow_name(source_flow["flow"]),
|
|
515
|
+
"version": source_flow["version"],
|
|
516
|
+
"metadata": {
|
|
517
|
+
"flowspec2": {
|
|
518
|
+
"profile": OPEN_WORKFLOW_PROFILE_ID,
|
|
519
|
+
"original_flow_id": source_flow["flow"],
|
|
520
|
+
"sections": preserved_sections,
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
"do": path_tasks,
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
entry_schema = source_flow["route"].get("entry_args_schema")
|
|
528
|
+
if isinstance(entry_schema, Mapping):
|
|
529
|
+
if _valid_entry_schema(entry_schema):
|
|
530
|
+
workflow_document["input"] = {
|
|
531
|
+
"schema": {
|
|
532
|
+
"format": "json",
|
|
533
|
+
"document": deepcopy(dict(entry_schema)),
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
else:
|
|
537
|
+
diagnostics.append(
|
|
538
|
+
CompatibilityDiagnostic(
|
|
539
|
+
severity="warning",
|
|
540
|
+
code="open_workflow.entry_schema_omitted",
|
|
541
|
+
source_path="$.route.entry_args_schema",
|
|
542
|
+
message=(
|
|
543
|
+
"entry_args_schema is preserved in profile metadata but omitted "
|
|
544
|
+
"from input because it is not a valid JSON Schema"
|
|
545
|
+
),
|
|
546
|
+
)
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
generated_profile_errors = _profile_validation_errors(workflow_document)
|
|
550
|
+
if generated_profile_errors:
|
|
551
|
+
_raise_validation_errors(
|
|
552
|
+
generated_profile_errors,
|
|
553
|
+
source_format=_FLOWSPEC_FORMAT,
|
|
554
|
+
target_format=_OPEN_WORKFLOW_FORMAT,
|
|
555
|
+
code="open_workflow.invalid_generated_profile",
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
return ConversionOutcome(
|
|
559
|
+
artifact=workflow_document,
|
|
560
|
+
report=CompatibilityReport(
|
|
561
|
+
source_format=_FLOWSPEC_FORMAT,
|
|
562
|
+
target_format=_OPEN_WORKFLOW_FORMAT,
|
|
563
|
+
diagnostics=tuple(diagnostics),
|
|
564
|
+
),
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def import_open_workflow(
|
|
569
|
+
workflow_document: Mapping[str, Any],
|
|
570
|
+
) -> ConversionOutcome[dict[str, Any]]:
|
|
571
|
+
"""Import only an Open Workflow document declaring the exact profile."""
|
|
572
|
+
|
|
573
|
+
source_workflow = deepcopy(dict(workflow_document))
|
|
574
|
+
source_profile_errors = _profile_validation_errors(source_workflow)
|
|
575
|
+
if source_profile_errors:
|
|
576
|
+
_raise_validation_errors(
|
|
577
|
+
source_profile_errors,
|
|
578
|
+
source_format=_OPEN_WORKFLOW_FORMAT,
|
|
579
|
+
target_format=_FLOWSPEC_FORMAT,
|
|
580
|
+
code="open_workflow.invalid_profile",
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
flowspec_metadata = source_workflow["document"]["metadata"]["flowspec2"]
|
|
584
|
+
imported_flow = deepcopy(flowspec_metadata["sections"])
|
|
585
|
+
imported_flow["flow"] = flowspec_metadata["original_flow_id"]
|
|
586
|
+
imported_flow["path"] = [
|
|
587
|
+
deepcopy(next(iter(task_item.values()))["with"]["step"])
|
|
588
|
+
for task_item in source_workflow["do"]
|
|
589
|
+
]
|
|
590
|
+
|
|
591
|
+
reconstructed_flow_errors = _flowspec_validation_errors(imported_flow)
|
|
592
|
+
if reconstructed_flow_errors:
|
|
593
|
+
_raise_validation_errors(
|
|
594
|
+
reconstructed_flow_errors,
|
|
595
|
+
source_format=_OPEN_WORKFLOW_FORMAT,
|
|
596
|
+
target_format=_FLOWSPEC_FORMAT,
|
|
597
|
+
code="open_workflow.invalid_reconstructed_flowspec",
|
|
598
|
+
)
|
|
599
|
+
if compilation_error := _compilation_error(imported_flow):
|
|
600
|
+
_raise_conversion_error(
|
|
601
|
+
source_format=_OPEN_WORKFLOW_FORMAT,
|
|
602
|
+
target_format=_FLOWSPEC_FORMAT,
|
|
603
|
+
code="open_workflow.non_executable_reconstructed_flowspec",
|
|
604
|
+
source_path="$.document.metadata.flowspec2",
|
|
605
|
+
message=(
|
|
606
|
+
f"flowspec2 compiler rejected the reconstructed document: {compilation_error}"
|
|
607
|
+
),
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
return ConversionOutcome(
|
|
611
|
+
artifact=imported_flow,
|
|
612
|
+
report=CompatibilityReport(
|
|
613
|
+
source_format=_OPEN_WORKFLOW_FORMAT,
|
|
614
|
+
target_format=_FLOWSPEC_FORMAT,
|
|
615
|
+
),
|
|
616
|
+
)
|
flowspec2/compat/rasa.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Public bounded Rasa CALM interoperability API."""
|
|
2
|
+
|
|
3
|
+
from .rasa_export import export_rasa
|
|
4
|
+
from .rasa_import import import_rasa
|
|
5
|
+
from .rasa_shared import (
|
|
6
|
+
RASA_FORMAT,
|
|
7
|
+
RASA_MINIMUM_VERSION,
|
|
8
|
+
RASA_PROFILE_VERSION,
|
|
9
|
+
RasaBundle,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"RASA_FORMAT",
|
|
14
|
+
"RASA_MINIMUM_VERSION",
|
|
15
|
+
"RASA_PROFILE_VERSION",
|
|
16
|
+
"RasaBundle",
|
|
17
|
+
"export_rasa",
|
|
18
|
+
"import_rasa",
|
|
19
|
+
]
|