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,498 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Custom PowerFx-like functions for declarative workflows.
|
|
4
|
+
|
|
5
|
+
This module provides Python implementations of custom PowerFx functions
|
|
6
|
+
that are used in declarative workflows but may not be available in the
|
|
7
|
+
standard PowerFx Python package.
|
|
8
|
+
|
|
9
|
+
These functions can be used as fallbacks when PowerFx is not available,
|
|
10
|
+
or registered with the PowerFx engine when it is available.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any, cast
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def message_text(messages: Any) -> str:
|
|
19
|
+
"""Extract text content from a message or list of messages.
|
|
20
|
+
|
|
21
|
+
This is equivalent to the .NET MessageText() function.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
messages: A message object, list of messages, or string
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
The concatenated text content of all messages
|
|
28
|
+
|
|
29
|
+
Examples:
|
|
30
|
+
.. code-block:: python
|
|
31
|
+
|
|
32
|
+
message_text([{"role": "assistant", "content": "Hello"}])
|
|
33
|
+
# Returns: 'Hello'
|
|
34
|
+
"""
|
|
35
|
+
if messages is None:
|
|
36
|
+
return ""
|
|
37
|
+
|
|
38
|
+
if isinstance(messages, str):
|
|
39
|
+
return messages
|
|
40
|
+
|
|
41
|
+
if isinstance(messages, dict):
|
|
42
|
+
# Single message object
|
|
43
|
+
messages_dict = cast(dict[str, Any], messages)
|
|
44
|
+
content: Any = messages_dict.get("content", "")
|
|
45
|
+
if isinstance(content, str):
|
|
46
|
+
return content
|
|
47
|
+
text_attr = getattr(content, "text", None)
|
|
48
|
+
if text_attr is not None:
|
|
49
|
+
return str(text_attr)
|
|
50
|
+
return str(content) if content else ""
|
|
51
|
+
|
|
52
|
+
if isinstance(messages, list):
|
|
53
|
+
# List of messages - concatenate all text
|
|
54
|
+
texts: list[str] = []
|
|
55
|
+
message_list = cast(list[Any], messages)
|
|
56
|
+
for msg in message_list:
|
|
57
|
+
if isinstance(msg, str):
|
|
58
|
+
texts.append(msg)
|
|
59
|
+
elif isinstance(msg, dict):
|
|
60
|
+
msg_dict = cast(dict[str, Any], msg)
|
|
61
|
+
msg_content: Any = msg_dict.get("content", "")
|
|
62
|
+
if isinstance(msg_content, str):
|
|
63
|
+
texts.append(msg_content)
|
|
64
|
+
elif msg_content:
|
|
65
|
+
texts.append(str(msg_content))
|
|
66
|
+
else:
|
|
67
|
+
msg_obj: object = msg
|
|
68
|
+
if hasattr(msg_obj, "content"):
|
|
69
|
+
msg_obj_content: Any = getattr(msg_obj, "content", None)
|
|
70
|
+
if isinstance(msg_obj_content, str):
|
|
71
|
+
texts.append(msg_obj_content)
|
|
72
|
+
elif (msg_obj_text := getattr(msg_obj_content, "text", None)) is not None:
|
|
73
|
+
texts.append(str(msg_obj_text))
|
|
74
|
+
elif msg_obj_content:
|
|
75
|
+
texts.append(str(msg_obj_content))
|
|
76
|
+
return " ".join(texts)
|
|
77
|
+
|
|
78
|
+
# Try to get text attribute
|
|
79
|
+
if hasattr(messages, "text"):
|
|
80
|
+
return str(messages.text)
|
|
81
|
+
if hasattr(messages, "content"):
|
|
82
|
+
content_attr: Any = messages.content
|
|
83
|
+
if isinstance(content_attr, str):
|
|
84
|
+
return content_attr
|
|
85
|
+
return str(content_attr) if content_attr else ""
|
|
86
|
+
|
|
87
|
+
return str(messages) if messages else ""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def user_message(text: str) -> dict[str, str]:
|
|
91
|
+
"""Create a user message object.
|
|
92
|
+
|
|
93
|
+
This is equivalent to the .NET UserMessage() function.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
text: The text content of the message
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
A message dictionary with role 'user'
|
|
100
|
+
|
|
101
|
+
Examples:
|
|
102
|
+
.. code-block:: python
|
|
103
|
+
|
|
104
|
+
user_message("Hello")
|
|
105
|
+
# Returns: {'role': 'user', 'content': 'Hello'}
|
|
106
|
+
"""
|
|
107
|
+
return {"role": "user", "content": str(text) if text else ""}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def assistant_message(text: str) -> dict[str, str]:
|
|
111
|
+
"""Create an assistant message object.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
text: The text content of the message
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
A message dictionary with role 'assistant'
|
|
118
|
+
|
|
119
|
+
Examples:
|
|
120
|
+
.. code-block:: python
|
|
121
|
+
|
|
122
|
+
assistant_message("Hello")
|
|
123
|
+
# Returns: {'role': 'assistant', 'content': 'Hello'}
|
|
124
|
+
"""
|
|
125
|
+
return {"role": "assistant", "content": str(text) if text else ""}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def agent_message(text: str) -> dict[str, str]:
|
|
129
|
+
"""Create an agent/assistant message object.
|
|
130
|
+
|
|
131
|
+
This is equivalent to the .NET AgentMessage() function.
|
|
132
|
+
It's an alias for assistant_message() for .NET compatibility.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
text: The text content of the message
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
A message dictionary with role 'assistant'
|
|
139
|
+
|
|
140
|
+
Examples:
|
|
141
|
+
.. code-block:: python
|
|
142
|
+
|
|
143
|
+
agent_message("Hello")
|
|
144
|
+
# Returns: {'role': 'assistant', 'content': 'Hello'}
|
|
145
|
+
"""
|
|
146
|
+
return {"role": "assistant", "content": str(text) if text else ""}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def system_message(text: str) -> dict[str, str]:
|
|
150
|
+
"""Create a system message object.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
text: The text content of the message
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
A message dictionary with role 'system'
|
|
157
|
+
|
|
158
|
+
Examples:
|
|
159
|
+
.. code-block:: python
|
|
160
|
+
|
|
161
|
+
system_message("You are a helpful assistant")
|
|
162
|
+
# Returns: {'role': 'system', 'content': 'You are a helpful assistant'}
|
|
163
|
+
"""
|
|
164
|
+
return {"role": "system", "content": str(text) if text else ""}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def if_func(condition: Any, true_value: Any, false_value: Any = None) -> Any:
|
|
168
|
+
"""Conditional expression - returns one value or another based on a condition.
|
|
169
|
+
|
|
170
|
+
This is equivalent to the PowerFx If() function.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
condition: The condition to evaluate (truthy/falsy)
|
|
174
|
+
true_value: Value to return if condition is truthy
|
|
175
|
+
false_value: Value to return if condition is falsy (defaults to None)
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
true_value if condition is truthy, otherwise false_value
|
|
179
|
+
"""
|
|
180
|
+
return true_value if condition else false_value
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def is_blank(value: Any) -> bool:
|
|
184
|
+
"""Check if a value is blank (None, empty string, empty list, etc.).
|
|
185
|
+
|
|
186
|
+
This is equivalent to the PowerFx IsBlank() function.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
value: The value to check
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
True if the value is considered blank
|
|
193
|
+
"""
|
|
194
|
+
if value is None:
|
|
195
|
+
return True
|
|
196
|
+
if isinstance(value, str) and not value.strip():
|
|
197
|
+
return True
|
|
198
|
+
if isinstance(value, (list, dict)):
|
|
199
|
+
return len(value) == 0 # type: ignore[reportUnknownArgumentType]
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def or_func(*args: Any) -> bool:
|
|
204
|
+
"""Logical OR - returns True if any argument is truthy.
|
|
205
|
+
|
|
206
|
+
This is equivalent to the PowerFx Or() function.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
*args: Variable number of values to check
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
True if any argument is truthy
|
|
213
|
+
"""
|
|
214
|
+
return any(bool(arg) for arg in args)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def and_func(*args: Any) -> bool:
|
|
218
|
+
"""Logical AND - returns True if all arguments are truthy.
|
|
219
|
+
|
|
220
|
+
This is equivalent to the PowerFx And() function.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
*args: Variable number of values to check
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
True if all arguments are truthy
|
|
227
|
+
"""
|
|
228
|
+
return all(bool(arg) for arg in args)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def not_func(value: Any) -> bool:
|
|
232
|
+
"""Logical NOT - returns the opposite boolean value.
|
|
233
|
+
|
|
234
|
+
This is equivalent to the PowerFx Not() function.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
value: The value to negate
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
True if value is falsy, False if truthy
|
|
241
|
+
"""
|
|
242
|
+
return not bool(value)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def count_rows(table: Any) -> int:
|
|
246
|
+
"""Count the number of rows/items in a table/list.
|
|
247
|
+
|
|
248
|
+
This is equivalent to the PowerFx CountRows() function.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
table: A list or table-like object
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
The number of rows/items
|
|
255
|
+
"""
|
|
256
|
+
if table is None:
|
|
257
|
+
return 0
|
|
258
|
+
if isinstance(table, (list, tuple)):
|
|
259
|
+
return len(cast(list[Any], table))
|
|
260
|
+
if isinstance(table, dict):
|
|
261
|
+
return len(cast(dict[str, Any], table))
|
|
262
|
+
return 0
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def first(table: Any) -> Any:
|
|
266
|
+
"""Get the first item from a table/list.
|
|
267
|
+
|
|
268
|
+
This is equivalent to the PowerFx First() function.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
table: A list or table-like object
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
The first item, or None if empty
|
|
275
|
+
"""
|
|
276
|
+
if table is None:
|
|
277
|
+
return None
|
|
278
|
+
if isinstance(table, (list, tuple)):
|
|
279
|
+
table_list = cast(list[Any], table)
|
|
280
|
+
if len(table_list) > 0:
|
|
281
|
+
return table_list[0]
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def last(table: Any) -> Any:
|
|
286
|
+
"""Get the last item from a table/list.
|
|
287
|
+
|
|
288
|
+
This is equivalent to the PowerFx Last() function.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
table: A list or table-like object
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
The last item, or None if empty
|
|
295
|
+
"""
|
|
296
|
+
if table is None:
|
|
297
|
+
return None
|
|
298
|
+
if isinstance(table, (list, tuple)):
|
|
299
|
+
table_list = cast(list[Any], table)
|
|
300
|
+
if len(table_list) > 0:
|
|
301
|
+
return table_list[-1]
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def find(substring: str | None, text: str | None) -> int | None:
|
|
306
|
+
"""Find the position of a substring within text.
|
|
307
|
+
|
|
308
|
+
This is equivalent to the PowerFx Find() function.
|
|
309
|
+
Returns None (Blank) if not found, otherwise 1-based index.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
substring: The substring to find
|
|
313
|
+
text: The text to search in
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
1-based index if found, None (Blank) if not found
|
|
317
|
+
"""
|
|
318
|
+
if substring is None or text is None:
|
|
319
|
+
return None
|
|
320
|
+
try:
|
|
321
|
+
index = str(text).find(str(substring))
|
|
322
|
+
return index + 1 if index >= 0 else None
|
|
323
|
+
except (TypeError, ValueError):
|
|
324
|
+
return None
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def upper(text: str | None) -> str:
|
|
328
|
+
"""Convert text to uppercase.
|
|
329
|
+
|
|
330
|
+
This is equivalent to the PowerFx Upper() function.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
text: The text to convert
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
Uppercase text
|
|
337
|
+
"""
|
|
338
|
+
if text is None:
|
|
339
|
+
return ""
|
|
340
|
+
return str(text).upper()
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def lower(text: str | None) -> str:
|
|
344
|
+
"""Convert text to lowercase.
|
|
345
|
+
|
|
346
|
+
This is equivalent to the PowerFx Lower() function.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
text: The text to convert
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
Lowercase text
|
|
353
|
+
"""
|
|
354
|
+
if text is None:
|
|
355
|
+
return ""
|
|
356
|
+
return str(text).lower()
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def concat_strings(*args: Any) -> str:
|
|
360
|
+
"""Concatenate multiple string arguments.
|
|
361
|
+
|
|
362
|
+
This is equivalent to the PowerFx Concat() function for string concatenation.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
*args: Variable number of values to concatenate
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
Concatenated string
|
|
369
|
+
"""
|
|
370
|
+
return "".join(str(arg) if arg is not None else "" for arg in args)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def concat_text(table: Any, field: str | None = None, separator: str = "") -> str:
|
|
374
|
+
"""Concatenate values from a table/list.
|
|
375
|
+
|
|
376
|
+
This is equivalent to the PowerFx Concat() function.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
table: A list of items
|
|
380
|
+
field: Optional field name to extract from each item
|
|
381
|
+
separator: Separator between values
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
Concatenated string
|
|
385
|
+
"""
|
|
386
|
+
if table is None:
|
|
387
|
+
return ""
|
|
388
|
+
if not isinstance(table, (list, tuple)):
|
|
389
|
+
return str(table)
|
|
390
|
+
|
|
391
|
+
values: list[str] = []
|
|
392
|
+
for item in cast(list[Any], table):
|
|
393
|
+
value: Any = None
|
|
394
|
+
if field and isinstance(item, dict):
|
|
395
|
+
item_dict = cast(dict[str, Any], item)
|
|
396
|
+
value = item_dict.get(field, "")
|
|
397
|
+
elif field and hasattr(item, field):
|
|
398
|
+
value = getattr(item, field, "")
|
|
399
|
+
else:
|
|
400
|
+
value = item
|
|
401
|
+
values.append(str(value) if value is not None else "")
|
|
402
|
+
|
|
403
|
+
return separator.join(values)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def for_all(table: Any, expression: str, field_mapping: dict[str, str] | None = None) -> list[Any]:
|
|
407
|
+
"""Apply an expression to each row of a table.
|
|
408
|
+
|
|
409
|
+
This is equivalent to the PowerFx ForAll() function.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
table: A list of records
|
|
413
|
+
expression: A string expression that references item fields
|
|
414
|
+
field_mapping: Optional dict mapping placeholder names to field names
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
List of results from applying expression to each row
|
|
418
|
+
|
|
419
|
+
Note:
|
|
420
|
+
The expression can use field names directly from the record.
|
|
421
|
+
For example: ForAll(items, "$" & name & ": " & description)
|
|
422
|
+
"""
|
|
423
|
+
if table is None or not isinstance(table, (list, tuple)):
|
|
424
|
+
return []
|
|
425
|
+
|
|
426
|
+
results: list[Any] = []
|
|
427
|
+
for item in cast(list[Any], table):
|
|
428
|
+
# If item is a dict, we can directly substitute field values
|
|
429
|
+
if isinstance(item, dict):
|
|
430
|
+
item_dict = cast(dict[str, Any], item)
|
|
431
|
+
# The expression is typically already evaluated by the expression parser
|
|
432
|
+
# This function primarily handles table iteration
|
|
433
|
+
# Return the item itself for further processing
|
|
434
|
+
results.append(item_dict)
|
|
435
|
+
else:
|
|
436
|
+
results.append(item)
|
|
437
|
+
|
|
438
|
+
return results
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def search_table(table: Any, value: Any, column: str) -> list[Any]:
|
|
442
|
+
"""Search for rows in a table where a column matches a value.
|
|
443
|
+
|
|
444
|
+
This is equivalent to the PowerFx Search() function.
|
|
445
|
+
|
|
446
|
+
Args:
|
|
447
|
+
table: A list of records
|
|
448
|
+
value: The value to search for
|
|
449
|
+
column: The column name to search in
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
List of matching records
|
|
453
|
+
"""
|
|
454
|
+
if table is None or not isinstance(table, (list, tuple)):
|
|
455
|
+
return []
|
|
456
|
+
|
|
457
|
+
results: list[Any] = []
|
|
458
|
+
search_value = str(value).lower() if value else ""
|
|
459
|
+
|
|
460
|
+
for item in cast(list[Any], table):
|
|
461
|
+
item_value: Any = None
|
|
462
|
+
if isinstance(item, dict):
|
|
463
|
+
item_dict = cast(dict[str, Any], item)
|
|
464
|
+
item_value = item_dict.get(column, "")
|
|
465
|
+
elif hasattr(item, column):
|
|
466
|
+
item_value = getattr(item, column, "")
|
|
467
|
+
else:
|
|
468
|
+
continue
|
|
469
|
+
|
|
470
|
+
# Case-insensitive contains search
|
|
471
|
+
if search_value in str(item_value).lower():
|
|
472
|
+
results.append(item)
|
|
473
|
+
|
|
474
|
+
return results
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
# Registry of custom functions
|
|
478
|
+
CUSTOM_FUNCTIONS: dict[str, Any] = {
|
|
479
|
+
"MessageText": message_text,
|
|
480
|
+
"UserMessage": user_message,
|
|
481
|
+
"AssistantMessage": assistant_message,
|
|
482
|
+
"AgentMessage": agent_message, # .NET compatibility alias for AssistantMessage
|
|
483
|
+
"SystemMessage": system_message,
|
|
484
|
+
"If": if_func,
|
|
485
|
+
"IsBlank": is_blank,
|
|
486
|
+
"Or": or_func,
|
|
487
|
+
"And": and_func,
|
|
488
|
+
"Not": not_func,
|
|
489
|
+
"CountRows": count_rows,
|
|
490
|
+
"First": first,
|
|
491
|
+
"Last": last,
|
|
492
|
+
"Find": find,
|
|
493
|
+
"Upper": upper,
|
|
494
|
+
"Lower": lower,
|
|
495
|
+
"Concat": concat_strings,
|
|
496
|
+
"Search": search_table,
|
|
497
|
+
"ForAll": for_all,
|
|
498
|
+
}
|