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.
@@ -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
+ }