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.
Files changed (92) hide show
  1. flowspec2/__init__.py +105 -0
  2. flowspec2/authoring/__init__.py +275 -0
  3. flowspec2/authoring/authoring-evidence-signature.schema.json +34 -0
  4. flowspec2/authoring/authoring-evidence.schema.json +455 -0
  5. flowspec2/authoring/authoring-operational-evidence.schema.json +160 -0
  6. flowspec2/authoring/benchmark.py +1409 -0
  7. flowspec2/authoring/corpus/await_correction.case.json +220 -0
  8. flowspec2/authoring/corpus/flowspec2.ctk.json +769 -0
  9. flowspec2/authoring/corpus/gated_derive.case.json +100 -0
  10. flowspec2/authoring/corpus/linear.case.json +55 -0
  11. flowspec2/authoring/corpus/manifest.json +31 -0
  12. flowspec2/authoring/corpus/subflow.case.json +66 -0
  13. flowspec2/authoring/corpus/terminal.case.json +94 -0
  14. flowspec2/authoring/corpus.py +307 -0
  15. flowspec2/authoring/ctk.py +836 -0
  16. flowspec2/authoring/detached_signature.py +120 -0
  17. flowspec2/authoring/evidence.py +311 -0
  18. flowspec2/authoring/evidence_signature.py +187 -0
  19. flowspec2/authoring/evidence_verification.py +229 -0
  20. flowspec2/authoring/gemini.py +215 -0
  21. flowspec2/authoring/operational-corpus.json +61 -0
  22. flowspec2/authoring/operational.py +956 -0
  23. flowspec2/authoring/operational_providers.py +167 -0
  24. flowspec2/authoring/presentation_review.py +1013 -0
  25. flowspec2/authoring/presentation_review_signature.py +305 -0
  26. flowspec2/authoring/projection.py +299 -0
  27. flowspec2/authoring/provider_prompt.py +83 -0
  28. flowspec2/backends/__init__.py +82 -0
  29. flowspec2/backends/http.py +189 -0
  30. flowspec2/checker.py +238 -0
  31. flowspec2/cli.py +789 -0
  32. flowspec2/cli_parser.py +345 -0
  33. flowspec2/clock.py +23 -0
  34. flowspec2/compat/__init__.py +47 -0
  35. flowspec2/compat/models.py +82 -0
  36. flowspec2/compat/open_workflow.py +616 -0
  37. flowspec2/compat/rasa.py +19 -0
  38. flowspec2/compat/rasa_export.py +876 -0
  39. flowspec2/compat/rasa_import.py +992 -0
  40. flowspec2/compat/rasa_shared.py +270 -0
  41. flowspec2/compat/schemas/open-workflow-conversation-1.schema.json +138 -0
  42. flowspec2/compat/schemas/vendor/open-workflow-1.0.3.LICENSE +201 -0
  43. flowspec2/compat/schemas/vendor/open-workflow-1.0.3.provenance.json +13 -0
  44. flowspec2/compat/schemas/vendor/open-workflow-1.0.3.workflow.yaml +1956 -0
  45. flowspec2/compat/tool_profiles.py +149 -0
  46. flowspec2/compat/yaml.py +147 -0
  47. flowspec2/compiler.py +957 -0
  48. flowspec2/compiler_contracts.py +17 -0
  49. flowspec2/compiler_resume_contracts.py +594 -0
  50. flowspec2/compiler_schema_relations.py +1830 -0
  51. flowspec2/compiler_tool_contracts.py +245 -0
  52. flowspec2/compiler_value_contracts.py +447 -0
  53. flowspec2/derive.py +32 -0
  54. flowspec2/diagnostics.py +94 -0
  55. flowspec2/domains.py +489 -0
  56. flowspec2/experimental/__init__.py +57 -0
  57. flowspec2/experimental/flowspec-3-draft.schema.json +923 -0
  58. flowspec2/experimental/v3-preview-loss-policy.json +161 -0
  59. flowspec2/experimental/v3_lowering.py +928 -0
  60. flowspec2/experimental/v3_preview.py +2460 -0
  61. flowspec2/flowspec-2.schema.json +635 -0
  62. flowspec2/interactive.py +300 -0
  63. flowspec2/ir.py +1459 -0
  64. flowspec2/json_codec.py +120 -0
  65. flowspec2/llm.py +366 -0
  66. flowspec2/models.py +121 -0
  67. flowspec2/nodes.py +1537 -0
  68. flowspec2/observability.py +151 -0
  69. flowspec2/predicates.py +89 -0
  70. flowspec2/profiles.py +118 -0
  71. flowspec2/py.typed +0 -0
  72. flowspec2/runtime.py +1059 -0
  73. flowspec2/schema.py +54 -0
  74. flowspec2/schema_contracts.py +203 -0
  75. flowspec2/semantic_derive_contracts.py +530 -0
  76. flowspec2/semantic_path_contracts.py +528 -0
  77. flowspec2/semantic_predicate_contracts.py +355 -0
  78. flowspec2/semantic_schema_contracts.py +137 -0
  79. flowspec2/semantic_source_contracts.py +21 -0
  80. flowspec2/semantic_state_contracts.py +450 -0
  81. flowspec2/semantic_support.py +40 -0
  82. flowspec2/semantics.py +410 -0
  83. flowspec2/state_migration.py +1034 -0
  84. flowspec2/subflows/__init__.py +1117 -0
  85. flowspec2/subflows/address.py +328 -0
  86. flowspec2/subflows/identification.py +1031 -0
  87. flowspec2/tools.py +1154 -0
  88. flowspec2-1.0.0.dist-info/METADATA +482 -0
  89. flowspec2-1.0.0.dist-info/RECORD +92 -0
  90. flowspec2-1.0.0.dist-info/WHEEL +4 -0
  91. flowspec2-1.0.0.dist-info/entry_points.txt +2 -0
  92. 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
+ )
@@ -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
+ ]