agent-framework-declarative 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.
- agent_framework_declarative/__init__.py +71 -0
- agent_framework_declarative/_loader.py +868 -0
- agent_framework_declarative/_models.py +1154 -0
- agent_framework_declarative/_workflows/__init__.py +167 -0
- agent_framework_declarative/_workflows/_declarative_base.py +1226 -0
- agent_framework_declarative/_workflows/_declarative_builder.py +1057 -0
- agent_framework_declarative/_workflows/_errors.py +38 -0
- agent_framework_declarative/_workflows/_executors_agents.py +1025 -0
- agent_framework_declarative/_workflows/_executors_basic.py +574 -0
- agent_framework_declarative/_workflows/_executors_control_flow.py +461 -0
- agent_framework_declarative/_workflows/_executors_external_input.py +243 -0
- agent_framework_declarative/_workflows/_executors_http.py +417 -0
- agent_framework_declarative/_workflows/_executors_mcp.py +549 -0
- agent_framework_declarative/_workflows/_executors_tools.py +660 -0
- agent_framework_declarative/_workflows/_factory.py +808 -0
- agent_framework_declarative/_workflows/_http_handler.py +237 -0
- agent_framework_declarative/_workflows/_mcp_handler.py +581 -0
- agent_framework_declarative/_workflows/_powerfx_functions.py +498 -0
- agent_framework_declarative/_workflows/_state.py +650 -0
- agent_framework_declarative-1.0.0.dist-info/METADATA +49 -0
- agent_framework_declarative-1.0.0.dist-info/RECORD +23 -0
- agent_framework_declarative-1.0.0.dist-info/WHEEL +4 -0
- agent_framework_declarative-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Basic action executors for the graph-based declarative workflow system.
|
|
4
|
+
|
|
5
|
+
These executors handle simple actions like SetValue, SendActivity, etc.
|
|
6
|
+
Each action becomes a node in the workflow graph.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import uuid
|
|
10
|
+
from collections.abc import Mapping
|
|
11
|
+
from typing import Any, cast
|
|
12
|
+
|
|
13
|
+
from agent_framework import (
|
|
14
|
+
WorkflowContext,
|
|
15
|
+
handler,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from ._declarative_base import (
|
|
19
|
+
ActionComplete,
|
|
20
|
+
DeclarativeActionExecutor,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _get_variable_path(action_def: dict[str, Any], key: str = "variable") -> str | None:
|
|
25
|
+
"""Extract variable path from action definition.
|
|
26
|
+
|
|
27
|
+
Supports .NET style (variable: Local.VarName) and nested object style (variable: {path: ...}).
|
|
28
|
+
"""
|
|
29
|
+
variable = action_def.get(key)
|
|
30
|
+
if isinstance(variable, str):
|
|
31
|
+
return variable
|
|
32
|
+
if isinstance(variable, Mapping):
|
|
33
|
+
path = variable.get("path") # type: ignore[reportUnknownVariableType]
|
|
34
|
+
return path if isinstance(path, str) else None
|
|
35
|
+
|
|
36
|
+
fallback_path = action_def.get("path")
|
|
37
|
+
return fallback_path if isinstance(fallback_path, str) else None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SetValueExecutor(DeclarativeActionExecutor):
|
|
41
|
+
"""Executor for the SetValue action.
|
|
42
|
+
|
|
43
|
+
Sets a value in the workflow state at a specified path.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
@handler
|
|
47
|
+
async def handle_action(
|
|
48
|
+
self,
|
|
49
|
+
trigger: Any,
|
|
50
|
+
ctx: WorkflowContext[ActionComplete],
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Handle the SetValue action."""
|
|
53
|
+
state = await self._ensure_state_initialized(ctx, trigger)
|
|
54
|
+
|
|
55
|
+
path = self._action_def.get("path")
|
|
56
|
+
value = self._action_def.get("value")
|
|
57
|
+
|
|
58
|
+
if path:
|
|
59
|
+
# Evaluate value if it's an expression
|
|
60
|
+
evaluated_value = state.eval_if_expression(value)
|
|
61
|
+
state.set(path, evaluated_value)
|
|
62
|
+
|
|
63
|
+
await ctx.send_message(ActionComplete())
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class SetVariableExecutor(DeclarativeActionExecutor):
|
|
67
|
+
"""Executor for the SetVariable action (.NET style naming)."""
|
|
68
|
+
|
|
69
|
+
@handler
|
|
70
|
+
async def handle_action(
|
|
71
|
+
self,
|
|
72
|
+
trigger: Any,
|
|
73
|
+
ctx: WorkflowContext[ActionComplete],
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Handle the SetVariable action."""
|
|
76
|
+
state = await self._ensure_state_initialized(ctx, trigger)
|
|
77
|
+
|
|
78
|
+
path = _get_variable_path(self._action_def)
|
|
79
|
+
value = self._action_def.get("value")
|
|
80
|
+
|
|
81
|
+
if path:
|
|
82
|
+
evaluated_value = state.eval_if_expression(value)
|
|
83
|
+
state.set(path, evaluated_value)
|
|
84
|
+
|
|
85
|
+
await ctx.send_message(ActionComplete())
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class CreateConversationExecutor(DeclarativeActionExecutor):
|
|
89
|
+
"""Executor for the CreateConversation action.
|
|
90
|
+
|
|
91
|
+
Generates a unique conversation ID and initialises a conversation entry
|
|
92
|
+
in ``System.conversations``. The generated ID is stored at the state
|
|
93
|
+
path specified by the ``conversationId`` parameter (if provided).
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
@handler
|
|
97
|
+
async def handle_action(
|
|
98
|
+
self,
|
|
99
|
+
trigger: Any,
|
|
100
|
+
ctx: WorkflowContext[ActionComplete],
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Handle the CreateConversation action."""
|
|
103
|
+
state = await self._ensure_state_initialized(ctx, trigger)
|
|
104
|
+
|
|
105
|
+
generated_id = str(uuid.uuid4())
|
|
106
|
+
|
|
107
|
+
# Store the generated ID at the requested path (e.g. "Local.myConvId")
|
|
108
|
+
conversation_id_path = _get_variable_path(self._action_def, "conversationId")
|
|
109
|
+
if conversation_id_path:
|
|
110
|
+
state.set(conversation_id_path, generated_id)
|
|
111
|
+
|
|
112
|
+
# Initialise the conversation entry in System.conversations
|
|
113
|
+
conversations: dict[str, Any] = state.get("System.conversations") or {}
|
|
114
|
+
conversations[generated_id] = {
|
|
115
|
+
"id": generated_id,
|
|
116
|
+
"messages": [],
|
|
117
|
+
}
|
|
118
|
+
state.set("System.conversations", conversations)
|
|
119
|
+
|
|
120
|
+
await ctx.send_message(ActionComplete())
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class SetTextVariableExecutor(DeclarativeActionExecutor):
|
|
124
|
+
"""Executor for the SetTextVariable action."""
|
|
125
|
+
|
|
126
|
+
@handler
|
|
127
|
+
async def handle_action(
|
|
128
|
+
self,
|
|
129
|
+
trigger: Any,
|
|
130
|
+
ctx: WorkflowContext[ActionComplete],
|
|
131
|
+
) -> None:
|
|
132
|
+
"""Handle the SetTextVariable action."""
|
|
133
|
+
state = await self._ensure_state_initialized(ctx, trigger)
|
|
134
|
+
|
|
135
|
+
path = _get_variable_path(self._action_def)
|
|
136
|
+
text = self._action_def.get("text", "")
|
|
137
|
+
|
|
138
|
+
if path:
|
|
139
|
+
evaluated_text = state.eval_if_expression(text)
|
|
140
|
+
state.set(path, str(evaluated_text) if evaluated_text is not None else "")
|
|
141
|
+
|
|
142
|
+
await ctx.send_message(ActionComplete())
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class SetMultipleVariablesExecutor(DeclarativeActionExecutor):
|
|
146
|
+
"""Executor for the SetMultipleVariables action."""
|
|
147
|
+
|
|
148
|
+
@handler
|
|
149
|
+
async def handle_action(
|
|
150
|
+
self,
|
|
151
|
+
trigger: Any,
|
|
152
|
+
ctx: WorkflowContext[ActionComplete],
|
|
153
|
+
) -> None:
|
|
154
|
+
"""Handle the SetMultipleVariables action."""
|
|
155
|
+
state = await self._ensure_state_initialized(ctx, trigger)
|
|
156
|
+
|
|
157
|
+
assignments = cast(
|
|
158
|
+
list[Mapping[str, Any]],
|
|
159
|
+
self._action_def.get("assignments") if isinstance(self._action_def.get("assignments"), list) else [],
|
|
160
|
+
)
|
|
161
|
+
for assignment in assignments:
|
|
162
|
+
if not isinstance(assignment, Mapping):
|
|
163
|
+
continue
|
|
164
|
+
variable = assignment.get("variable")
|
|
165
|
+
path: str | None
|
|
166
|
+
if isinstance(variable, str):
|
|
167
|
+
path = variable
|
|
168
|
+
elif isinstance(variable, Mapping):
|
|
169
|
+
path_value = variable.get("path") # type: ignore[reportUnknownMemberType]
|
|
170
|
+
path = path_value if isinstance(path_value, str) else None
|
|
171
|
+
else:
|
|
172
|
+
fallback_path = assignment.get("path")
|
|
173
|
+
path = fallback_path if isinstance(fallback_path, str) else None
|
|
174
|
+
value = assignment.get("value")
|
|
175
|
+
if path:
|
|
176
|
+
evaluated_value = state.eval_if_expression(value)
|
|
177
|
+
state.set(path, evaluated_value)
|
|
178
|
+
|
|
179
|
+
await ctx.send_message(ActionComplete())
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class ResetVariableExecutor(DeclarativeActionExecutor):
|
|
183
|
+
"""Executor for the ResetVariable action."""
|
|
184
|
+
|
|
185
|
+
@handler
|
|
186
|
+
async def handle_action(
|
|
187
|
+
self,
|
|
188
|
+
trigger: Any,
|
|
189
|
+
ctx: WorkflowContext[ActionComplete],
|
|
190
|
+
) -> None:
|
|
191
|
+
"""Handle the ResetVariable action."""
|
|
192
|
+
state = await self._ensure_state_initialized(ctx, trigger)
|
|
193
|
+
|
|
194
|
+
path = _get_variable_path(self._action_def)
|
|
195
|
+
|
|
196
|
+
if path:
|
|
197
|
+
# Reset to None/empty
|
|
198
|
+
state.set(path, None)
|
|
199
|
+
|
|
200
|
+
await ctx.send_message(ActionComplete())
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class ClearAllVariablesExecutor(DeclarativeActionExecutor):
|
|
204
|
+
"""Executor for the ClearAllVariables action."""
|
|
205
|
+
|
|
206
|
+
@handler
|
|
207
|
+
async def handle_action(
|
|
208
|
+
self,
|
|
209
|
+
trigger: Any,
|
|
210
|
+
ctx: WorkflowContext[ActionComplete],
|
|
211
|
+
) -> None:
|
|
212
|
+
"""Handle the ClearAllVariables action."""
|
|
213
|
+
state = await self._ensure_state_initialized(ctx, trigger)
|
|
214
|
+
|
|
215
|
+
# Get state data and clear Local variables
|
|
216
|
+
state_data = state.get_state_data()
|
|
217
|
+
state_data["Local"] = {}
|
|
218
|
+
state.set_state_data(state_data)
|
|
219
|
+
|
|
220
|
+
await ctx.send_message(ActionComplete())
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class SendActivityExecutor(DeclarativeActionExecutor):
|
|
224
|
+
"""Executor for the SendActivity action.
|
|
225
|
+
|
|
226
|
+
Sends a text message or activity as workflow output.
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
@handler
|
|
230
|
+
async def handle_action(
|
|
231
|
+
self,
|
|
232
|
+
trigger: Any,
|
|
233
|
+
ctx: WorkflowContext[ActionComplete, str],
|
|
234
|
+
) -> None:
|
|
235
|
+
"""Handle the SendActivity action."""
|
|
236
|
+
state = await self._ensure_state_initialized(ctx, trigger)
|
|
237
|
+
|
|
238
|
+
activity = self._action_def.get("activity", "")
|
|
239
|
+
|
|
240
|
+
# Activity can be a string directly or a dict with a "text" field
|
|
241
|
+
if isinstance(activity, Mapping):
|
|
242
|
+
text: Any = activity.get("text", "") # type: ignore[reportUnknownMemberType]
|
|
243
|
+
else:
|
|
244
|
+
text = activity
|
|
245
|
+
|
|
246
|
+
if isinstance(text, str):
|
|
247
|
+
# First evaluate any =expression syntax
|
|
248
|
+
text = state.eval_if_expression(text)
|
|
249
|
+
# Then interpolate any {Variable.Path} template syntax
|
|
250
|
+
if isinstance(text, str):
|
|
251
|
+
text = state.interpolate_string(text)
|
|
252
|
+
|
|
253
|
+
# Yield the text as workflow output
|
|
254
|
+
if text:
|
|
255
|
+
await ctx.yield_output(str(text)) # type: ignore[reportUnknownArgumentType]
|
|
256
|
+
|
|
257
|
+
await ctx.send_message(ActionComplete())
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class EditTableExecutor(DeclarativeActionExecutor):
|
|
261
|
+
"""Executor for the EditTable action.
|
|
262
|
+
|
|
263
|
+
Performs operations on a table (list) variable such as add, remove, or clear.
|
|
264
|
+
This is equivalent to the .NET EditTable action.
|
|
265
|
+
|
|
266
|
+
YAML example:
|
|
267
|
+
- kind: EditTable
|
|
268
|
+
table: Local.Items
|
|
269
|
+
operation: add # add, remove, clear
|
|
270
|
+
value: =Local.NewItem
|
|
271
|
+
index: 0 # optional, for insert at position
|
|
272
|
+
"""
|
|
273
|
+
|
|
274
|
+
@handler
|
|
275
|
+
async def handle_action(
|
|
276
|
+
self,
|
|
277
|
+
trigger: Any,
|
|
278
|
+
ctx: WorkflowContext[ActionComplete],
|
|
279
|
+
) -> None:
|
|
280
|
+
"""Handle the EditTable action."""
|
|
281
|
+
state = await self._ensure_state_initialized(ctx, trigger)
|
|
282
|
+
|
|
283
|
+
table_path = self._action_def.get("table") or _get_variable_path(self._action_def, "variable")
|
|
284
|
+
operation = self._action_def.get("operation", "add").lower()
|
|
285
|
+
value = self._action_def.get("value")
|
|
286
|
+
index = self._action_def.get("index")
|
|
287
|
+
|
|
288
|
+
if table_path:
|
|
289
|
+
# Get current table value
|
|
290
|
+
current_table_value = state.get(table_path)
|
|
291
|
+
current_table: list[Any]
|
|
292
|
+
if current_table_value is None:
|
|
293
|
+
current_table = []
|
|
294
|
+
elif isinstance(current_table_value, list):
|
|
295
|
+
current_table = list(current_table_value) # type: ignore[reportUnknownArgumentType]
|
|
296
|
+
else:
|
|
297
|
+
current_table = [current_table_value]
|
|
298
|
+
|
|
299
|
+
if operation == "add" or operation == "insert":
|
|
300
|
+
evaluated_value = state.eval_if_expression(value)
|
|
301
|
+
if index is not None:
|
|
302
|
+
evaluated_index = state.eval_if_expression(index)
|
|
303
|
+
idx = int(evaluated_index) if evaluated_index is not None else len(current_table)
|
|
304
|
+
current_table.insert(idx, evaluated_value)
|
|
305
|
+
else:
|
|
306
|
+
current_table.append(evaluated_value)
|
|
307
|
+
|
|
308
|
+
elif operation == "remove":
|
|
309
|
+
if value is not None:
|
|
310
|
+
# Remove by value
|
|
311
|
+
evaluated_value = state.eval_if_expression(value)
|
|
312
|
+
if evaluated_value in current_table:
|
|
313
|
+
current_table.remove(evaluated_value)
|
|
314
|
+
elif index is not None:
|
|
315
|
+
# Remove by index
|
|
316
|
+
evaluated_index = state.eval_if_expression(index)
|
|
317
|
+
idx = int(evaluated_index) if evaluated_index is not None else -1
|
|
318
|
+
if 0 <= idx < len(current_table):
|
|
319
|
+
current_table.pop(idx)
|
|
320
|
+
|
|
321
|
+
elif operation == "clear":
|
|
322
|
+
current_table = []
|
|
323
|
+
|
|
324
|
+
elif operation == "set" or operation == "update":
|
|
325
|
+
# Update item at index
|
|
326
|
+
if index is not None:
|
|
327
|
+
evaluated_value = state.eval_if_expression(value)
|
|
328
|
+
evaluated_index = state.eval_if_expression(index)
|
|
329
|
+
idx = int(evaluated_index) if evaluated_index is not None else 0
|
|
330
|
+
if 0 <= idx < len(current_table):
|
|
331
|
+
current_table[idx] = evaluated_value
|
|
332
|
+
|
|
333
|
+
state.set(table_path, current_table)
|
|
334
|
+
|
|
335
|
+
await ctx.send_message(ActionComplete())
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class EditTableV2Executor(DeclarativeActionExecutor):
|
|
339
|
+
"""Executor for the EditTableV2 action.
|
|
340
|
+
|
|
341
|
+
Enhanced table editing with more operations and better record support.
|
|
342
|
+
This is equivalent to the .NET EditTableV2 action.
|
|
343
|
+
|
|
344
|
+
YAML example:
|
|
345
|
+
- kind: EditTableV2
|
|
346
|
+
table: Local.Records
|
|
347
|
+
operation: addOrUpdate # add, remove, clear, addOrUpdate, filter
|
|
348
|
+
item: =Local.NewRecord
|
|
349
|
+
key: id # for addOrUpdate, the field to match on
|
|
350
|
+
condition: =item.status = "active" # for filter operation
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
@handler
|
|
354
|
+
async def handle_action(
|
|
355
|
+
self,
|
|
356
|
+
trigger: Any,
|
|
357
|
+
ctx: WorkflowContext[ActionComplete],
|
|
358
|
+
) -> None:
|
|
359
|
+
"""Handle the EditTableV2 action."""
|
|
360
|
+
state = await self._ensure_state_initialized(ctx, trigger)
|
|
361
|
+
|
|
362
|
+
table_path = self._action_def.get("table") or _get_variable_path(self._action_def, "variable")
|
|
363
|
+
operation = self._action_def.get("operation", "add").lower()
|
|
364
|
+
item = self._action_def.get("item") or self._action_def.get("value")
|
|
365
|
+
key_field = self._action_def.get("key")
|
|
366
|
+
index = self._action_def.get("index")
|
|
367
|
+
|
|
368
|
+
if table_path:
|
|
369
|
+
# Get current table value
|
|
370
|
+
current_table_value = state.get(table_path)
|
|
371
|
+
current_table: list[Any]
|
|
372
|
+
if current_table_value is None:
|
|
373
|
+
current_table = []
|
|
374
|
+
elif isinstance(current_table_value, list):
|
|
375
|
+
current_table = list(current_table_value) # type: ignore[reportUnknownArgumentType]
|
|
376
|
+
else:
|
|
377
|
+
current_table = [current_table_value]
|
|
378
|
+
|
|
379
|
+
if operation == "add":
|
|
380
|
+
evaluated_item = state.eval_if_expression(item)
|
|
381
|
+
if index is not None:
|
|
382
|
+
evaluated_index = state.eval_if_expression(index)
|
|
383
|
+
idx = int(evaluated_index) if evaluated_index is not None else len(current_table)
|
|
384
|
+
current_table.insert(idx, evaluated_item)
|
|
385
|
+
else:
|
|
386
|
+
current_table.append(evaluated_item)
|
|
387
|
+
|
|
388
|
+
elif operation == "remove":
|
|
389
|
+
if item is not None:
|
|
390
|
+
evaluated_item = state.eval_if_expression(item)
|
|
391
|
+
if key_field and isinstance(evaluated_item, dict):
|
|
392
|
+
# Remove by key match
|
|
393
|
+
evaluated_item_dict = cast(dict[str, Any], evaluated_item)
|
|
394
|
+
key_value = evaluated_item_dict.get(key_field)
|
|
395
|
+
current_table = [
|
|
396
|
+
r
|
|
397
|
+
for r in current_table
|
|
398
|
+
if not (isinstance(r, dict) and cast(dict[str, Any], r).get(key_field) == key_value)
|
|
399
|
+
]
|
|
400
|
+
elif evaluated_item in current_table:
|
|
401
|
+
current_table.remove(evaluated_item)
|
|
402
|
+
elif index is not None:
|
|
403
|
+
evaluated_index = state.eval_if_expression(index)
|
|
404
|
+
idx = int(evaluated_index) if evaluated_index is not None else -1
|
|
405
|
+
if 0 <= idx < len(current_table):
|
|
406
|
+
current_table.pop(idx)
|
|
407
|
+
|
|
408
|
+
elif operation == "clear":
|
|
409
|
+
current_table = []
|
|
410
|
+
|
|
411
|
+
elif operation == "addorupdate":
|
|
412
|
+
evaluated_item = state.eval_if_expression(item)
|
|
413
|
+
if key_field and isinstance(evaluated_item, dict):
|
|
414
|
+
key_value = evaluated_item.get(key_field) # type: ignore[reportUnknownArgumentType]
|
|
415
|
+
# Find existing item with same key
|
|
416
|
+
found_idx = -1
|
|
417
|
+
for i, r in enumerate(current_table):
|
|
418
|
+
if isinstance(r, dict) and cast(dict[str, Any], r).get(key_field) == key_value:
|
|
419
|
+
found_idx = i
|
|
420
|
+
break
|
|
421
|
+
if found_idx >= 0:
|
|
422
|
+
# Update existing
|
|
423
|
+
current_table[found_idx] = evaluated_item
|
|
424
|
+
else:
|
|
425
|
+
# Add new
|
|
426
|
+
current_table.append(evaluated_item)
|
|
427
|
+
else:
|
|
428
|
+
# No key field - just add
|
|
429
|
+
current_table.append(evaluated_item)
|
|
430
|
+
|
|
431
|
+
elif operation == "update":
|
|
432
|
+
evaluated_item = state.eval_if_expression(item)
|
|
433
|
+
if index is not None:
|
|
434
|
+
evaluated_index = state.eval_if_expression(index)
|
|
435
|
+
idx = int(evaluated_index) if evaluated_index is not None else 0
|
|
436
|
+
if 0 <= idx < len(current_table):
|
|
437
|
+
current_table[idx] = evaluated_item
|
|
438
|
+
elif key_field and isinstance(evaluated_item, dict):
|
|
439
|
+
key_value = evaluated_item.get(key_field) # type: ignore[reportUnknownArgumentType]
|
|
440
|
+
for i, r in enumerate(current_table):
|
|
441
|
+
if isinstance(r, dict) and cast(dict[str, Any], r).get(key_field) == key_value:
|
|
442
|
+
current_table[i] = evaluated_item
|
|
443
|
+
break
|
|
444
|
+
|
|
445
|
+
state.set(table_path, current_table)
|
|
446
|
+
|
|
447
|
+
await ctx.send_message(ActionComplete())
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
class ParseValueExecutor(DeclarativeActionExecutor):
|
|
451
|
+
"""Executor for the ParseValue action.
|
|
452
|
+
|
|
453
|
+
Parses a value expression and optionally converts it to a target type.
|
|
454
|
+
This is equivalent to the .NET ParseValue action.
|
|
455
|
+
|
|
456
|
+
YAML example:
|
|
457
|
+
- kind: ParseValue
|
|
458
|
+
variable: Local.ParsedData
|
|
459
|
+
value: =System.LastMessage.Text
|
|
460
|
+
valueType: object # optional: string, number, boolean, object, array
|
|
461
|
+
"""
|
|
462
|
+
|
|
463
|
+
@handler
|
|
464
|
+
async def handle_action(
|
|
465
|
+
self,
|
|
466
|
+
trigger: Any,
|
|
467
|
+
ctx: WorkflowContext[ActionComplete],
|
|
468
|
+
) -> None:
|
|
469
|
+
"""Handle the ParseValue action."""
|
|
470
|
+
state = await self._ensure_state_initialized(ctx, trigger)
|
|
471
|
+
|
|
472
|
+
path = _get_variable_path(self._action_def)
|
|
473
|
+
value = self._action_def.get("value")
|
|
474
|
+
value_type = self._action_def.get("valueType")
|
|
475
|
+
|
|
476
|
+
if path and value is not None:
|
|
477
|
+
# Evaluate the value expression
|
|
478
|
+
evaluated_value = state.eval_if_expression(value)
|
|
479
|
+
|
|
480
|
+
# Convert to target type if specified
|
|
481
|
+
if value_type:
|
|
482
|
+
evaluated_value = self._convert_to_type(evaluated_value, value_type)
|
|
483
|
+
|
|
484
|
+
state.set(path, evaluated_value)
|
|
485
|
+
|
|
486
|
+
await ctx.send_message(ActionComplete())
|
|
487
|
+
|
|
488
|
+
def _convert_to_type(self, value: Any, target_type: str) -> Any:
|
|
489
|
+
"""Convert a value to the specified target type.
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
value: The value to convert
|
|
493
|
+
target_type: Target type (string, number, boolean, object, array)
|
|
494
|
+
|
|
495
|
+
Returns:
|
|
496
|
+
The converted value
|
|
497
|
+
"""
|
|
498
|
+
import json
|
|
499
|
+
|
|
500
|
+
target_type = target_type.lower()
|
|
501
|
+
|
|
502
|
+
if target_type == "string":
|
|
503
|
+
if value is None:
|
|
504
|
+
return ""
|
|
505
|
+
return str(value)
|
|
506
|
+
|
|
507
|
+
if target_type in ("number", "int", "integer", "float", "decimal"):
|
|
508
|
+
if value is None:
|
|
509
|
+
return 0
|
|
510
|
+
if isinstance(value, str):
|
|
511
|
+
# Try to parse as number
|
|
512
|
+
try:
|
|
513
|
+
if "." in value:
|
|
514
|
+
return float(value)
|
|
515
|
+
return int(value)
|
|
516
|
+
except ValueError:
|
|
517
|
+
return 0
|
|
518
|
+
return float(value) if isinstance(value, (int, float)) else 0
|
|
519
|
+
|
|
520
|
+
if target_type in ("boolean", "bool"):
|
|
521
|
+
if value is None:
|
|
522
|
+
return False
|
|
523
|
+
if isinstance(value, str):
|
|
524
|
+
return value.lower() in ("true", "yes", "1", "on")
|
|
525
|
+
return bool(value)
|
|
526
|
+
|
|
527
|
+
if target_type in ("object", "record"):
|
|
528
|
+
if value is None:
|
|
529
|
+
return {}
|
|
530
|
+
if isinstance(value, dict):
|
|
531
|
+
return cast(dict[str, Any], value)
|
|
532
|
+
if isinstance(value, str):
|
|
533
|
+
try:
|
|
534
|
+
parsed = json.loads(value)
|
|
535
|
+
if isinstance(parsed, dict):
|
|
536
|
+
return cast(dict[str, Any], parsed)
|
|
537
|
+
return {"value": parsed}
|
|
538
|
+
except json.JSONDecodeError:
|
|
539
|
+
return {"value": value}
|
|
540
|
+
return {"value": value}
|
|
541
|
+
|
|
542
|
+
if target_type in ("array", "table", "list"):
|
|
543
|
+
if value is None:
|
|
544
|
+
return []
|
|
545
|
+
if isinstance(value, list):
|
|
546
|
+
return cast(list[Any], value)
|
|
547
|
+
if isinstance(value, str):
|
|
548
|
+
try:
|
|
549
|
+
parsed = json.loads(value)
|
|
550
|
+
if isinstance(parsed, list):
|
|
551
|
+
return cast(list[Any], parsed)
|
|
552
|
+
return [parsed]
|
|
553
|
+
except json.JSONDecodeError:
|
|
554
|
+
return [value]
|
|
555
|
+
return [value]
|
|
556
|
+
|
|
557
|
+
# Unknown type - return as-is
|
|
558
|
+
return value
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
# Mapping of action kinds to executor classes
|
|
562
|
+
BASIC_ACTION_EXECUTORS: dict[str, type[DeclarativeActionExecutor]] = {
|
|
563
|
+
"CreateConversation": CreateConversationExecutor,
|
|
564
|
+
"SetValue": SetValueExecutor,
|
|
565
|
+
"SetVariable": SetVariableExecutor,
|
|
566
|
+
"SetTextVariable": SetTextVariableExecutor,
|
|
567
|
+
"SetMultipleVariables": SetMultipleVariablesExecutor,
|
|
568
|
+
"ResetVariable": ResetVariableExecutor,
|
|
569
|
+
"ClearAllVariables": ClearAllVariablesExecutor,
|
|
570
|
+
"SendActivity": SendActivityExecutor,
|
|
571
|
+
"ParseValue": ParseValueExecutor,
|
|
572
|
+
"EditTable": EditTableExecutor,
|
|
573
|
+
"EditTableV2": EditTableV2Executor,
|
|
574
|
+
}
|