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,992 @@
|
|
|
1
|
+
"""Bounded Rasa CALM to FlowSpec2 import conversion."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Mapping, Sequence, cast
|
|
6
|
+
|
|
7
|
+
from .models import (
|
|
8
|
+
CompatibilityDiagnostic,
|
|
9
|
+
CompatibilityReport,
|
|
10
|
+
ConversionOutcome,
|
|
11
|
+
DiagnosticSeverity,
|
|
12
|
+
enforce_compatibility_policy,
|
|
13
|
+
)
|
|
14
|
+
from .rasa_shared import (
|
|
15
|
+
FLOWSPEC_FLOW_IDENTIFIER_PATTERN,
|
|
16
|
+
FLOWSPEC_FORMAT,
|
|
17
|
+
RASA_ACTION_IDENTIFIER_PATTERN,
|
|
18
|
+
RASA_DOMAIN_VERSION,
|
|
19
|
+
RASA_FORMAT,
|
|
20
|
+
compatibility_diagnostic,
|
|
21
|
+
domain_button_options,
|
|
22
|
+
flowspec_compilation_diagnostic,
|
|
23
|
+
flowspec_validation_diagnostics,
|
|
24
|
+
mapping_or_none,
|
|
25
|
+
rasa_response_text_supported,
|
|
26
|
+
rasa_set_slots_name_supported,
|
|
27
|
+
rasa_set_slots_value_supported,
|
|
28
|
+
sequence_or_none,
|
|
29
|
+
step_identifier_diagnostics,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _import_slot_mapping_diagnostics(
|
|
34
|
+
slot_name: str,
|
|
35
|
+
slot_declaration: Mapping[str, Any],
|
|
36
|
+
diagnostics: list[CompatibilityDiagnostic],
|
|
37
|
+
) -> None:
|
|
38
|
+
source_path = f"$.slots.{slot_name}"
|
|
39
|
+
if "mappings" in slot_declaration:
|
|
40
|
+
mappings = sequence_or_none(slot_declaration.get("mappings"))
|
|
41
|
+
if (
|
|
42
|
+
mappings is None
|
|
43
|
+
or len(mappings) != 1
|
|
44
|
+
or mapping_or_none(mappings[0]) != {"type": "from_llm"}
|
|
45
|
+
):
|
|
46
|
+
diagnostics.append(
|
|
47
|
+
compatibility_diagnostic(
|
|
48
|
+
"error",
|
|
49
|
+
"RASA_SLOT_MAPPING_UNSUPPORTED",
|
|
50
|
+
f"{source_path}.mappings",
|
|
51
|
+
"only an omitted mapping or one exact from_llm mapping is portable",
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
allowed_properties = {"type", "values", "mappings"}
|
|
55
|
+
for property_name in slot_declaration.keys() - allowed_properties:
|
|
56
|
+
diagnostics.append(
|
|
57
|
+
compatibility_diagnostic(
|
|
58
|
+
"error",
|
|
59
|
+
"RASA_SLOT_PROPERTY_UNSUPPORTED",
|
|
60
|
+
f"{source_path}.{property_name}",
|
|
61
|
+
f"Rasa slot property {property_name!r} has no flowspec2 portable equivalent",
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _import_domain_specification(
|
|
67
|
+
slot_name: str,
|
|
68
|
+
slot_declaration: Mapping[str, Any],
|
|
69
|
+
diagnostics: list[CompatibilityDiagnostic],
|
|
70
|
+
) -> dict[str, Any] | None:
|
|
71
|
+
source_path = f"$.slots.{slot_name}"
|
|
72
|
+
_import_slot_mapping_diagnostics(slot_name, slot_declaration, diagnostics)
|
|
73
|
+
slot_type = slot_declaration.get("type")
|
|
74
|
+
if slot_type == "categorical":
|
|
75
|
+
values = sequence_or_none(slot_declaration.get("values"))
|
|
76
|
+
if values is None or not values or not all(isinstance(value, str) for value in values):
|
|
77
|
+
diagnostics.append(
|
|
78
|
+
compatibility_diagnostic(
|
|
79
|
+
"error",
|
|
80
|
+
"RASA_CATEGORICAL_VALUES_INVALID",
|
|
81
|
+
f"{source_path}.values",
|
|
82
|
+
"a portable categorical slot requires a non-empty list of strings",
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
return None
|
|
86
|
+
string_values = cast(Sequence[str], values)
|
|
87
|
+
if len({value.casefold() for value in string_values}) != len(string_values):
|
|
88
|
+
diagnostics.append(
|
|
89
|
+
compatibility_diagnostic(
|
|
90
|
+
"error",
|
|
91
|
+
"RASA_CATEGORICAL_CASE_COLLISION",
|
|
92
|
+
f"{source_path}.values",
|
|
93
|
+
"case-insensitive Rasa categories cannot become distinct flowspec2 tokens",
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
return {"type": "categorical", "values": list(string_values)}
|
|
97
|
+
if slot_type == "bool":
|
|
98
|
+
if "values" in slot_declaration:
|
|
99
|
+
diagnostics.append(
|
|
100
|
+
compatibility_diagnostic(
|
|
101
|
+
"error",
|
|
102
|
+
"RASA_SLOT_PROPERTY_UNSUPPORTED",
|
|
103
|
+
f"{source_path}.values",
|
|
104
|
+
"bool slots do not use categorical values in the portable profile",
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
return {"type": "bool"}
|
|
108
|
+
if slot_type == "text":
|
|
109
|
+
if "values" in slot_declaration:
|
|
110
|
+
diagnostics.append(
|
|
111
|
+
compatibility_diagnostic(
|
|
112
|
+
"error",
|
|
113
|
+
"RASA_SLOT_PROPERTY_UNSUPPORTED",
|
|
114
|
+
f"{source_path}.values",
|
|
115
|
+
"text slots do not use categorical values in the portable profile",
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
return {"type": "free_text"}
|
|
119
|
+
diagnostics.append(
|
|
120
|
+
compatibility_diagnostic(
|
|
121
|
+
"error",
|
|
122
|
+
"RASA_SLOT_TYPE_UNSUPPORTED",
|
|
123
|
+
f"{source_path}.type",
|
|
124
|
+
f"slot type {slot_type!r} is outside categorical, bool and text",
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _parse_set_slots_payload(payload: object) -> tuple[str, str] | None:
|
|
131
|
+
if (
|
|
132
|
+
not isinstance(payload, str)
|
|
133
|
+
or not payload.startswith("/SetSlots(")
|
|
134
|
+
or not payload.endswith(")")
|
|
135
|
+
):
|
|
136
|
+
return None
|
|
137
|
+
assignment = payload[len("/SetSlots(") : -1]
|
|
138
|
+
if "=" not in assignment or "," in assignment:
|
|
139
|
+
return None
|
|
140
|
+
slot_name, slot_value = assignment.split("=", 1)
|
|
141
|
+
if not slot_name or not slot_value:
|
|
142
|
+
return None
|
|
143
|
+
if not rasa_set_slots_name_supported(slot_name):
|
|
144
|
+
return None
|
|
145
|
+
if not rasa_set_slots_value_supported(slot_value):
|
|
146
|
+
return None
|
|
147
|
+
return slot_name, slot_value
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _import_buttons(
|
|
151
|
+
buttons_value: object,
|
|
152
|
+
slot_name: str,
|
|
153
|
+
domain_specification: Mapping[str, Any],
|
|
154
|
+
source_path: str,
|
|
155
|
+
diagnostics: list[CompatibilityDiagnostic],
|
|
156
|
+
) -> dict[str, Any] | None:
|
|
157
|
+
buttons = sequence_or_none(buttons_value)
|
|
158
|
+
if buttons is None or not buttons:
|
|
159
|
+
diagnostics.append(
|
|
160
|
+
compatibility_diagnostic(
|
|
161
|
+
"warning",
|
|
162
|
+
"RASA_STATIC_BUTTONS_UNSUPPORTED",
|
|
163
|
+
source_path,
|
|
164
|
+
"buttons must be a non-empty list of title/payload mappings",
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
return None
|
|
168
|
+
if not rasa_set_slots_name_supported(slot_name):
|
|
169
|
+
diagnostics.append(
|
|
170
|
+
compatibility_diagnostic(
|
|
171
|
+
"warning",
|
|
172
|
+
"RASA_SETSLOTS_SLOT_NAME_UNSUPPORTED",
|
|
173
|
+
source_path,
|
|
174
|
+
"portable SetSlots names must be non-empty and forbid '=,()'",
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
expected_options = domain_button_options(domain_specification)
|
|
180
|
+
expected_titles = dict(expected_options)
|
|
181
|
+
accepts_legacy_boolean_titles = domain_specification.get("type") == "bool"
|
|
182
|
+
parsed_values: list[str] = []
|
|
183
|
+
portable = True
|
|
184
|
+
for button_index, button_value in enumerate(buttons):
|
|
185
|
+
button = mapping_or_none(button_value)
|
|
186
|
+
button_path = f"{source_path}[{button_index}]"
|
|
187
|
+
if button is None or set(button) != {"title", "payload"}:
|
|
188
|
+
diagnostics.append(
|
|
189
|
+
compatibility_diagnostic(
|
|
190
|
+
"warning",
|
|
191
|
+
"RASA_STATIC_BUTTONS_UNSUPPORTED",
|
|
192
|
+
button_path,
|
|
193
|
+
"each portable button must contain exactly title and payload",
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
portable = False
|
|
197
|
+
continue
|
|
198
|
+
button_title = button.get("title")
|
|
199
|
+
button_payload = button.get("payload")
|
|
200
|
+
if any(
|
|
201
|
+
isinstance(button_text, str) and not rasa_response_text_supported(button_text)
|
|
202
|
+
for button_text in (button_title, button_payload)
|
|
203
|
+
):
|
|
204
|
+
diagnostics.append(
|
|
205
|
+
compatibility_diagnostic(
|
|
206
|
+
"error",
|
|
207
|
+
"RASA_RESPONSE_INTERPOLATION_UNSUPPORTED",
|
|
208
|
+
button_path,
|
|
209
|
+
"curly braces in a button title or payload trigger Rasa response interpolation",
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
portable = False
|
|
213
|
+
continue
|
|
214
|
+
parsed_assignment = _parse_set_slots_payload(button.get("payload"))
|
|
215
|
+
if parsed_assignment is None:
|
|
216
|
+
diagnostics.append(
|
|
217
|
+
compatibility_diagnostic(
|
|
218
|
+
"warning",
|
|
219
|
+
"RASA_SETSLOTS_PAYLOAD_UNSUPPORTED",
|
|
220
|
+
f"{button_path}.payload",
|
|
221
|
+
"the payload must be one valid case-sensitive /SetSlots(slot=value) command",
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
portable = False
|
|
225
|
+
continue
|
|
226
|
+
assigned_slot, assigned_value = parsed_assignment
|
|
227
|
+
if assigned_slot != slot_name:
|
|
228
|
+
diagnostics.append(
|
|
229
|
+
compatibility_diagnostic(
|
|
230
|
+
"error",
|
|
231
|
+
"RASA_BUTTON_SLOT_MISMATCH",
|
|
232
|
+
f"{button_path}.payload",
|
|
233
|
+
"a collect response button may set only its collected slot",
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
portable = False
|
|
237
|
+
accepted_titles = {expected_titles.get(assigned_value)}
|
|
238
|
+
if accepts_legacy_boolean_titles:
|
|
239
|
+
accepted_titles.add(assigned_value)
|
|
240
|
+
if button.get("title") not in accepted_titles:
|
|
241
|
+
diagnostics.append(
|
|
242
|
+
compatibility_diagnostic(
|
|
243
|
+
"warning",
|
|
244
|
+
"RASA_BUTTON_TITLE_UNSUPPORTED",
|
|
245
|
+
f"{button_path}.title",
|
|
246
|
+
"the button title must match the domain's canonical presentation",
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
portable = False
|
|
250
|
+
parsed_values.append(assigned_value)
|
|
251
|
+
|
|
252
|
+
expected_values = [button_value for button_value, _button_title in expected_options]
|
|
253
|
+
if parsed_values != expected_values:
|
|
254
|
+
diagnostics.append(
|
|
255
|
+
compatibility_diagnostic(
|
|
256
|
+
"warning",
|
|
257
|
+
"RASA_BUTTON_VALUES_MISMATCH",
|
|
258
|
+
source_path,
|
|
259
|
+
"buttons must cover the slot domain exactly and in domain order",
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
portable = False
|
|
263
|
+
if not portable:
|
|
264
|
+
return None
|
|
265
|
+
return {"kind": "buttons", "field": slot_name}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _import_prompt(
|
|
269
|
+
response_key: str,
|
|
270
|
+
slot_name: str,
|
|
271
|
+
responses: Mapping[str, Any],
|
|
272
|
+
domain_name: str,
|
|
273
|
+
domain_specification: Mapping[str, Any],
|
|
274
|
+
diagnostics: list[CompatibilityDiagnostic],
|
|
275
|
+
) -> tuple[dict[str, Any] | None, dict[str, Any] | None]:
|
|
276
|
+
source_path = f"$.responses.{response_key}"
|
|
277
|
+
variations = sequence_or_none(responses.get(response_key))
|
|
278
|
+
if variations is None or len(variations) != 1:
|
|
279
|
+
diagnostics.append(
|
|
280
|
+
compatibility_diagnostic(
|
|
281
|
+
"error",
|
|
282
|
+
"RASA_RESPONSE_VARIATIONS_UNSUPPORTED",
|
|
283
|
+
source_path,
|
|
284
|
+
"the portable profile requires exactly one unconditional response variation",
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
return None, None
|
|
288
|
+
variation = mapping_or_none(variations[0])
|
|
289
|
+
if variation is None or not isinstance(variation.get("text"), str) or not variation["text"]:
|
|
290
|
+
diagnostics.append(
|
|
291
|
+
compatibility_diagnostic(
|
|
292
|
+
"error",
|
|
293
|
+
"RASA_RESPONSE_TEXT_REQUIRED",
|
|
294
|
+
f"{source_path}[0].text",
|
|
295
|
+
"the portable response variation requires non-empty text",
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
return None, None
|
|
299
|
+
|
|
300
|
+
allowed_properties = {"text", "buttons"}
|
|
301
|
+
for property_name in variation.keys() - allowed_properties:
|
|
302
|
+
diagnostics.append(
|
|
303
|
+
compatibility_diagnostic(
|
|
304
|
+
"error",
|
|
305
|
+
"RASA_RESPONSE_PROPERTY_UNSUPPORTED",
|
|
306
|
+
f"{source_path}[0].{property_name}",
|
|
307
|
+
f"conditional or rich response property {property_name!r} is not portable",
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
response_text = cast(str, variation["text"])
|
|
311
|
+
if not rasa_response_text_supported(response_text):
|
|
312
|
+
diagnostics.append(
|
|
313
|
+
compatibility_diagnostic(
|
|
314
|
+
"error",
|
|
315
|
+
"RASA_RESPONSE_INTERPOLATION_UNSUPPORTED",
|
|
316
|
+
f"{source_path}[0].text",
|
|
317
|
+
"Rasa response interpolation has no literal flowspec2 prompt equivalent",
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
prompt = {"text": response_text}
|
|
321
|
+
interactive: dict[str, Any] | None = None
|
|
322
|
+
if "buttons" in variation:
|
|
323
|
+
interactive = _import_buttons(
|
|
324
|
+
variation["buttons"],
|
|
325
|
+
slot_name,
|
|
326
|
+
domain_specification,
|
|
327
|
+
f"{source_path}[0].buttons",
|
|
328
|
+
diagnostics,
|
|
329
|
+
)
|
|
330
|
+
if interactive is not None:
|
|
331
|
+
interactive["from_domain"] = domain_name
|
|
332
|
+
return prompt, interactive
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _import_collect_step(
|
|
336
|
+
step: Mapping[str, Any],
|
|
337
|
+
flow_identifier: str,
|
|
338
|
+
step_index: int,
|
|
339
|
+
slot_declarations: Mapping[str, Any],
|
|
340
|
+
domain_names: Mapping[str, str],
|
|
341
|
+
flowspec_domains: Mapping[str, Any],
|
|
342
|
+
responses: Mapping[str, Any],
|
|
343
|
+
diagnostics: list[CompatibilityDiagnostic],
|
|
344
|
+
) -> dict[str, Any] | None:
|
|
345
|
+
source_path = f"$.flows.{flow_identifier}.steps[{step_index}]"
|
|
346
|
+
slot_name = step.get("collect")
|
|
347
|
+
if not isinstance(slot_name, str) or not slot_name:
|
|
348
|
+
diagnostics.append(
|
|
349
|
+
compatibility_diagnostic(
|
|
350
|
+
"error",
|
|
351
|
+
"RASA_COLLECT_SLOT_INVALID",
|
|
352
|
+
f"{source_path}.collect",
|
|
353
|
+
"collect must name a non-empty domain slot",
|
|
354
|
+
)
|
|
355
|
+
)
|
|
356
|
+
return None
|
|
357
|
+
if mapping_or_none(slot_declarations.get(slot_name)) is None or slot_name not in domain_names:
|
|
358
|
+
diagnostics.append(
|
|
359
|
+
compatibility_diagnostic(
|
|
360
|
+
"error",
|
|
361
|
+
"RASA_COLLECT_SLOT_MISSING",
|
|
362
|
+
f"{source_path}.collect",
|
|
363
|
+
f"slot {slot_name!r} has no portable domain declaration",
|
|
364
|
+
)
|
|
365
|
+
)
|
|
366
|
+
return None
|
|
367
|
+
|
|
368
|
+
allowed_properties = {"collect", "id", "description", "utter", "ask_before_filling"}
|
|
369
|
+
for property_name in step.keys() - allowed_properties:
|
|
370
|
+
diagnostics.append(
|
|
371
|
+
compatibility_diagnostic(
|
|
372
|
+
"error",
|
|
373
|
+
"RASA_STEP_CONTROL_UNSUPPORTED",
|
|
374
|
+
f"{source_path}.{property_name}",
|
|
375
|
+
f"collect property {property_name!r} changes control flow "
|
|
376
|
+
"outside the portable profile",
|
|
377
|
+
)
|
|
378
|
+
)
|
|
379
|
+
if "description" in step and not isinstance(step.get("description"), str):
|
|
380
|
+
diagnostics.append(
|
|
381
|
+
compatibility_diagnostic(
|
|
382
|
+
"error",
|
|
383
|
+
"RASA_COLLECT_DESCRIPTION_INVALID",
|
|
384
|
+
f"{source_path}.description",
|
|
385
|
+
"a collect description must be a string",
|
|
386
|
+
)
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
domain_name = domain_names[slot_name]
|
|
390
|
+
domain_specification = cast(Mapping[str, Any], flowspec_domains[domain_name])
|
|
391
|
+
response_key_value = step.get("utter", f"utter_ask_{slot_name}")
|
|
392
|
+
if not isinstance(response_key_value, str):
|
|
393
|
+
diagnostics.append(
|
|
394
|
+
compatibility_diagnostic(
|
|
395
|
+
"error",
|
|
396
|
+
"RASA_UTTER_KEY_INVALID",
|
|
397
|
+
f"{source_path}.utter",
|
|
398
|
+
"utter must name one domain response",
|
|
399
|
+
)
|
|
400
|
+
)
|
|
401
|
+
response_key_value = f"utter_ask_{slot_name}"
|
|
402
|
+
elif not response_key_value.startswith("utter_"):
|
|
403
|
+
diagnostics.append(
|
|
404
|
+
compatibility_diagnostic(
|
|
405
|
+
"error",
|
|
406
|
+
"RASA_RESPONSE_KEY_UNSUPPORTED",
|
|
407
|
+
f"{source_path}.utter",
|
|
408
|
+
"Rasa response keys referenced by collect steps must start with 'utter_'",
|
|
409
|
+
)
|
|
410
|
+
)
|
|
411
|
+
prompt, interactive = _import_prompt(
|
|
412
|
+
response_key_value,
|
|
413
|
+
slot_name,
|
|
414
|
+
responses,
|
|
415
|
+
domain_name,
|
|
416
|
+
domain_specification,
|
|
417
|
+
diagnostics,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
ask_before_filling = step.get("ask_before_filling", False)
|
|
421
|
+
if not isinstance(ask_before_filling, bool):
|
|
422
|
+
diagnostics.append(
|
|
423
|
+
compatibility_diagnostic(
|
|
424
|
+
"error",
|
|
425
|
+
"RASA_ASK_BEFORE_FILLING_INVALID",
|
|
426
|
+
f"{source_path}.ask_before_filling",
|
|
427
|
+
"ask_before_filling must be boolean",
|
|
428
|
+
)
|
|
429
|
+
)
|
|
430
|
+
ask_before_filling = False
|
|
431
|
+
if ask_before_filling:
|
|
432
|
+
diagnostics.append(
|
|
433
|
+
compatibility_diagnostic(
|
|
434
|
+
"error",
|
|
435
|
+
"RASA_ASK_BEFORE_FILLING_UNSUPPORTED",
|
|
436
|
+
f"{source_path}.ask_before_filling",
|
|
437
|
+
"Rasa clears a prefilled slot before asking, which flowspec2 "
|
|
438
|
+
"portable collect does not do",
|
|
439
|
+
)
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
flowspec_step: dict[str, Any] = {"slot": slot_name}
|
|
443
|
+
if isinstance(step.get("id"), str) and step["id"]:
|
|
444
|
+
flowspec_step["step"] = step["id"]
|
|
445
|
+
if prompt is not None:
|
|
446
|
+
if isinstance(step.get("description"), str):
|
|
447
|
+
prompt["extract_hint"] = step["description"]
|
|
448
|
+
flowspec_step["prompt"] = prompt
|
|
449
|
+
if interactive is not None:
|
|
450
|
+
flowspec_step["interactive"] = interactive
|
|
451
|
+
return flowspec_step
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _import_terminal_action(
|
|
455
|
+
step: Mapping[str, Any],
|
|
456
|
+
flow_identifier: str,
|
|
457
|
+
step_index: int,
|
|
458
|
+
step_count: int,
|
|
459
|
+
declared_actions: frozenset[str],
|
|
460
|
+
diagnostics: list[CompatibilityDiagnostic],
|
|
461
|
+
) -> tuple[dict[str, Any], dict[str, Any]] | None:
|
|
462
|
+
source_path = f"$.flows.{flow_identifier}.steps[{step_index}]"
|
|
463
|
+
if step_index != step_count - 1:
|
|
464
|
+
diagnostics.append(
|
|
465
|
+
compatibility_diagnostic(
|
|
466
|
+
"error",
|
|
467
|
+
"RASA_ACTION_POSITION_UNSUPPORTED",
|
|
468
|
+
source_path,
|
|
469
|
+
"only one final custom action can represent a flowspec2 terminal",
|
|
470
|
+
)
|
|
471
|
+
)
|
|
472
|
+
return None
|
|
473
|
+
for property_name in step.keys() - {"action", "id"}:
|
|
474
|
+
diagnostics.append(
|
|
475
|
+
compatibility_diagnostic(
|
|
476
|
+
"error",
|
|
477
|
+
"RASA_STEP_CONTROL_UNSUPPORTED",
|
|
478
|
+
f"{source_path}.{property_name}",
|
|
479
|
+
f"terminal action property {property_name!r} changes control flow",
|
|
480
|
+
)
|
|
481
|
+
)
|
|
482
|
+
action_name = step.get("action")
|
|
483
|
+
if not isinstance(action_name, str) or not RASA_ACTION_IDENTIFIER_PATTERN.fullmatch(
|
|
484
|
+
action_name
|
|
485
|
+
):
|
|
486
|
+
diagnostics.append(
|
|
487
|
+
compatibility_diagnostic(
|
|
488
|
+
"error",
|
|
489
|
+
"RASA_ACTION_NAME_UNSUPPORTED",
|
|
490
|
+
f"{source_path}.action",
|
|
491
|
+
"the final action must be a valid Rasa custom-action identifier",
|
|
492
|
+
)
|
|
493
|
+
)
|
|
494
|
+
return None
|
|
495
|
+
if action_name not in declared_actions:
|
|
496
|
+
diagnostics.append(
|
|
497
|
+
compatibility_diagnostic(
|
|
498
|
+
"error",
|
|
499
|
+
"RASA_ACTION_DECLARATION_MISSING",
|
|
500
|
+
f"{source_path}.action",
|
|
501
|
+
"the final custom action must be listed in domain.yml actions",
|
|
502
|
+
)
|
|
503
|
+
)
|
|
504
|
+
terminal_step_identifier = step.get("id", action_name)
|
|
505
|
+
if not isinstance(terminal_step_identifier, str) or not terminal_step_identifier:
|
|
506
|
+
terminal_step_identifier = action_name
|
|
507
|
+
|
|
508
|
+
diagnostics.append(
|
|
509
|
+
compatibility_diagnostic(
|
|
510
|
+
"warning",
|
|
511
|
+
"RASA_ACTION_ADAPTER_REQUIRED",
|
|
512
|
+
source_path,
|
|
513
|
+
"the imported terminal uses conservative lifecycle defaults: "
|
|
514
|
+
"non-idempotent and reset after every outcome",
|
|
515
|
+
)
|
|
516
|
+
)
|
|
517
|
+
terminal = {
|
|
518
|
+
"step": terminal_step_identifier,
|
|
519
|
+
"tool": action_name,
|
|
520
|
+
"idempotent": False,
|
|
521
|
+
"outcomes": {
|
|
522
|
+
"success": {"reset_next": True},
|
|
523
|
+
"retryable": {"preserve_state": False},
|
|
524
|
+
"fatal": {"reset_next": True},
|
|
525
|
+
},
|
|
526
|
+
}
|
|
527
|
+
return {"terminal": True}, terminal
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def _select_rasa_flow(
|
|
531
|
+
flows_document: Mapping[str, Any],
|
|
532
|
+
flow_id: str | None,
|
|
533
|
+
diagnostics: list[CompatibilityDiagnostic],
|
|
534
|
+
) -> tuple[str, Mapping[str, Any]] | None:
|
|
535
|
+
flows = mapping_or_none(flows_document.get("flows"))
|
|
536
|
+
if flows is None or not flows:
|
|
537
|
+
diagnostics.append(
|
|
538
|
+
compatibility_diagnostic(
|
|
539
|
+
"error",
|
|
540
|
+
"RASA_FLOWS_MAPPING_REQUIRED",
|
|
541
|
+
"$.flows",
|
|
542
|
+
"flows.yml must contain a non-empty flows mapping",
|
|
543
|
+
)
|
|
544
|
+
)
|
|
545
|
+
return None
|
|
546
|
+
string_flow_ids = [candidate_id for candidate_id in flows if isinstance(candidate_id, str)]
|
|
547
|
+
if len(string_flow_ids) != len(flows):
|
|
548
|
+
diagnostics.append(
|
|
549
|
+
compatibility_diagnostic(
|
|
550
|
+
"error",
|
|
551
|
+
"RASA_FLOW_ID_INVALID",
|
|
552
|
+
"$.flows",
|
|
553
|
+
"every Rasa flow id must be a string",
|
|
554
|
+
)
|
|
555
|
+
)
|
|
556
|
+
selected_flow_id = flow_id
|
|
557
|
+
if selected_flow_id is None:
|
|
558
|
+
if len(string_flow_ids) != 1:
|
|
559
|
+
diagnostics.append(
|
|
560
|
+
compatibility_diagnostic(
|
|
561
|
+
"error",
|
|
562
|
+
"RASA_FLOW_SELECTION_REQUIRED",
|
|
563
|
+
"$.flows",
|
|
564
|
+
"flow_id is required when flows.yml contains more than one flow",
|
|
565
|
+
)
|
|
566
|
+
)
|
|
567
|
+
selected_flow_id = string_flow_ids[0] if string_flow_ids else None
|
|
568
|
+
if selected_flow_id is None or selected_flow_id not in flows:
|
|
569
|
+
diagnostics.append(
|
|
570
|
+
compatibility_diagnostic(
|
|
571
|
+
"error",
|
|
572
|
+
"RASA_FLOW_NOT_FOUND",
|
|
573
|
+
"$.flows",
|
|
574
|
+
f"selected flow {selected_flow_id!r} does not exist",
|
|
575
|
+
)
|
|
576
|
+
)
|
|
577
|
+
return None
|
|
578
|
+
selected_flow = mapping_or_none(flows[selected_flow_id])
|
|
579
|
+
if selected_flow is None:
|
|
580
|
+
diagnostics.append(
|
|
581
|
+
compatibility_diagnostic(
|
|
582
|
+
"error",
|
|
583
|
+
"RASA_FLOW_DEFINITION_INVALID",
|
|
584
|
+
f"$.flows.{selected_flow_id}",
|
|
585
|
+
"the selected flow must be a mapping",
|
|
586
|
+
)
|
|
587
|
+
)
|
|
588
|
+
return None
|
|
589
|
+
return selected_flow_id, selected_flow
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def _import_flow_property_diagnostics(
|
|
593
|
+
flow_identifier: str,
|
|
594
|
+
flow_definition: Mapping[str, Any],
|
|
595
|
+
diagnostics: list[CompatibilityDiagnostic],
|
|
596
|
+
) -> None:
|
|
597
|
+
allowed_properties = {
|
|
598
|
+
"description",
|
|
599
|
+
"steps",
|
|
600
|
+
"persisted_slots",
|
|
601
|
+
"run_pattern_completed",
|
|
602
|
+
}
|
|
603
|
+
for property_name in flow_definition.keys() - allowed_properties:
|
|
604
|
+
severity: DiagnosticSeverity = "warning" if property_name == "name" else "error"
|
|
605
|
+
code = (
|
|
606
|
+
"RASA_FLOW_NAME_UNSUPPORTED"
|
|
607
|
+
if property_name == "name"
|
|
608
|
+
else "RASA_FLOW_CONTROL_UNSUPPORTED"
|
|
609
|
+
)
|
|
610
|
+
diagnostics.append(
|
|
611
|
+
compatibility_diagnostic(
|
|
612
|
+
severity,
|
|
613
|
+
code,
|
|
614
|
+
f"$.flows.{flow_identifier}.{property_name}",
|
|
615
|
+
(
|
|
616
|
+
"flowspec2 has no separate human-readable flow-name field"
|
|
617
|
+
if property_name == "name"
|
|
618
|
+
else f"Rasa flow property {property_name!r} is outside the "
|
|
619
|
+
"linear portable profile"
|
|
620
|
+
),
|
|
621
|
+
)
|
|
622
|
+
)
|
|
623
|
+
if flow_definition.get("run_pattern_completed") is not False:
|
|
624
|
+
diagnostics.append(
|
|
625
|
+
compatibility_diagnostic(
|
|
626
|
+
"error",
|
|
627
|
+
"RASA_COMPLETION_PATTERN_UNSUPPORTED",
|
|
628
|
+
f"$.flows.{flow_identifier}.run_pattern_completed",
|
|
629
|
+
"the portable profile requires run_pattern_completed: false because "
|
|
630
|
+
"Rasa otherwise runs completion-pattern behavior with no flowspec2 equivalent",
|
|
631
|
+
)
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def _import_document_property_diagnostics(
|
|
636
|
+
flows_document: Mapping[str, Any],
|
|
637
|
+
domain_document: Mapping[str, Any],
|
|
638
|
+
diagnostics: list[CompatibilityDiagnostic],
|
|
639
|
+
) -> None:
|
|
640
|
+
for property_name in flows_document.keys() - {"flows", "version"}:
|
|
641
|
+
diagnostics.append(
|
|
642
|
+
compatibility_diagnostic(
|
|
643
|
+
"error",
|
|
644
|
+
"RASA_FLOWS_DOCUMENT_PROPERTY_UNSUPPORTED",
|
|
645
|
+
f"$.{property_name}",
|
|
646
|
+
f"flows.yml property {property_name!r} is outside the portable profile",
|
|
647
|
+
)
|
|
648
|
+
)
|
|
649
|
+
for property_name in domain_document.keys() - {
|
|
650
|
+
"version",
|
|
651
|
+
"slots",
|
|
652
|
+
"responses",
|
|
653
|
+
"actions",
|
|
654
|
+
}:
|
|
655
|
+
diagnostics.append(
|
|
656
|
+
compatibility_diagnostic(
|
|
657
|
+
"error",
|
|
658
|
+
"RASA_DOMAIN_DOCUMENT_PROPERTY_UNSUPPORTED",
|
|
659
|
+
f"$.{property_name}",
|
|
660
|
+
f"domain.yml property {property_name!r} is outside the portable profile",
|
|
661
|
+
)
|
|
662
|
+
)
|
|
663
|
+
for document_name, document, virtual_root in (
|
|
664
|
+
("flows.yml", flows_document, "flows_document"),
|
|
665
|
+
("domain.yml", domain_document, "domain_document"),
|
|
666
|
+
):
|
|
667
|
+
if "version" in document and document.get("version") != RASA_DOMAIN_VERSION:
|
|
668
|
+
diagnostics.append(
|
|
669
|
+
compatibility_diagnostic(
|
|
670
|
+
"error",
|
|
671
|
+
"RASA_DOCUMENT_VERSION_UNSUPPORTED",
|
|
672
|
+
f"$.{virtual_root}.version",
|
|
673
|
+
f"{document_name} version must be {RASA_DOMAIN_VERSION!r} when declared",
|
|
674
|
+
)
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def _import_persisted_slots_diagnostics(
|
|
679
|
+
flow_identifier: str,
|
|
680
|
+
flow_definition: Mapping[str, Any],
|
|
681
|
+
referenced_slot_names: set[str],
|
|
682
|
+
diagnostics: list[CompatibilityDiagnostic],
|
|
683
|
+
) -> None:
|
|
684
|
+
source_path = f"$.flows.{flow_identifier}.persisted_slots"
|
|
685
|
+
if "persisted_slots" not in flow_definition:
|
|
686
|
+
if referenced_slot_names:
|
|
687
|
+
diagnostics.append(
|
|
688
|
+
compatibility_diagnostic(
|
|
689
|
+
"warning",
|
|
690
|
+
"RASA_COLLECTED_SLOT_RESET_UNSUPPORTED",
|
|
691
|
+
source_path,
|
|
692
|
+
"Rasa resets collected slots by default, while flowspec2 data slots persist",
|
|
693
|
+
)
|
|
694
|
+
)
|
|
695
|
+
return
|
|
696
|
+
persisted_slots = sequence_or_none(flow_definition.get("persisted_slots"))
|
|
697
|
+
if persisted_slots is None or not all(
|
|
698
|
+
isinstance(slot_name, str) for slot_name in persisted_slots
|
|
699
|
+
):
|
|
700
|
+
diagnostics.append(
|
|
701
|
+
compatibility_diagnostic(
|
|
702
|
+
"error",
|
|
703
|
+
"RASA_PERSISTED_SLOTS_INVALID",
|
|
704
|
+
source_path,
|
|
705
|
+
"persisted_slots must be a list of collected slot names",
|
|
706
|
+
)
|
|
707
|
+
)
|
|
708
|
+
return
|
|
709
|
+
persisted_slot_names = cast(Sequence[str], persisted_slots)
|
|
710
|
+
if len(persisted_slot_names) != len(set(persisted_slot_names)):
|
|
711
|
+
diagnostics.append(
|
|
712
|
+
compatibility_diagnostic(
|
|
713
|
+
"error",
|
|
714
|
+
"RASA_PERSISTED_SLOTS_INVALID",
|
|
715
|
+
source_path,
|
|
716
|
+
"persisted_slots must not repeat a slot name",
|
|
717
|
+
)
|
|
718
|
+
)
|
|
719
|
+
unexpected_slot_names = set(persisted_slot_names) - referenced_slot_names
|
|
720
|
+
if unexpected_slot_names:
|
|
721
|
+
diagnostics.append(
|
|
722
|
+
compatibility_diagnostic(
|
|
723
|
+
"error",
|
|
724
|
+
"RASA_PERSISTED_SLOT_NOT_COLLECTED",
|
|
725
|
+
source_path,
|
|
726
|
+
"persisted slots are not collected by the selected flow: "
|
|
727
|
+
f"{sorted(unexpected_slot_names)!r}",
|
|
728
|
+
)
|
|
729
|
+
)
|
|
730
|
+
reset_slot_names = referenced_slot_names - set(persisted_slot_names)
|
|
731
|
+
if reset_slot_names:
|
|
732
|
+
diagnostics.append(
|
|
733
|
+
compatibility_diagnostic(
|
|
734
|
+
"warning",
|
|
735
|
+
"RASA_COLLECTED_SLOT_RESET_UNSUPPORTED",
|
|
736
|
+
source_path,
|
|
737
|
+
"these collected slots reset in Rasa but persist in flowspec2: "
|
|
738
|
+
f"{sorted(reset_slot_names)!r}",
|
|
739
|
+
)
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def import_rasa(
|
|
744
|
+
flows_document: Mapping[str, Any],
|
|
745
|
+
domain_document: Mapping[str, Any],
|
|
746
|
+
*,
|
|
747
|
+
flow_id: str | None = None,
|
|
748
|
+
flow_version: str = "1.0.0",
|
|
749
|
+
allow_lossy: bool = False,
|
|
750
|
+
) -> ConversionOutcome[dict[str, Any]]:
|
|
751
|
+
"""Convert one selected Rasa CALM flow from the bounded portable subset."""
|
|
752
|
+
|
|
753
|
+
diagnostics: list[CompatibilityDiagnostic] = []
|
|
754
|
+
_import_document_property_diagnostics(
|
|
755
|
+
flows_document,
|
|
756
|
+
domain_document,
|
|
757
|
+
diagnostics,
|
|
758
|
+
)
|
|
759
|
+
selected_flow = _select_rasa_flow(flows_document, flow_id, diagnostics)
|
|
760
|
+
selected_flow_identifier = (
|
|
761
|
+
selected_flow[0] if selected_flow is not None else flow_id or "invalid_flow"
|
|
762
|
+
)
|
|
763
|
+
flow_definition = selected_flow[1] if selected_flow is not None else {}
|
|
764
|
+
_import_flow_property_diagnostics(selected_flow_identifier, flow_definition, diagnostics)
|
|
765
|
+
if not FLOWSPEC_FLOW_IDENTIFIER_PATTERN.fullmatch(selected_flow_identifier):
|
|
766
|
+
diagnostics.append(
|
|
767
|
+
compatibility_diagnostic(
|
|
768
|
+
"error",
|
|
769
|
+
"RASA_FLOW_ID_UNSUPPORTED",
|
|
770
|
+
f"$.flows.{selected_flow_identifier}",
|
|
771
|
+
"the selected id must already satisfy flowspec2's lowercase "
|
|
772
|
+
"underscore identifier contract",
|
|
773
|
+
)
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
description = flow_definition.get("description")
|
|
777
|
+
if not isinstance(description, str) or not description:
|
|
778
|
+
diagnostics.append(
|
|
779
|
+
compatibility_diagnostic(
|
|
780
|
+
"error",
|
|
781
|
+
"RASA_FLOW_DESCRIPTION_REQUIRED",
|
|
782
|
+
f"$.flows.{selected_flow_identifier}.description",
|
|
783
|
+
"the selected flow requires a non-empty description",
|
|
784
|
+
)
|
|
785
|
+
)
|
|
786
|
+
description = "Invalid imported Rasa flow"
|
|
787
|
+
steps = sequence_or_none(flow_definition.get("steps"))
|
|
788
|
+
if steps is None or not steps:
|
|
789
|
+
diagnostics.append(
|
|
790
|
+
compatibility_diagnostic(
|
|
791
|
+
"error",
|
|
792
|
+
"RASA_FLOW_STEPS_REQUIRED",
|
|
793
|
+
f"$.flows.{selected_flow_identifier}.steps",
|
|
794
|
+
"the selected flow requires a non-empty steps list",
|
|
795
|
+
)
|
|
796
|
+
)
|
|
797
|
+
steps = ()
|
|
798
|
+
step_identifier_diagnostics(
|
|
799
|
+
steps,
|
|
800
|
+
f"$.flows.{selected_flow_identifier}.steps",
|
|
801
|
+
diagnostics,
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
referenced_slot_names = {
|
|
805
|
+
slot_name
|
|
806
|
+
for step_value in steps
|
|
807
|
+
if (step := mapping_or_none(step_value)) is not None
|
|
808
|
+
and isinstance((slot_name := step.get("collect")), str)
|
|
809
|
+
and slot_name
|
|
810
|
+
}
|
|
811
|
+
if referenced_slot_names:
|
|
812
|
+
diagnostics.append(
|
|
813
|
+
compatibility_diagnostic(
|
|
814
|
+
"warning",
|
|
815
|
+
"RASA_LLM_SLOT_SEMANTICS_UNSUPPORTED",
|
|
816
|
+
f"$.flows.{selected_flow_identifier}.steps",
|
|
817
|
+
"Rasa CALM collection can fill or correct slots opportunistically "
|
|
818
|
+
"and processes interruption or repair commands during collection; "
|
|
819
|
+
"imported flowspec2 collection is sequential with explicit "
|
|
820
|
+
"correction and transition routing",
|
|
821
|
+
)
|
|
822
|
+
)
|
|
823
|
+
_import_persisted_slots_diagnostics(
|
|
824
|
+
selected_flow_identifier,
|
|
825
|
+
flow_definition,
|
|
826
|
+
referenced_slot_names,
|
|
827
|
+
diagnostics,
|
|
828
|
+
)
|
|
829
|
+
slot_declarations = mapping_or_none(domain_document.get("slots"))
|
|
830
|
+
if slot_declarations is None:
|
|
831
|
+
if referenced_slot_names:
|
|
832
|
+
diagnostics.append(
|
|
833
|
+
compatibility_diagnostic(
|
|
834
|
+
"error",
|
|
835
|
+
"RASA_DOMAIN_SLOTS_REQUIRED",
|
|
836
|
+
"$.slots",
|
|
837
|
+
"domain.yml must contain a slots mapping for every collected slot",
|
|
838
|
+
)
|
|
839
|
+
)
|
|
840
|
+
slot_declarations = {}
|
|
841
|
+
responses = mapping_or_none(domain_document.get("responses"))
|
|
842
|
+
if responses is None:
|
|
843
|
+
if referenced_slot_names:
|
|
844
|
+
diagnostics.append(
|
|
845
|
+
compatibility_diagnostic(
|
|
846
|
+
"error",
|
|
847
|
+
"RASA_RESPONSES_MAPPING_REQUIRED",
|
|
848
|
+
"$.responses",
|
|
849
|
+
"domain.yml must contain responses for every collected slot",
|
|
850
|
+
)
|
|
851
|
+
)
|
|
852
|
+
responses = {}
|
|
853
|
+
|
|
854
|
+
flowspec_domains: dict[str, Any] = {}
|
|
855
|
+
flowspec_slots: dict[str, Any] = {}
|
|
856
|
+
domain_names: dict[str, str] = {}
|
|
857
|
+
for slot_name in sorted(referenced_slot_names):
|
|
858
|
+
slot_declaration = mapping_or_none(slot_declarations.get(slot_name))
|
|
859
|
+
if slot_declaration is None:
|
|
860
|
+
diagnostics.append(
|
|
861
|
+
compatibility_diagnostic(
|
|
862
|
+
"error",
|
|
863
|
+
"RASA_SLOT_DEFINITION_INVALID",
|
|
864
|
+
f"$.slots.{slot_name}",
|
|
865
|
+
"a collected slot requires a mapping declaration in domain.yml",
|
|
866
|
+
)
|
|
867
|
+
)
|
|
868
|
+
continue
|
|
869
|
+
domain_specification = _import_domain_specification(
|
|
870
|
+
slot_name,
|
|
871
|
+
slot_declaration,
|
|
872
|
+
diagnostics,
|
|
873
|
+
)
|
|
874
|
+
if domain_specification is None:
|
|
875
|
+
continue
|
|
876
|
+
domain_name = f"rasa_{slot_name}"
|
|
877
|
+
domain_names[slot_name] = domain_name
|
|
878
|
+
flowspec_domains[domain_name] = domain_specification
|
|
879
|
+
flowspec_slots[slot_name] = {
|
|
880
|
+
"domain": domain_name,
|
|
881
|
+
"persist": "data",
|
|
882
|
+
"required": True,
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
actions_value = domain_document.get("actions", [])
|
|
886
|
+
actions = sequence_or_none(actions_value)
|
|
887
|
+
if actions is None or not all(isinstance(action_name, str) for action_name in actions):
|
|
888
|
+
diagnostics.append(
|
|
889
|
+
compatibility_diagnostic(
|
|
890
|
+
"error",
|
|
891
|
+
"RASA_ACTIONS_DECLARATION_INVALID",
|
|
892
|
+
"$.actions",
|
|
893
|
+
"domain.yml actions must be a list of custom-action names",
|
|
894
|
+
)
|
|
895
|
+
)
|
|
896
|
+
declared_actions: frozenset[str] = frozenset()
|
|
897
|
+
else:
|
|
898
|
+
declared_actions = frozenset(cast(Sequence[str], actions))
|
|
899
|
+
|
|
900
|
+
flowspec_path: list[dict[str, Any]] = []
|
|
901
|
+
imported_terminal: dict[str, Any] | None = None
|
|
902
|
+
for step_index, step_value in enumerate(steps):
|
|
903
|
+
step = mapping_or_none(step_value)
|
|
904
|
+
source_path = f"$.flows.{selected_flow_identifier}.steps[{step_index}]"
|
|
905
|
+
if step is None:
|
|
906
|
+
diagnostics.append(
|
|
907
|
+
compatibility_diagnostic(
|
|
908
|
+
"error",
|
|
909
|
+
"RASA_STEP_DEFINITION_INVALID",
|
|
910
|
+
source_path,
|
|
911
|
+
"each Rasa flow step must be a mapping",
|
|
912
|
+
)
|
|
913
|
+
)
|
|
914
|
+
continue
|
|
915
|
+
if "collect" in step:
|
|
916
|
+
imported_step = _import_collect_step(
|
|
917
|
+
step,
|
|
918
|
+
selected_flow_identifier,
|
|
919
|
+
step_index,
|
|
920
|
+
slot_declarations,
|
|
921
|
+
domain_names,
|
|
922
|
+
flowspec_domains,
|
|
923
|
+
responses,
|
|
924
|
+
diagnostics,
|
|
925
|
+
)
|
|
926
|
+
if imported_step is not None:
|
|
927
|
+
flowspec_path.append(imported_step)
|
|
928
|
+
continue
|
|
929
|
+
if "action" in step:
|
|
930
|
+
imported_action = _import_terminal_action(
|
|
931
|
+
step,
|
|
932
|
+
selected_flow_identifier,
|
|
933
|
+
step_index,
|
|
934
|
+
len(steps),
|
|
935
|
+
declared_actions,
|
|
936
|
+
diagnostics,
|
|
937
|
+
)
|
|
938
|
+
if imported_action is not None:
|
|
939
|
+
terminal_marker, imported_terminal = imported_action
|
|
940
|
+
flowspec_path.append(terminal_marker)
|
|
941
|
+
continue
|
|
942
|
+
if "collect" not in step:
|
|
943
|
+
step_type = next(
|
|
944
|
+
(
|
|
945
|
+
property_name
|
|
946
|
+
for property_name in ("set_slots", "call", "link", "noop")
|
|
947
|
+
if property_name in step
|
|
948
|
+
),
|
|
949
|
+
"unknown",
|
|
950
|
+
)
|
|
951
|
+
diagnostics.append(
|
|
952
|
+
compatibility_diagnostic(
|
|
953
|
+
"error",
|
|
954
|
+
"RASA_STEP_TYPE_UNSUPPORTED",
|
|
955
|
+
source_path,
|
|
956
|
+
f"Rasa step type {step_type!r} cannot be imported without "
|
|
957
|
+
"inventing flowspec2 semantics",
|
|
958
|
+
)
|
|
959
|
+
)
|
|
960
|
+
continue
|
|
961
|
+
|
|
962
|
+
imported_flow: dict[str, Any] = {
|
|
963
|
+
"schema": FLOWSPEC_FORMAT,
|
|
964
|
+
"flow": selected_flow_identifier,
|
|
965
|
+
"version": flow_version,
|
|
966
|
+
"route": {"description": description},
|
|
967
|
+
"domains": flowspec_domains,
|
|
968
|
+
"slots": flowspec_slots,
|
|
969
|
+
"path": flowspec_path,
|
|
970
|
+
}
|
|
971
|
+
if imported_terminal is not None:
|
|
972
|
+
imported_flow["terminal"] = imported_terminal
|
|
973
|
+
generated_validation_diagnostics = flowspec_validation_diagnostics(
|
|
974
|
+
imported_flow,
|
|
975
|
+
"RASA_GENERATED_FLOWSPEC_INVALID",
|
|
976
|
+
)
|
|
977
|
+
diagnostics.extend(generated_validation_diagnostics)
|
|
978
|
+
if not generated_validation_diagnostics and (
|
|
979
|
+
compilation_diagnostic := flowspec_compilation_diagnostic(
|
|
980
|
+
imported_flow,
|
|
981
|
+
"RASA_GENERATED_FLOWSPEC_NON_EXECUTABLE",
|
|
982
|
+
)
|
|
983
|
+
):
|
|
984
|
+
diagnostics.append(compilation_diagnostic)
|
|
985
|
+
|
|
986
|
+
report = CompatibilityReport(
|
|
987
|
+
source_format=RASA_FORMAT,
|
|
988
|
+
target_format=FLOWSPEC_FORMAT,
|
|
989
|
+
diagnostics=tuple(diagnostics),
|
|
990
|
+
)
|
|
991
|
+
enforce_compatibility_policy(report, allow_lossy=allow_lossy)
|
|
992
|
+
return ConversionOutcome(artifact=imported_flow, report=report)
|