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,650 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""WorkflowState manages PowerFx variables during declarative workflow execution.
|
|
4
|
+
|
|
5
|
+
This module provides state management for declarative workflows, handling:
|
|
6
|
+
- Workflow inputs (read-only)
|
|
7
|
+
- Turn-scoped variables
|
|
8
|
+
- Workflow outputs
|
|
9
|
+
- Agent results and context
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import uuid
|
|
16
|
+
from collections.abc import Mapping
|
|
17
|
+
from typing import Any, cast
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from powerfx import Engine
|
|
21
|
+
|
|
22
|
+
_powerfx_engine: Engine | None = Engine()
|
|
23
|
+
except (ImportError, RuntimeError):
|
|
24
|
+
# ImportError: powerfx package not installed
|
|
25
|
+
# RuntimeError: .NET runtime not available or misconfigured
|
|
26
|
+
_powerfx_engine = None
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger("agent_framework.declarative")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class WorkflowState:
|
|
32
|
+
"""Manages variables and state during declarative workflow execution.
|
|
33
|
+
|
|
34
|
+
WorkflowState provides a unified interface for:
|
|
35
|
+
|
|
36
|
+
- Reading workflow inputs (immutable after initialization)
|
|
37
|
+
- Managing Local-scoped variables that persist across actions
|
|
38
|
+
- Storing agent results and making them available to subsequent actions
|
|
39
|
+
- Evaluating PowerFx expressions with the current state as context
|
|
40
|
+
|
|
41
|
+
The state is organized into namespaces that mirror the .NET implementation:
|
|
42
|
+
|
|
43
|
+
- Workflow.Inputs: Initial inputs to the workflow
|
|
44
|
+
- Workflow.Outputs: Values to be returned from the workflow
|
|
45
|
+
- Local: Variables that persist within the current workflow turn
|
|
46
|
+
- System: System-level variables (ConversationId, LastMessage, etc.)
|
|
47
|
+
- Agent: Results from the most recent agent invocation
|
|
48
|
+
- Conversation: Conversation history and messages
|
|
49
|
+
|
|
50
|
+
Examples:
|
|
51
|
+
.. code-block:: python
|
|
52
|
+
|
|
53
|
+
from agent_framework_declarative import WorkflowState
|
|
54
|
+
|
|
55
|
+
# Initialize with inputs
|
|
56
|
+
state = WorkflowState(inputs={"query": "Hello", "user_id": "123"})
|
|
57
|
+
|
|
58
|
+
# Access inputs (read-only)
|
|
59
|
+
query = state.get("Workflow.Inputs.query") # "Hello"
|
|
60
|
+
|
|
61
|
+
# Set Local-scoped variables
|
|
62
|
+
state.set("Local.results", [])
|
|
63
|
+
state.append("Local.results", "item1")
|
|
64
|
+
state.append("Local.results", "item2")
|
|
65
|
+
|
|
66
|
+
# Set workflow outputs
|
|
67
|
+
state.set("Workflow.Outputs.response", "Completed")
|
|
68
|
+
|
|
69
|
+
.. code-block:: python
|
|
70
|
+
|
|
71
|
+
from agent_framework_declarative import WorkflowState
|
|
72
|
+
|
|
73
|
+
# PowerFx expression evaluation
|
|
74
|
+
state = WorkflowState(inputs={"name": "World"})
|
|
75
|
+
result = state.eval("=Concat('Hello ', Workflow.Inputs.name)")
|
|
76
|
+
# result: "Hello World"
|
|
77
|
+
|
|
78
|
+
# Non-PowerFx strings are returned as-is
|
|
79
|
+
plain = state.eval("Hello World")
|
|
80
|
+
# plain: "Hello World"
|
|
81
|
+
|
|
82
|
+
.. code-block:: python
|
|
83
|
+
|
|
84
|
+
from agent_framework_declarative import WorkflowState
|
|
85
|
+
|
|
86
|
+
# Working with agent results
|
|
87
|
+
state = WorkflowState()
|
|
88
|
+
state.set_agent_result(
|
|
89
|
+
text="The answer is 42.",
|
|
90
|
+
messages=[],
|
|
91
|
+
tool_calls=[],
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Access agent result in subsequent actions
|
|
95
|
+
response = state.get("Agent.text") # "The answer is 42."
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__(
|
|
99
|
+
self,
|
|
100
|
+
inputs: Mapping[str, Any] | None = None,
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Initialize workflow state with optional inputs.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
inputs: Initial inputs to the workflow. These become available
|
|
106
|
+
as Workflow.Inputs.* and are immutable after initialization.
|
|
107
|
+
"""
|
|
108
|
+
self._inputs: dict[str, Any] = dict(inputs) if inputs else {}
|
|
109
|
+
self._local: dict[str, Any] = {}
|
|
110
|
+
self._outputs: dict[str, Any] = {}
|
|
111
|
+
conversation_id = str(uuid.uuid4())
|
|
112
|
+
self._system: dict[str, Any] = {
|
|
113
|
+
"ConversationId": conversation_id,
|
|
114
|
+
"LastMessage": {"Text": "", "Id": ""},
|
|
115
|
+
"LastMessageText": "",
|
|
116
|
+
"LastMessageId": "",
|
|
117
|
+
"conversations": {
|
|
118
|
+
conversation_id: {"id": conversation_id, "messages": []},
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
self._agent: dict[str, Any] = {}
|
|
122
|
+
self._conversation: dict[str, Any] = {
|
|
123
|
+
"messages": [],
|
|
124
|
+
"history": [],
|
|
125
|
+
}
|
|
126
|
+
self._custom: dict[str, Any] = {}
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def inputs(self) -> Mapping[str, Any]:
|
|
130
|
+
"""Get the workflow inputs (read-only)."""
|
|
131
|
+
return self._inputs
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def outputs(self) -> dict[str, Any]:
|
|
135
|
+
"""Get the workflow outputs."""
|
|
136
|
+
return self._outputs
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def local(self) -> dict[str, Any]:
|
|
140
|
+
"""Get the Local-scoped variables."""
|
|
141
|
+
return self._local
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def system(self) -> dict[str, Any]:
|
|
145
|
+
"""Get the System-scoped variables."""
|
|
146
|
+
return self._system
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def agent(self) -> dict[str, Any]:
|
|
150
|
+
"""Get the most recent agent result."""
|
|
151
|
+
return self._agent
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def conversation(self) -> dict[str, Any]:
|
|
155
|
+
"""Get the conversation state."""
|
|
156
|
+
return self._conversation
|
|
157
|
+
|
|
158
|
+
def get(self, path: str, default: Any = None) -> Any:
|
|
159
|
+
"""Get a value from the state using a dot-notated path.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
path: Dot-notated path like 'Local.results' or 'Workflow.Inputs.query'
|
|
163
|
+
default: Default value if path doesn't exist
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
The value at the path, or default if not found
|
|
167
|
+
"""
|
|
168
|
+
parts = path.split(".")
|
|
169
|
+
if not parts:
|
|
170
|
+
return default
|
|
171
|
+
|
|
172
|
+
namespace = parts[0]
|
|
173
|
+
remaining = parts[1:]
|
|
174
|
+
|
|
175
|
+
# Handle Workflow.Inputs and Workflow.Outputs specially
|
|
176
|
+
if namespace == "Workflow" and remaining:
|
|
177
|
+
sub_namespace = remaining[0]
|
|
178
|
+
remaining = remaining[1:]
|
|
179
|
+
if sub_namespace == "Inputs":
|
|
180
|
+
obj: Any = self._inputs
|
|
181
|
+
elif sub_namespace == "Outputs":
|
|
182
|
+
obj = self._outputs
|
|
183
|
+
else:
|
|
184
|
+
return default
|
|
185
|
+
elif namespace == "Local":
|
|
186
|
+
obj = self._local
|
|
187
|
+
elif namespace == "System":
|
|
188
|
+
obj = self._system
|
|
189
|
+
elif namespace == "Agent":
|
|
190
|
+
obj = self._agent
|
|
191
|
+
elif namespace == "Conversation":
|
|
192
|
+
obj = self._conversation
|
|
193
|
+
else:
|
|
194
|
+
# Try custom namespace
|
|
195
|
+
obj = self._custom.get(namespace, default)
|
|
196
|
+
if obj is default:
|
|
197
|
+
return default
|
|
198
|
+
|
|
199
|
+
# Navigate the remaining path
|
|
200
|
+
for part in remaining:
|
|
201
|
+
if isinstance(obj, dict):
|
|
202
|
+
obj_dict: dict[str, Any] = cast(dict[str, Any], obj)
|
|
203
|
+
obj = obj_dict.get(part, default)
|
|
204
|
+
if obj is default:
|
|
205
|
+
return default
|
|
206
|
+
elif hasattr(obj, part):
|
|
207
|
+
obj = getattr(obj, part)
|
|
208
|
+
else:
|
|
209
|
+
return default
|
|
210
|
+
|
|
211
|
+
return obj
|
|
212
|
+
|
|
213
|
+
def set(self, path: str, value: Any) -> None:
|
|
214
|
+
"""Set a value in the state using a dot-notated path.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
path: Dot-notated path like 'Local.results' or 'Workflow.Outputs.response'
|
|
218
|
+
value: The value to set
|
|
219
|
+
|
|
220
|
+
Raises:
|
|
221
|
+
ValueError: If attempting to set Workflow.Inputs (which is read-only)
|
|
222
|
+
"""
|
|
223
|
+
parts = path.split(".")
|
|
224
|
+
if not parts:
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
namespace = parts[0]
|
|
228
|
+
remaining = parts[1:]
|
|
229
|
+
|
|
230
|
+
# Handle Workflow.Inputs and Workflow.Outputs specially
|
|
231
|
+
if namespace == "Workflow":
|
|
232
|
+
if not remaining:
|
|
233
|
+
raise ValueError("Cannot set 'Workflow' directly; use 'Workflow.Outputs.*'")
|
|
234
|
+
sub_namespace = remaining[0]
|
|
235
|
+
remaining = remaining[1:]
|
|
236
|
+
if sub_namespace == "Inputs":
|
|
237
|
+
raise ValueError("Cannot modify Workflow.Inputs - they are read-only")
|
|
238
|
+
if sub_namespace == "Outputs":
|
|
239
|
+
target = self._outputs
|
|
240
|
+
else:
|
|
241
|
+
raise ValueError(f"Unknown Workflow namespace: {sub_namespace}")
|
|
242
|
+
elif namespace == "Local":
|
|
243
|
+
target = self._local
|
|
244
|
+
elif namespace == "System":
|
|
245
|
+
target = self._system
|
|
246
|
+
elif namespace == "Agent":
|
|
247
|
+
target = self._agent
|
|
248
|
+
elif namespace == "Conversation":
|
|
249
|
+
target = self._conversation
|
|
250
|
+
else:
|
|
251
|
+
# Create or use custom namespace
|
|
252
|
+
if namespace not in self._custom:
|
|
253
|
+
self._custom[namespace] = {}
|
|
254
|
+
target = self._custom[namespace]
|
|
255
|
+
|
|
256
|
+
# Navigate to the parent and set the value
|
|
257
|
+
if not remaining:
|
|
258
|
+
# Setting the namespace root itself - this shouldn't happen normally
|
|
259
|
+
raise ValueError(f"Cannot replace entire namespace '{namespace}'")
|
|
260
|
+
|
|
261
|
+
# Navigate to parent, creating dicts as needed
|
|
262
|
+
for part in remaining[:-1]:
|
|
263
|
+
if part not in target:
|
|
264
|
+
target[part] = {}
|
|
265
|
+
target = target[part]
|
|
266
|
+
|
|
267
|
+
# Set the final value
|
|
268
|
+
target[remaining[-1]] = value
|
|
269
|
+
|
|
270
|
+
def append(self, path: str, value: Any) -> None:
|
|
271
|
+
"""Append a value to a list at the specified path.
|
|
272
|
+
|
|
273
|
+
If the path doesn't exist, creates a new list with the value.
|
|
274
|
+
If the path exists but isn't a list, raises ValueError.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
path: Dot-notated path to a list
|
|
278
|
+
value: The value to append
|
|
279
|
+
|
|
280
|
+
Raises:
|
|
281
|
+
ValueError: If the existing value is not a list
|
|
282
|
+
"""
|
|
283
|
+
existing = self.get(path)
|
|
284
|
+
if existing is None:
|
|
285
|
+
self.set(path, [value])
|
|
286
|
+
elif isinstance(existing, list):
|
|
287
|
+
existing_list = cast(list[Any], existing)
|
|
288
|
+
existing_list.append(value)
|
|
289
|
+
self.set(path, existing_list)
|
|
290
|
+
else:
|
|
291
|
+
raise ValueError(f"Cannot append to non-list at path '{path}'")
|
|
292
|
+
|
|
293
|
+
def set_agent_result(
|
|
294
|
+
self,
|
|
295
|
+
text: str | None = None,
|
|
296
|
+
messages: list[Any] | None = None,
|
|
297
|
+
tool_calls: list[Any] | None = None,
|
|
298
|
+
**kwargs: Any,
|
|
299
|
+
) -> None:
|
|
300
|
+
"""Set the result from the most recent agent invocation.
|
|
301
|
+
|
|
302
|
+
This updates the 'agent' namespace with the agent's response,
|
|
303
|
+
making it available to subsequent actions via agent.text, agent.messages, etc.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
text: The text content of the agent's response
|
|
307
|
+
messages: The messages from the agent
|
|
308
|
+
tool_calls: Any tool calls made by the agent
|
|
309
|
+
**kwargs: Additional result data
|
|
310
|
+
"""
|
|
311
|
+
self._agent = {
|
|
312
|
+
"text": text,
|
|
313
|
+
"messages": messages or [],
|
|
314
|
+
"toolCalls": tool_calls or [],
|
|
315
|
+
**kwargs,
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
def add_conversation_message(self, message: Any) -> None:
|
|
319
|
+
"""Add a message to the conversation history.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
message: The message to add (typically a Message or similar)
|
|
323
|
+
"""
|
|
324
|
+
self._conversation["messages"].append(message)
|
|
325
|
+
self._conversation["history"].append(message)
|
|
326
|
+
|
|
327
|
+
def to_powerfx_symbols(self) -> dict[str, Any]:
|
|
328
|
+
"""Convert the current state to a PowerFx symbols dictionary.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
A dictionary suitable for passing to PowerFx Engine.eval()
|
|
332
|
+
"""
|
|
333
|
+
symbols = {
|
|
334
|
+
"Workflow": {
|
|
335
|
+
"Inputs": dict(self._inputs),
|
|
336
|
+
"Outputs": dict(self._outputs),
|
|
337
|
+
},
|
|
338
|
+
"Local": dict(self._local),
|
|
339
|
+
"System": dict(self._system),
|
|
340
|
+
"Agent": dict(self._agent),
|
|
341
|
+
"Conversation": dict(self._conversation),
|
|
342
|
+
# Also expose inputs at top level for backward compatibility with =inputs.X syntax
|
|
343
|
+
"inputs": dict(self._inputs),
|
|
344
|
+
**self._custom,
|
|
345
|
+
}
|
|
346
|
+
# Debug log the Local symbols to help diagnose type issues
|
|
347
|
+
if self._local:
|
|
348
|
+
for key, value in self._local.items():
|
|
349
|
+
logger.debug(
|
|
350
|
+
f"PowerFx symbol Local.{key}: type={type(value).__name__}, "
|
|
351
|
+
f"value_preview={str(value)[:100] if value else None}"
|
|
352
|
+
)
|
|
353
|
+
return symbols
|
|
354
|
+
|
|
355
|
+
def eval(self, expression: str) -> Any:
|
|
356
|
+
"""Evaluate a PowerFx expression with the current state.
|
|
357
|
+
|
|
358
|
+
Expressions starting with '=' are evaluated as PowerFx.
|
|
359
|
+
Other strings are returned as-is (after variable interpolation if applicable).
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
expression: The expression to evaluate
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
The evaluated result, or the original expression if not a PowerFx expression
|
|
366
|
+
"""
|
|
367
|
+
if not expression:
|
|
368
|
+
return expression
|
|
369
|
+
|
|
370
|
+
if not expression.startswith("="):
|
|
371
|
+
return expression
|
|
372
|
+
|
|
373
|
+
# Strip the leading '=' for evaluation
|
|
374
|
+
formula = expression[1:]
|
|
375
|
+
|
|
376
|
+
if _powerfx_engine is not None:
|
|
377
|
+
# Try PowerFx evaluation first
|
|
378
|
+
try:
|
|
379
|
+
symbols = self.to_powerfx_symbols()
|
|
380
|
+
return _powerfx_engine.eval(formula, symbols=symbols)
|
|
381
|
+
except Exception as exc:
|
|
382
|
+
logger.warning(f"PowerFx evaluation failed for '{expression[:50]}': {exc}")
|
|
383
|
+
# Fall through to simple evaluation
|
|
384
|
+
|
|
385
|
+
# Fallback: Simple expression evaluation using custom functions
|
|
386
|
+
return self._eval_simple(formula)
|
|
387
|
+
|
|
388
|
+
def _eval_simple(self, formula: str) -> Any:
|
|
389
|
+
"""Simple expression evaluation when PowerFx is not available.
|
|
390
|
+
|
|
391
|
+
Supports:
|
|
392
|
+
- Variable references: Local.X, System.X, Workflow.Inputs.X
|
|
393
|
+
- Simple function calls: IsBlank(x), Find(a, b), etc.
|
|
394
|
+
- Simple comparisons: x < 4, x = "value"
|
|
395
|
+
- Logical operators: And, Or, Not, ||, !
|
|
396
|
+
- Negation: !expression
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
formula: The formula to evaluate (without leading '=')
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
The evaluated result
|
|
403
|
+
"""
|
|
404
|
+
from ._powerfx_functions import CUSTOM_FUNCTIONS
|
|
405
|
+
|
|
406
|
+
formula = formula.strip()
|
|
407
|
+
|
|
408
|
+
# Handle negation prefix
|
|
409
|
+
if formula.startswith("!"):
|
|
410
|
+
inner = formula[1:].strip()
|
|
411
|
+
result = self._eval_simple(inner)
|
|
412
|
+
return not bool(result)
|
|
413
|
+
|
|
414
|
+
# Handle Not() function
|
|
415
|
+
if formula.startswith("Not(") and formula.endswith(")"):
|
|
416
|
+
inner = formula[4:-1].strip()
|
|
417
|
+
result = self._eval_simple(inner)
|
|
418
|
+
return not bool(result)
|
|
419
|
+
|
|
420
|
+
# Handle function calls
|
|
421
|
+
for func_name, func in CUSTOM_FUNCTIONS.items():
|
|
422
|
+
if formula.startswith(f"{func_name}(") and formula.endswith(")"):
|
|
423
|
+
args_str = formula[len(func_name) + 1 : -1]
|
|
424
|
+
# Simple argument parsing (doesn't handle nested calls well)
|
|
425
|
+
args = self._parse_function_args(args_str)
|
|
426
|
+
evaluated_args = [self._eval_simple(arg) if isinstance(arg, str) else arg for arg in args]
|
|
427
|
+
try:
|
|
428
|
+
return func(*evaluated_args)
|
|
429
|
+
except Exception as e:
|
|
430
|
+
logger.warning(f"Function {func_name} failed: {e}")
|
|
431
|
+
return formula
|
|
432
|
+
|
|
433
|
+
# Handle And operator
|
|
434
|
+
if " And " in formula:
|
|
435
|
+
parts = formula.split(" And ", 1)
|
|
436
|
+
left = self._eval_simple(parts[0])
|
|
437
|
+
right = self._eval_simple(parts[1])
|
|
438
|
+
return bool(left) and bool(right)
|
|
439
|
+
|
|
440
|
+
# Handle Or operator (||)
|
|
441
|
+
if " || " in formula or " Or " in formula:
|
|
442
|
+
parts = formula.split(" || ", 1) if " || " in formula else formula.split(" Or ", 1)
|
|
443
|
+
left = self._eval_simple(parts[0])
|
|
444
|
+
right = self._eval_simple(parts[1])
|
|
445
|
+
return bool(left) or bool(right)
|
|
446
|
+
|
|
447
|
+
# Handle comparison operators
|
|
448
|
+
for op in [" < ", " > ", " <= ", " >= ", " <> ", " = "]:
|
|
449
|
+
if op in formula:
|
|
450
|
+
parts = formula.split(op, 1)
|
|
451
|
+
left = self._eval_simple(parts[0].strip())
|
|
452
|
+
right = self._eval_simple(parts[1].strip())
|
|
453
|
+
if op == " < ":
|
|
454
|
+
return left < right
|
|
455
|
+
if op == " > ":
|
|
456
|
+
return left > right
|
|
457
|
+
if op == " <= ":
|
|
458
|
+
return left <= right
|
|
459
|
+
if op == " >= ":
|
|
460
|
+
return left >= right
|
|
461
|
+
if op == " <> ":
|
|
462
|
+
return left != right
|
|
463
|
+
if op == " = ":
|
|
464
|
+
return left == right
|
|
465
|
+
|
|
466
|
+
# Handle arithmetic operators
|
|
467
|
+
if " + " in formula:
|
|
468
|
+
parts = formula.split(" + ", 1)
|
|
469
|
+
left = self._eval_simple(parts[0].strip())
|
|
470
|
+
right = self._eval_simple(parts[1].strip())
|
|
471
|
+
# Treat None as 0 for arithmetic (PowerFx behavior)
|
|
472
|
+
if left is None:
|
|
473
|
+
left = 0
|
|
474
|
+
if right is None:
|
|
475
|
+
right = 0
|
|
476
|
+
# Try numeric addition first, fall back to string concat
|
|
477
|
+
try:
|
|
478
|
+
return float(left) + float(right)
|
|
479
|
+
except (ValueError, TypeError):
|
|
480
|
+
return str(left) + str(right)
|
|
481
|
+
|
|
482
|
+
if " - " in formula:
|
|
483
|
+
parts = formula.split(" - ", 1)
|
|
484
|
+
left = self._eval_simple(parts[0].strip())
|
|
485
|
+
right = self._eval_simple(parts[1].strip())
|
|
486
|
+
# Treat None as 0 for arithmetic (PowerFx behavior)
|
|
487
|
+
if left is None:
|
|
488
|
+
left = 0
|
|
489
|
+
if right is None:
|
|
490
|
+
right = 0
|
|
491
|
+
try:
|
|
492
|
+
return float(left) - float(right)
|
|
493
|
+
except (ValueError, TypeError):
|
|
494
|
+
return formula
|
|
495
|
+
|
|
496
|
+
# Handle multiplication
|
|
497
|
+
if " * " in formula:
|
|
498
|
+
parts = formula.split(" * ", 1)
|
|
499
|
+
left = self._eval_simple(parts[0].strip())
|
|
500
|
+
right = self._eval_simple(parts[1].strip())
|
|
501
|
+
# Treat None as 0 for arithmetic (PowerFx behavior)
|
|
502
|
+
if left is None:
|
|
503
|
+
left = 0
|
|
504
|
+
if right is None:
|
|
505
|
+
right = 0
|
|
506
|
+
try:
|
|
507
|
+
return float(left) * float(right)
|
|
508
|
+
except (ValueError, TypeError):
|
|
509
|
+
return formula
|
|
510
|
+
|
|
511
|
+
# Handle division with div-by-zero protection
|
|
512
|
+
if " / " in formula:
|
|
513
|
+
parts = formula.split(" / ", 1)
|
|
514
|
+
left = self._eval_simple(parts[0].strip())
|
|
515
|
+
right = self._eval_simple(parts[1].strip())
|
|
516
|
+
# Treat None as 0 for arithmetic (PowerFx behavior)
|
|
517
|
+
if left is None:
|
|
518
|
+
left = 0
|
|
519
|
+
if right is None:
|
|
520
|
+
right = 0
|
|
521
|
+
try:
|
|
522
|
+
right_float = float(right)
|
|
523
|
+
if right_float == 0:
|
|
524
|
+
# PowerFx returns Error for division by zero; we return None (Blank)
|
|
525
|
+
logger.warning(f"Division by zero in expression: {formula}")
|
|
526
|
+
return None
|
|
527
|
+
return float(left) / right_float
|
|
528
|
+
except (ValueError, TypeError):
|
|
529
|
+
return formula
|
|
530
|
+
|
|
531
|
+
# Handle string literals
|
|
532
|
+
if (formula.startswith('"') and formula.endswith('"')) or (formula.startswith("'") and formula.endswith("'")):
|
|
533
|
+
return formula[1:-1]
|
|
534
|
+
|
|
535
|
+
# Handle numeric literals
|
|
536
|
+
try:
|
|
537
|
+
if "." in formula:
|
|
538
|
+
return float(formula)
|
|
539
|
+
return int(formula)
|
|
540
|
+
except ValueError:
|
|
541
|
+
pass
|
|
542
|
+
|
|
543
|
+
# Handle boolean literals
|
|
544
|
+
if formula.lower() == "true":
|
|
545
|
+
return True
|
|
546
|
+
if formula.lower() == "false":
|
|
547
|
+
return False
|
|
548
|
+
|
|
549
|
+
# Handle variable references
|
|
550
|
+
if "." in formula:
|
|
551
|
+
# For known namespaces, return None if not found (PowerFx semantics)
|
|
552
|
+
# rather than the formula string
|
|
553
|
+
if formula.startswith(("Local.", "Workflow.", "Agent.", "Conversation.", "System.")):
|
|
554
|
+
return self.get(formula)
|
|
555
|
+
not_found = object()
|
|
556
|
+
value = self.get(formula, default=not_found)
|
|
557
|
+
if value is not not_found:
|
|
558
|
+
return value
|
|
559
|
+
|
|
560
|
+
# Return the formula as-is if we can't evaluate it
|
|
561
|
+
return formula
|
|
562
|
+
|
|
563
|
+
def _parse_function_args(self, args_str: str) -> list[str]:
|
|
564
|
+
"""Parse function arguments, handling nested parentheses and strings.
|
|
565
|
+
|
|
566
|
+
Args:
|
|
567
|
+
args_str: The argument string (without outer parentheses)
|
|
568
|
+
|
|
569
|
+
Returns:
|
|
570
|
+
List of argument strings
|
|
571
|
+
"""
|
|
572
|
+
args: list[str] = []
|
|
573
|
+
current = ""
|
|
574
|
+
depth = 0
|
|
575
|
+
in_string = False
|
|
576
|
+
string_char = None
|
|
577
|
+
|
|
578
|
+
for char in args_str:
|
|
579
|
+
if char in ('"', "'") and not in_string:
|
|
580
|
+
in_string = True
|
|
581
|
+
string_char = char
|
|
582
|
+
current += char
|
|
583
|
+
elif char == string_char and in_string:
|
|
584
|
+
in_string = False
|
|
585
|
+
string_char = None
|
|
586
|
+
current += char
|
|
587
|
+
elif char == "(" and not in_string:
|
|
588
|
+
depth += 1
|
|
589
|
+
current += char
|
|
590
|
+
elif char == ")" and not in_string:
|
|
591
|
+
depth -= 1
|
|
592
|
+
current += char
|
|
593
|
+
elif char == "," and depth == 0 and not in_string:
|
|
594
|
+
args.append(current.strip())
|
|
595
|
+
current = ""
|
|
596
|
+
else:
|
|
597
|
+
current += char
|
|
598
|
+
|
|
599
|
+
if current.strip():
|
|
600
|
+
args.append(current.strip())
|
|
601
|
+
|
|
602
|
+
return args
|
|
603
|
+
|
|
604
|
+
def eval_if_expression(self, value: Any) -> Any:
|
|
605
|
+
"""Evaluate a value if it's a PowerFx expression, otherwise return as-is.
|
|
606
|
+
|
|
607
|
+
This is a convenience method that handles both expressions and literals.
|
|
608
|
+
|
|
609
|
+
Args:
|
|
610
|
+
value: A value that may or may not be a PowerFx expression
|
|
611
|
+
|
|
612
|
+
Returns:
|
|
613
|
+
The evaluated result if it's an expression, or the original value
|
|
614
|
+
"""
|
|
615
|
+
if isinstance(value, str):
|
|
616
|
+
return self.eval(value)
|
|
617
|
+
if isinstance(value, dict):
|
|
618
|
+
return {str(k): self.eval_if_expression(v) for k, v in value.items()} # type: ignore[reportUnknownVariableType]
|
|
619
|
+
if isinstance(value, list):
|
|
620
|
+
return [self.eval_if_expression(item) for item in value] # type: ignore[reportUnknownVariableType]
|
|
621
|
+
return value
|
|
622
|
+
|
|
623
|
+
def reset_local(self) -> None:
|
|
624
|
+
"""Reset Local-scoped variables for a new turn.
|
|
625
|
+
|
|
626
|
+
This clears the Local namespace while preserving other state.
|
|
627
|
+
"""
|
|
628
|
+
self._local.clear()
|
|
629
|
+
|
|
630
|
+
def reset_agent(self) -> None:
|
|
631
|
+
"""Reset the agent result for a new agent invocation."""
|
|
632
|
+
self._agent.clear()
|
|
633
|
+
|
|
634
|
+
def clone(self) -> WorkflowState:
|
|
635
|
+
"""Create a shallow copy of the state.
|
|
636
|
+
|
|
637
|
+
Returns:
|
|
638
|
+
A new WorkflowState with copied data
|
|
639
|
+
"""
|
|
640
|
+
import copy
|
|
641
|
+
|
|
642
|
+
new_state = WorkflowState()
|
|
643
|
+
new_state._inputs = copy.copy(self._inputs)
|
|
644
|
+
new_state._local = copy.copy(self._local)
|
|
645
|
+
new_state._system = copy.copy(self._system)
|
|
646
|
+
new_state._outputs = copy.copy(self._outputs)
|
|
647
|
+
new_state._agent = copy.copy(self._agent)
|
|
648
|
+
new_state._conversation = copy.copy(self._conversation)
|
|
649
|
+
new_state._custom = copy.copy(self._custom)
|
|
650
|
+
return new_state
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agent-framework-declarative
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Declarative specification support for Microsoft Agent Framework.
|
|
5
|
+
Author-email: Microsoft <af-support@microsoft.com>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: agent-framework-core>=1.9.0,<2
|
|
19
|
+
Requires-Dist: httpx>=0.27,<1
|
|
20
|
+
Requires-Dist: powerfx>=0.0.32,<0.0.35; python_version < '3.14'
|
|
21
|
+
Requires-Dist: pyyaml>=6.0,<7.0
|
|
22
|
+
Project-URL: homepage, https://aka.ms/agent-framework
|
|
23
|
+
Project-URL: issues, https://github.com/microsoft/agent-framework/issues
|
|
24
|
+
Project-URL: release_notes, https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true
|
|
25
|
+
Project-URL: source, https://github.com/microsoft/agent-framework/tree/main/python
|
|
26
|
+
|
|
27
|
+
# Get Started with Microsoft Agent Framework Declarative
|
|
28
|
+
|
|
29
|
+
Please install this package via pip:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install agent-framework-declarative
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Release stage
|
|
36
|
+
|
|
37
|
+
This package ships at two different stability levels:
|
|
38
|
+
|
|
39
|
+
- **Declarative workflows** (`WorkflowFactory`, executors, handlers, and the
|
|
40
|
+
`_workflows` surface) are **stable**.
|
|
41
|
+
- **Declarative agents** (`AgentFactory` and the YAML agent loading/parsing path:
|
|
42
|
+
`DeclarativeLoaderError`, `ProviderLookupError`, `ProviderTypeMapping`) are
|
|
43
|
+
**experimental** and may change or be removed in future versions without notice.
|
|
44
|
+
Using any of these symbols emits an `ExperimentalWarning` on first use.
|
|
45
|
+
|
|
46
|
+
## Declarative features
|
|
47
|
+
|
|
48
|
+
The declarative packages provides support for building agents based on a declarative yaml specification.
|
|
49
|
+
|