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,808 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""WorkflowFactory creates executable Workflow objects from YAML definitions.
|
|
4
|
+
|
|
5
|
+
This module provides the main entry point for declarative workflow support,
|
|
6
|
+
parsing YAML workflow definitions and creating Workflow objects that can be
|
|
7
|
+
executed using the core workflow runtime.
|
|
8
|
+
|
|
9
|
+
Each YAML action becomes a real Executor node in the workflow graph,
|
|
10
|
+
enabling checkpointing, visualization, and pause/resume capabilities.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from collections.abc import Mapping
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, cast
|
|
19
|
+
|
|
20
|
+
import yaml
|
|
21
|
+
from agent_framework import (
|
|
22
|
+
AgentExecutor,
|
|
23
|
+
CheckpointStorage,
|
|
24
|
+
SupportsAgentRun,
|
|
25
|
+
Workflow,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
from .._loader import AgentFactory
|
|
29
|
+
from ._declarative_base import DeclarativeEnvConfig, discover_env_references
|
|
30
|
+
from ._declarative_builder import DeclarativeWorkflowBuilder
|
|
31
|
+
from ._errors import DeclarativeWorkflowError
|
|
32
|
+
from ._http_handler import HttpRequestHandler
|
|
33
|
+
from ._mcp_handler import MCPToolHandler
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger("agent_framework.declarative")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
__all__ = ["WorkflowFactory"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class WorkflowFactory:
|
|
42
|
+
"""Factory for creating executable Workflow objects from YAML definitions.
|
|
43
|
+
|
|
44
|
+
WorkflowFactory parses declarative workflow YAML files and creates
|
|
45
|
+
Workflow objects that can be executed using the core workflow runtime.
|
|
46
|
+
Each YAML action becomes a real Executor node in the workflow graph,
|
|
47
|
+
enabling checkpointing at action boundaries, visualization, and pause/resume.
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
.. code-block:: python
|
|
51
|
+
|
|
52
|
+
from agent_framework.declarative import WorkflowFactory
|
|
53
|
+
|
|
54
|
+
# Basic usage: create workflow from YAML file
|
|
55
|
+
factory = WorkflowFactory()
|
|
56
|
+
workflow = factory.create_workflow_from_yaml_path("workflow.yaml")
|
|
57
|
+
|
|
58
|
+
async for event in workflow.run({"query": "Hello"}, stream=True):
|
|
59
|
+
print(event)
|
|
60
|
+
|
|
61
|
+
.. code-block:: python
|
|
62
|
+
|
|
63
|
+
from agent_framework.declarative import WorkflowFactory
|
|
64
|
+
from agent_framework import FileCheckpointStorage
|
|
65
|
+
|
|
66
|
+
# With checkpointing for pause/resume support
|
|
67
|
+
storage = FileCheckpointStorage(path="./checkpoints")
|
|
68
|
+
factory = WorkflowFactory(checkpoint_storage=storage)
|
|
69
|
+
workflow = factory.create_workflow_from_yaml_path("workflow.yaml")
|
|
70
|
+
|
|
71
|
+
.. code-block:: python
|
|
72
|
+
|
|
73
|
+
from agent_framework.openai import OpenAIChatClient
|
|
74
|
+
from agent_framework.declarative import WorkflowFactory
|
|
75
|
+
|
|
76
|
+
# Pre-register agents for InvokeAzureAgent actions
|
|
77
|
+
client = OpenAIChatClient()
|
|
78
|
+
agent = client.as_agent(name="MyAgent", instructions="You are helpful.")
|
|
79
|
+
|
|
80
|
+
factory = WorkflowFactory(agents={"MyAgent": agent})
|
|
81
|
+
workflow = factory.create_workflow_from_yaml_path("workflow.yaml")
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
_agents: dict[str, SupportsAgentRun | AgentExecutor]
|
|
85
|
+
|
|
86
|
+
def __init__(
|
|
87
|
+
self,
|
|
88
|
+
*,
|
|
89
|
+
agent_factory: AgentFactory | None = None,
|
|
90
|
+
agents: Mapping[str, SupportsAgentRun | AgentExecutor] | None = None,
|
|
91
|
+
bindings: Mapping[str, Any] | None = None,
|
|
92
|
+
env_file: str | None = None,
|
|
93
|
+
checkpoint_storage: CheckpointStorage | None = None,
|
|
94
|
+
max_iterations: int | None = None,
|
|
95
|
+
http_request_handler: HttpRequestHandler | None = None,
|
|
96
|
+
mcp_tool_handler: MCPToolHandler | None = None,
|
|
97
|
+
configuration: Mapping[str, str] | None = None,
|
|
98
|
+
restrict_env_to_configuration: bool = True,
|
|
99
|
+
) -> None:
|
|
100
|
+
"""Initialize the workflow factory.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
agent_factory: Optional AgentFactory for creating agents from inline YAML definitions.
|
|
104
|
+
agents: Optional pre-created agents by name. These are looked up when processing
|
|
105
|
+
InvokeAzureAgent actions in the workflow YAML.
|
|
106
|
+
bindings: Optional function bindings for tool calls within workflow actions.
|
|
107
|
+
env_file: Optional path to .env file for environment variables used in agent creation.
|
|
108
|
+
checkpoint_storage: Optional checkpoint storage enabling pause/resume functionality.
|
|
109
|
+
max_iterations: Optional maximum runner supersteps. Overrides the YAML ``maxTurns``
|
|
110
|
+
field and the core default (100). Workflows with ``GotoAction`` loops (e.g.
|
|
111
|
+
DeepResearch) typically need a higher value.
|
|
112
|
+
http_request_handler: Optional handler used to dispatch HTTP requests for
|
|
113
|
+
``HttpRequestAction``. Required if the workflow contains any
|
|
114
|
+
``HttpRequestAction``; build will fail with :class:`DeclarativeWorkflowError`
|
|
115
|
+
otherwise. Use :class:`agent_framework.declarative.DefaultHttpRequestHandler`
|
|
116
|
+
for a no-policy ``httpx``-based default, or supply your own implementation
|
|
117
|
+
to enforce SSRF guards, allowlisting, or auth resolution.
|
|
118
|
+
mcp_tool_handler: Optional handler used to dispatch MCP tool calls for
|
|
119
|
+
``InvokeMcpTool``. Required if the workflow contains any
|
|
120
|
+
``InvokeMcpTool``; build will fail with :class:`DeclarativeWorkflowError`
|
|
121
|
+
otherwise. Use :class:`agent_framework.declarative.DefaultMCPToolHandler`
|
|
122
|
+
for a default backed by :class:`agent_framework.MCPStreamableHTTPTool`,
|
|
123
|
+
or supply your own implementation to enforce SSRF guards, allowlisting,
|
|
124
|
+
or auth/connection resolution.
|
|
125
|
+
configuration: Optional mapping that populates the PowerFx ``Env``
|
|
126
|
+
symbol referenced from workflow YAML expressions (e.g.
|
|
127
|
+
``=Env.MY_KEY``). Keys supplied here are always exposed
|
|
128
|
+
under ``Env.<key>``; the process ``os.environ`` is consulted
|
|
129
|
+
only when ``restrict_env_to_configuration`` is ``False``.
|
|
130
|
+
When neither source produces a value the ``Env`` symbol is
|
|
131
|
+
omitted so ``=Env.X`` evaluates to the literal expression
|
|
132
|
+
string.
|
|
133
|
+
restrict_env_to_configuration: When ``True`` (default), the
|
|
134
|
+
``Env`` PowerFx symbol is populated exclusively from
|
|
135
|
+
``configuration``; ``os.environ`` is never consulted. Set to
|
|
136
|
+
``False`` to additionally fall back to ``os.environ`` for
|
|
137
|
+
names absent from ``configuration`` that the workflow YAML
|
|
138
|
+
explicitly references. The fallback is constrained to names
|
|
139
|
+
discovered in PowerFx expressions inside the workflow
|
|
140
|
+
definition so unrelated environment variables never enter
|
|
141
|
+
the PowerFx scope.
|
|
142
|
+
|
|
143
|
+
Examples:
|
|
144
|
+
.. code-block:: python
|
|
145
|
+
|
|
146
|
+
from agent_framework.declarative import WorkflowFactory
|
|
147
|
+
|
|
148
|
+
# Minimal initialization
|
|
149
|
+
factory = WorkflowFactory()
|
|
150
|
+
|
|
151
|
+
.. code-block:: python
|
|
152
|
+
|
|
153
|
+
from agent_framework.openai import OpenAIChatClient
|
|
154
|
+
from agent_framework.declarative import WorkflowFactory
|
|
155
|
+
|
|
156
|
+
# With pre-registered agents
|
|
157
|
+
client = OpenAIChatClient()
|
|
158
|
+
agents = {
|
|
159
|
+
"WriterAgent": client.as_agent(name="Writer", instructions="Write content."),
|
|
160
|
+
"ReviewerAgent": client.as_agent(name="Reviewer", instructions="Review content."),
|
|
161
|
+
}
|
|
162
|
+
factory = WorkflowFactory(agents=agents)
|
|
163
|
+
|
|
164
|
+
.. code-block:: python
|
|
165
|
+
|
|
166
|
+
from agent_framework import FileCheckpointStorage
|
|
167
|
+
from agent_framework.declarative import WorkflowFactory
|
|
168
|
+
|
|
169
|
+
# With checkpoint storage for pause/resume
|
|
170
|
+
factory = WorkflowFactory(
|
|
171
|
+
checkpoint_storage=FileCheckpointStorage("./checkpoints"),
|
|
172
|
+
env_file=".env",
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
.. code-block:: python
|
|
176
|
+
|
|
177
|
+
from agent_framework.declarative import WorkflowFactory
|
|
178
|
+
|
|
179
|
+
# Inject named values for =Env.* references in the workflow YAML
|
|
180
|
+
factory = WorkflowFactory(
|
|
181
|
+
configuration={
|
|
182
|
+
"MY_SERVER_URL": "https://example.com",
|
|
183
|
+
"MY_TOOL_NAME": "search",
|
|
184
|
+
},
|
|
185
|
+
)
|
|
186
|
+
"""
|
|
187
|
+
self._agent_factory = agent_factory or AgentFactory(env_file_path=env_file)
|
|
188
|
+
self._agents: dict[str, SupportsAgentRun | AgentExecutor] = dict(agents) if agents else {}
|
|
189
|
+
self._bindings: dict[str, Any] = dict(bindings) if bindings else {}
|
|
190
|
+
self._tools: dict[str, Any] = {} # Tool registry for InvokeFunctionTool actions
|
|
191
|
+
self._checkpoint_storage = checkpoint_storage
|
|
192
|
+
self._max_iterations = max_iterations
|
|
193
|
+
self._http_request_handler = http_request_handler
|
|
194
|
+
self._mcp_tool_handler = mcp_tool_handler
|
|
195
|
+
self._configuration: dict[str, str] = dict(configuration) if configuration else {}
|
|
196
|
+
self._restrict_env_to_configuration = restrict_env_to_configuration
|
|
197
|
+
|
|
198
|
+
def create_workflow_from_yaml_path(
|
|
199
|
+
self,
|
|
200
|
+
yaml_path: str | Path,
|
|
201
|
+
) -> Workflow:
|
|
202
|
+
"""Create a Workflow from a YAML file path.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
yaml_path: Path to the YAML workflow definition file.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
An executable Workflow object with action nodes for each YAML action.
|
|
209
|
+
|
|
210
|
+
Raises:
|
|
211
|
+
DeclarativeWorkflowError: If the YAML is invalid or cannot be parsed.
|
|
212
|
+
FileNotFoundError: If the YAML file doesn't exist.
|
|
213
|
+
|
|
214
|
+
Examples:
|
|
215
|
+
.. code-block:: python
|
|
216
|
+
|
|
217
|
+
from agent_framework.declarative import WorkflowFactory
|
|
218
|
+
|
|
219
|
+
factory = WorkflowFactory()
|
|
220
|
+
workflow = factory.create_workflow_from_yaml_path("workflow.yaml")
|
|
221
|
+
|
|
222
|
+
# Execute the workflow
|
|
223
|
+
async for event in workflow.run({"input": "Hello"}, stream=True):
|
|
224
|
+
print(event)
|
|
225
|
+
|
|
226
|
+
.. code-block:: python
|
|
227
|
+
|
|
228
|
+
from pathlib import Path
|
|
229
|
+
from agent_framework.declarative import WorkflowFactory
|
|
230
|
+
|
|
231
|
+
# Using Path object
|
|
232
|
+
workflow_path = Path(__file__).parent / "workflows" / "customer_support.yaml"
|
|
233
|
+
factory = WorkflowFactory()
|
|
234
|
+
workflow = factory.create_workflow_from_yaml_path(workflow_path)
|
|
235
|
+
"""
|
|
236
|
+
if not isinstance(yaml_path, Path):
|
|
237
|
+
yaml_path = Path(yaml_path)
|
|
238
|
+
|
|
239
|
+
if not yaml_path.exists():
|
|
240
|
+
raise FileNotFoundError(f"Workflow YAML file not found: {yaml_path}")
|
|
241
|
+
|
|
242
|
+
with open(yaml_path) as f:
|
|
243
|
+
yaml_content = f.read()
|
|
244
|
+
|
|
245
|
+
return self.create_workflow_from_yaml(yaml_content, base_path=yaml_path.parent)
|
|
246
|
+
|
|
247
|
+
def create_workflow_from_yaml(
|
|
248
|
+
self,
|
|
249
|
+
yaml_content: str,
|
|
250
|
+
base_path: Path | None = None,
|
|
251
|
+
) -> Workflow:
|
|
252
|
+
"""Create a Workflow from a YAML string.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
yaml_content: The YAML workflow definition as a string.
|
|
256
|
+
base_path: Optional base path for resolving relative file references
|
|
257
|
+
in agent definitions.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
An executable Workflow object with action nodes for each YAML action.
|
|
261
|
+
|
|
262
|
+
Raises:
|
|
263
|
+
DeclarativeWorkflowError: If the YAML is invalid or cannot be parsed.
|
|
264
|
+
|
|
265
|
+
Examples:
|
|
266
|
+
.. code-block:: python
|
|
267
|
+
|
|
268
|
+
from agent_framework.declarative import WorkflowFactory
|
|
269
|
+
|
|
270
|
+
yaml_content = '''
|
|
271
|
+
kind: Workflow
|
|
272
|
+
trigger:
|
|
273
|
+
kind: OnConversationStart
|
|
274
|
+
id: greeting_workflow
|
|
275
|
+
actions:
|
|
276
|
+
- kind: SetVariable
|
|
277
|
+
id: set_greeting
|
|
278
|
+
variable: Local.Greeting
|
|
279
|
+
value: "Hello, World!"
|
|
280
|
+
- kind: SendActivity
|
|
281
|
+
id: send_greeting
|
|
282
|
+
activity: =Local.Greeting
|
|
283
|
+
'''
|
|
284
|
+
|
|
285
|
+
factory = WorkflowFactory()
|
|
286
|
+
workflow = factory.create_workflow_from_yaml(yaml_content)
|
|
287
|
+
|
|
288
|
+
.. code-block:: python
|
|
289
|
+
|
|
290
|
+
from pathlib import Path
|
|
291
|
+
from agent_framework.declarative import WorkflowFactory
|
|
292
|
+
|
|
293
|
+
# With base_path for resolving relative agent file references
|
|
294
|
+
yaml_content = '''
|
|
295
|
+
kind: Workflow
|
|
296
|
+
agents:
|
|
297
|
+
MyAgent:
|
|
298
|
+
file: ./agents/my_agent.yaml
|
|
299
|
+
trigger:
|
|
300
|
+
actions:
|
|
301
|
+
- kind: InvokeAzureAgent
|
|
302
|
+
agent:
|
|
303
|
+
name: MyAgent
|
|
304
|
+
'''
|
|
305
|
+
|
|
306
|
+
factory = WorkflowFactory()
|
|
307
|
+
workflow = factory.create_workflow_from_yaml(
|
|
308
|
+
yaml_content,
|
|
309
|
+
base_path=Path("./workflows"),
|
|
310
|
+
)
|
|
311
|
+
"""
|
|
312
|
+
try:
|
|
313
|
+
workflow_def = yaml.safe_load(yaml_content)
|
|
314
|
+
except yaml.YAMLError as e:
|
|
315
|
+
raise DeclarativeWorkflowError(f"Invalid YAML: {e}") from e
|
|
316
|
+
|
|
317
|
+
return self.create_workflow_from_definition(workflow_def, base_path=base_path)
|
|
318
|
+
|
|
319
|
+
def create_workflow_from_definition(
|
|
320
|
+
self,
|
|
321
|
+
workflow_def: dict[str, Any],
|
|
322
|
+
base_path: Path | None = None,
|
|
323
|
+
) -> Workflow:
|
|
324
|
+
"""Create a Workflow from a parsed workflow definition dictionary.
|
|
325
|
+
|
|
326
|
+
This is the lowest-level creation method, useful when you already have
|
|
327
|
+
a parsed dictionary (e.g., from programmatic construction or custom parsing).
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
workflow_def: The parsed workflow definition dictionary containing
|
|
331
|
+
'kind', 'trigger', 'actions', and optionally 'agents' keys.
|
|
332
|
+
base_path: Optional base path for resolving relative file references
|
|
333
|
+
in agent definitions.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
An executable Workflow object with action nodes for each YAML action.
|
|
337
|
+
|
|
338
|
+
Raises:
|
|
339
|
+
DeclarativeWorkflowError: If the definition is invalid or missing required fields.
|
|
340
|
+
|
|
341
|
+
Examples:
|
|
342
|
+
.. code-block:: python
|
|
343
|
+
|
|
344
|
+
from agent_framework.declarative import WorkflowFactory
|
|
345
|
+
|
|
346
|
+
# Programmatically construct a workflow definition
|
|
347
|
+
workflow_def = {
|
|
348
|
+
"kind": "Workflow",
|
|
349
|
+
"name": "my_workflow",
|
|
350
|
+
"trigger": {
|
|
351
|
+
"kind": "OnConversationStart",
|
|
352
|
+
"id": "main_trigger",
|
|
353
|
+
"actions": [
|
|
354
|
+
{
|
|
355
|
+
"kind": "SetVariable",
|
|
356
|
+
"id": "init",
|
|
357
|
+
"variable": "Local.Counter",
|
|
358
|
+
"value": 0,
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
"kind": "SendActivity",
|
|
362
|
+
"id": "output",
|
|
363
|
+
"activity": "Counter initialized",
|
|
364
|
+
},
|
|
365
|
+
],
|
|
366
|
+
},
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
factory = WorkflowFactory()
|
|
370
|
+
workflow = factory.create_workflow_from_definition(workflow_def)
|
|
371
|
+
"""
|
|
372
|
+
# Validate the workflow definition
|
|
373
|
+
self._validate_workflow_def(workflow_def)
|
|
374
|
+
|
|
375
|
+
# Extract workflow metadata
|
|
376
|
+
# Support both "name" field and trigger.id for workflow name
|
|
377
|
+
name: str = workflow_def.get("name", "")
|
|
378
|
+
if not name:
|
|
379
|
+
trigger: dict[str, Any] = workflow_def.get("trigger", {})
|
|
380
|
+
trigger_id = trigger.get("id", "declarative_workflow")
|
|
381
|
+
name = str(trigger_id) if trigger_id else "declarative_workflow"
|
|
382
|
+
description = workflow_def.get("description")
|
|
383
|
+
|
|
384
|
+
# Create agents from definitions
|
|
385
|
+
agents: dict[str, SupportsAgentRun | AgentExecutor] = dict(self._agents)
|
|
386
|
+
agent_defs = workflow_def.get("agents", {})
|
|
387
|
+
|
|
388
|
+
for agent_name, agent_def in agent_defs.items():
|
|
389
|
+
if agent_name in agents:
|
|
390
|
+
# Already have this agent
|
|
391
|
+
continue
|
|
392
|
+
|
|
393
|
+
# Create agent using AgentFactory
|
|
394
|
+
try:
|
|
395
|
+
agent = self._create_agent_from_def(agent_def, base_path)
|
|
396
|
+
agents[agent_name] = agent
|
|
397
|
+
logger.debug(f"Created agent '{agent_name}' from definition")
|
|
398
|
+
except Exception as e:
|
|
399
|
+
logger.error(f"Failed to create agent '{agent_name}': {e}")
|
|
400
|
+
raise DeclarativeWorkflowError(f"Failed to create agent '{agent_name}': {e}") from e
|
|
401
|
+
|
|
402
|
+
return self._create_workflow(workflow_def, name, description, agents)
|
|
403
|
+
|
|
404
|
+
def _create_workflow(
|
|
405
|
+
self,
|
|
406
|
+
workflow_def: dict[str, Any],
|
|
407
|
+
name: str,
|
|
408
|
+
description: str | None,
|
|
409
|
+
agents: dict[str, SupportsAgentRun | AgentExecutor],
|
|
410
|
+
) -> Workflow:
|
|
411
|
+
"""Create workflow from definition.
|
|
412
|
+
|
|
413
|
+
Each YAML action becomes a real Executor node in the workflow graph.
|
|
414
|
+
This enables checkpointing at action boundaries.
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
workflow_def: The workflow definition
|
|
418
|
+
name: Workflow name
|
|
419
|
+
description: Workflow description
|
|
420
|
+
agents: Registry of agent instances
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
Workflow with individual action executors as nodes
|
|
424
|
+
"""
|
|
425
|
+
# Normalize workflow definition to have actions at top level
|
|
426
|
+
normalized_def = self._normalize_workflow_def(workflow_def)
|
|
427
|
+
normalized_def["name"] = name
|
|
428
|
+
if description:
|
|
429
|
+
normalized_def["description"] = description
|
|
430
|
+
|
|
431
|
+
# Build the DeclarativeEnvConfig from the factory's configuration and the
|
|
432
|
+
# set of Env references actually used in the workflow PowerFx expressions.
|
|
433
|
+
# The referenced-name allowlist constrains ``os.environ`` fallback (when
|
|
434
|
+
# enabled) so unrelated variables never enter the PowerFx scope.
|
|
435
|
+
env_config = DeclarativeEnvConfig(
|
|
436
|
+
values=dict(self._configuration),
|
|
437
|
+
restrict_to_configuration=self._restrict_env_to_configuration,
|
|
438
|
+
referenced_names=frozenset(discover_env_references(normalized_def)),
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
# Build the graph-based workflow, passing agents and tools for specialized executors
|
|
442
|
+
try:
|
|
443
|
+
graph_builder = DeclarativeWorkflowBuilder(
|
|
444
|
+
normalized_def,
|
|
445
|
+
workflow_id=name,
|
|
446
|
+
agents=agents,
|
|
447
|
+
tools=self._tools,
|
|
448
|
+
checkpoint_storage=self._checkpoint_storage,
|
|
449
|
+
max_iterations=self._max_iterations,
|
|
450
|
+
http_request_handler=self._http_request_handler,
|
|
451
|
+
mcp_tool_handler=self._mcp_tool_handler,
|
|
452
|
+
env_config=env_config,
|
|
453
|
+
)
|
|
454
|
+
workflow = graph_builder.build()
|
|
455
|
+
except ValueError as e:
|
|
456
|
+
raise DeclarativeWorkflowError(f"Failed to build graph-based workflow: {e}") from e
|
|
457
|
+
|
|
458
|
+
# Store agents, bindings, and tools for reference (executors already have them)
|
|
459
|
+
workflow._declarative_agents = agents # type: ignore[attr-defined]
|
|
460
|
+
workflow._declarative_bindings = self._bindings # type: ignore[attr-defined]
|
|
461
|
+
workflow._declarative_tools = self._tools # type: ignore[attr-defined]
|
|
462
|
+
|
|
463
|
+
# Store input schema if defined in workflow definition
|
|
464
|
+
# This allows DevUI to generate proper input forms
|
|
465
|
+
if "inputs" in workflow_def:
|
|
466
|
+
workflow.input_schema = self._convert_inputs_to_json_schema(workflow_def["inputs"]) # type: ignore[attr-defined]
|
|
467
|
+
|
|
468
|
+
logger.debug(
|
|
469
|
+
"Created graph-based workflow '%s' with %d executors",
|
|
470
|
+
name,
|
|
471
|
+
len(graph_builder._executors), # type: ignore[reportPrivateUsage]
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
return workflow
|
|
475
|
+
|
|
476
|
+
def _normalize_workflow_def(self, workflow_def: dict[str, Any]) -> dict[str, Any]:
|
|
477
|
+
"""Normalize workflow definition to have actions at top level.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
workflow_def: The workflow definition
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
Normalized definition with actions at top level
|
|
484
|
+
"""
|
|
485
|
+
actions = self._get_actions_from_def(workflow_def)
|
|
486
|
+
return {
|
|
487
|
+
**workflow_def,
|
|
488
|
+
"actions": actions,
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
def _validate_workflow_def(self, workflow_def: dict[str, Any]) -> None:
|
|
492
|
+
"""Validate a workflow definition.
|
|
493
|
+
|
|
494
|
+
Args:
|
|
495
|
+
workflow_def: The workflow definition to validate
|
|
496
|
+
|
|
497
|
+
Raises:
|
|
498
|
+
DeclarativeWorkflowError: If the definition is invalid
|
|
499
|
+
"""
|
|
500
|
+
if not isinstance(workflow_def, dict):
|
|
501
|
+
raise DeclarativeWorkflowError("Workflow definition must be a dictionary")
|
|
502
|
+
|
|
503
|
+
# Handle both formats:
|
|
504
|
+
# 1. Direct actions list: {"actions": [...]}
|
|
505
|
+
# 2. Trigger-based: {"kind": "Workflow", "trigger": {"actions": [...]}}
|
|
506
|
+
actions = self._get_actions_from_def(workflow_def)
|
|
507
|
+
|
|
508
|
+
if not isinstance(actions, list):
|
|
509
|
+
raise DeclarativeWorkflowError("Workflow 'actions' must be a list")
|
|
510
|
+
|
|
511
|
+
# Validate each action has a kind
|
|
512
|
+
for i, action in enumerate(actions):
|
|
513
|
+
if not isinstance(action, dict):
|
|
514
|
+
raise DeclarativeWorkflowError(f"Action at index {i} must be a dictionary")
|
|
515
|
+
if "kind" not in action:
|
|
516
|
+
raise DeclarativeWorkflowError(f"Action at index {i} missing 'kind' field")
|
|
517
|
+
|
|
518
|
+
def _get_actions_from_def(self, workflow_def: dict[str, Any]) -> list[dict[str, Any]]:
|
|
519
|
+
"""Extract actions from a workflow definition.
|
|
520
|
+
|
|
521
|
+
Handles both direct actions format and trigger-based format.
|
|
522
|
+
|
|
523
|
+
Args:
|
|
524
|
+
workflow_def: The workflow definition
|
|
525
|
+
|
|
526
|
+
Returns:
|
|
527
|
+
List of action definitions
|
|
528
|
+
|
|
529
|
+
Raises:
|
|
530
|
+
DeclarativeWorkflowError: If no actions can be found
|
|
531
|
+
"""
|
|
532
|
+
# Try direct actions first
|
|
533
|
+
if "actions" in workflow_def:
|
|
534
|
+
actions: list[dict[str, Any]] = workflow_def["actions"]
|
|
535
|
+
return actions
|
|
536
|
+
|
|
537
|
+
# Try trigger-based format
|
|
538
|
+
if "trigger" in workflow_def:
|
|
539
|
+
trigger = workflow_def["trigger"]
|
|
540
|
+
if isinstance(trigger, dict) and "actions" in trigger:
|
|
541
|
+
trigger_actions: list[dict[str, Any]] = list(trigger["actions"]) # type: ignore[arg-type]
|
|
542
|
+
return trigger_actions
|
|
543
|
+
|
|
544
|
+
raise DeclarativeWorkflowError("Workflow definition must have 'actions' field or 'trigger.actions' field")
|
|
545
|
+
|
|
546
|
+
def _create_agent_from_def(
|
|
547
|
+
self,
|
|
548
|
+
agent_def: dict[str, Any],
|
|
549
|
+
base_path: Path | None = None,
|
|
550
|
+
) -> Any:
|
|
551
|
+
"""Create an agent from a definition.
|
|
552
|
+
|
|
553
|
+
Args:
|
|
554
|
+
agent_def: The agent definition dictionary
|
|
555
|
+
base_path: Optional base path for resolving relative file references
|
|
556
|
+
|
|
557
|
+
Returns:
|
|
558
|
+
An agent instance
|
|
559
|
+
"""
|
|
560
|
+
# Check if it's a reference to an external file
|
|
561
|
+
if "file" in agent_def:
|
|
562
|
+
file_path = agent_def["file"]
|
|
563
|
+
if base_path and not Path(file_path).is_absolute():
|
|
564
|
+
file_path = base_path / file_path
|
|
565
|
+
return self._agent_factory.create_agent_from_yaml_path(file_path)
|
|
566
|
+
|
|
567
|
+
# Check if it's an inline agent definition
|
|
568
|
+
if "kind" in agent_def:
|
|
569
|
+
return self._agent_factory.create_agent_from_dict(agent_def)
|
|
570
|
+
|
|
571
|
+
# Handle connection-based agent (like Azure AI agents)
|
|
572
|
+
if "connection" in agent_def:
|
|
573
|
+
# This would create a hosted agent client
|
|
574
|
+
# For now, we'll need the user to provide pre-created agents
|
|
575
|
+
raise DeclarativeWorkflowError(
|
|
576
|
+
"Connection-based agents must be provided via the 'agents' parameter. "
|
|
577
|
+
"Create the agent using the appropriate client and pass it to WorkflowFactory."
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
raise DeclarativeWorkflowError(
|
|
581
|
+
f"Invalid agent definition. Expected 'file', 'kind', or 'connection': {agent_def}"
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
def register_agent(self, name: str, agent: SupportsAgentRun | AgentExecutor) -> WorkflowFactory:
|
|
585
|
+
"""Register an agent instance with the factory for use in workflows.
|
|
586
|
+
|
|
587
|
+
Registered agents are available to InvokeAzureAgent actions by name.
|
|
588
|
+
This method supports fluent chaining.
|
|
589
|
+
|
|
590
|
+
Args:
|
|
591
|
+
name: The name to register the agent under. Must match the agent name
|
|
592
|
+
referenced in InvokeAzureAgent actions.
|
|
593
|
+
agent: The agent instance (typically a Agent or similar).
|
|
594
|
+
|
|
595
|
+
Returns:
|
|
596
|
+
Self for method chaining.
|
|
597
|
+
|
|
598
|
+
Examples:
|
|
599
|
+
.. code-block:: python
|
|
600
|
+
|
|
601
|
+
from agent_framework.openai import OpenAIChatClient
|
|
602
|
+
from agent_framework.declarative import WorkflowFactory
|
|
603
|
+
|
|
604
|
+
client = OpenAIChatClient()
|
|
605
|
+
|
|
606
|
+
# Method chaining to register multiple agents
|
|
607
|
+
factory = (
|
|
608
|
+
WorkflowFactory()
|
|
609
|
+
.register_agent(
|
|
610
|
+
"Writer",
|
|
611
|
+
client.as_agent(
|
|
612
|
+
name="Writer",
|
|
613
|
+
instructions="Write content.",
|
|
614
|
+
),
|
|
615
|
+
)
|
|
616
|
+
.register_agent(
|
|
617
|
+
"Reviewer",
|
|
618
|
+
client.as_agent(
|
|
619
|
+
name="Reviewer",
|
|
620
|
+
instructions="Review content.",
|
|
621
|
+
),
|
|
622
|
+
)
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
workflow = factory.create_workflow_from_yaml_path("workflow.yaml")
|
|
626
|
+
"""
|
|
627
|
+
self._agents[name] = agent
|
|
628
|
+
return self
|
|
629
|
+
|
|
630
|
+
def register_binding(self, name: str, func: Any) -> WorkflowFactory:
|
|
631
|
+
"""Register a function binding with the factory for use in workflow actions.
|
|
632
|
+
|
|
633
|
+
Bindings allow workflow actions to invoke Python functions by name.
|
|
634
|
+
This method supports fluent chaining.
|
|
635
|
+
|
|
636
|
+
Args:
|
|
637
|
+
name: The name to register the function under.
|
|
638
|
+
func: The function to bind.
|
|
639
|
+
|
|
640
|
+
Returns:
|
|
641
|
+
Self for method chaining.
|
|
642
|
+
|
|
643
|
+
Examples:
|
|
644
|
+
.. code-block:: python
|
|
645
|
+
|
|
646
|
+
from agent_framework.declarative import WorkflowFactory
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def get_weather(location: str) -> str:
|
|
650
|
+
return f"Weather in {location}: Sunny, 72F"
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def send_email(to: str, subject: str, body: str) -> bool:
|
|
654
|
+
# Send email logic
|
|
655
|
+
return True
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
# Register functions for use in workflow
|
|
659
|
+
factory = (
|
|
660
|
+
WorkflowFactory()
|
|
661
|
+
.register_binding("get_weather", get_weather)
|
|
662
|
+
.register_binding("send_email", send_email)
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
workflow = factory.create_workflow_from_yaml_path("workflow.yaml")
|
|
666
|
+
"""
|
|
667
|
+
if not callable(func):
|
|
668
|
+
raise TypeError(f"Expected a callable for binding '{name}', got {type(func).__name__}")
|
|
669
|
+
self._bindings[name] = func
|
|
670
|
+
return self
|
|
671
|
+
|
|
672
|
+
def register_tool(self, name: str, func: Any) -> WorkflowFactory:
|
|
673
|
+
"""Register a function with the factory for use in InvokeFunctionTool actions.
|
|
674
|
+
|
|
675
|
+
Registered functions are available to InvokeFunctionTool actions by name via the functionName field.
|
|
676
|
+
This method supports fluent chaining.
|
|
677
|
+
|
|
678
|
+
Args:
|
|
679
|
+
name: The name to register the function under. Must match the functionName
|
|
680
|
+
referenced in InvokeFunctionTool actions.
|
|
681
|
+
func: The function to register (can be sync or async).
|
|
682
|
+
|
|
683
|
+
Returns:
|
|
684
|
+
Self for method chaining.
|
|
685
|
+
|
|
686
|
+
Examples:
|
|
687
|
+
.. code-block:: python
|
|
688
|
+
|
|
689
|
+
from agent_framework_declarative import WorkflowFactory
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def get_weather(location: str, unit: str = "F") -> dict:
|
|
693
|
+
return {"temp": 72, "unit": unit, "location": location}
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
async def fetch_data(url: str) -> dict:
|
|
697
|
+
# Async function example
|
|
698
|
+
return {"data": "..."}
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
# Register functions for use in InvokeFunctionTool workflow actions
|
|
702
|
+
factory = (
|
|
703
|
+
WorkflowFactory().register_tool("get_weather", get_weather).register_tool("fetch_data", fetch_data)
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
workflow = factory.create_workflow_from_yaml_path("workflow.yaml")
|
|
707
|
+
|
|
708
|
+
The workflow YAML can then reference these tools:
|
|
709
|
+
|
|
710
|
+
.. code-block:: yaml
|
|
711
|
+
|
|
712
|
+
actions:
|
|
713
|
+
- kind: InvokeFunctionTool
|
|
714
|
+
id: call_weather
|
|
715
|
+
functionName: get_weather
|
|
716
|
+
arguments:
|
|
717
|
+
location: =Local.city
|
|
718
|
+
unit: F
|
|
719
|
+
output:
|
|
720
|
+
result: Local.weatherData
|
|
721
|
+
"""
|
|
722
|
+
if not callable(func):
|
|
723
|
+
raise TypeError(f"Expected a callable for tool '{name}', got {type(func).__name__}")
|
|
724
|
+
self._tools[name] = func
|
|
725
|
+
return self
|
|
726
|
+
|
|
727
|
+
def _convert_inputs_to_json_schema(self, inputs_def: dict[str, Any]) -> dict[str, Any]:
|
|
728
|
+
"""Convert a declarative inputs definition to JSON Schema.
|
|
729
|
+
|
|
730
|
+
The inputs definition uses a simplified format:
|
|
731
|
+
inputs:
|
|
732
|
+
age:
|
|
733
|
+
type: integer
|
|
734
|
+
description: The user's age
|
|
735
|
+
name:
|
|
736
|
+
type: string
|
|
737
|
+
|
|
738
|
+
This is converted to standard JSON Schema format.
|
|
739
|
+
|
|
740
|
+
Args:
|
|
741
|
+
inputs_def: The inputs definition from the workflow YAML
|
|
742
|
+
|
|
743
|
+
Returns:
|
|
744
|
+
A JSON Schema object
|
|
745
|
+
"""
|
|
746
|
+
properties: dict[str, Any] = {}
|
|
747
|
+
required: list[str] = []
|
|
748
|
+
|
|
749
|
+
for field_name, field_def in inputs_def.items():
|
|
750
|
+
if isinstance(field_def, dict):
|
|
751
|
+
# Field has type and possibly other attributes
|
|
752
|
+
prop: dict[str, Any] = {}
|
|
753
|
+
field_def_dict: dict[str, Any] = cast(dict[str, Any], field_def)
|
|
754
|
+
field_type: str = str(field_def_dict.get("type", "string"))
|
|
755
|
+
|
|
756
|
+
# Map declarative types to JSON Schema types
|
|
757
|
+
type_mapping: dict[str, str] = {
|
|
758
|
+
"string": "string",
|
|
759
|
+
"str": "string",
|
|
760
|
+
"integer": "integer",
|
|
761
|
+
"int": "integer",
|
|
762
|
+
"number": "number",
|
|
763
|
+
"float": "number",
|
|
764
|
+
"boolean": "boolean",
|
|
765
|
+
"bool": "boolean",
|
|
766
|
+
"array": "array",
|
|
767
|
+
"list": "array",
|
|
768
|
+
"object": "object",
|
|
769
|
+
"dict": "object",
|
|
770
|
+
}
|
|
771
|
+
prop["type"] = type_mapping.get(field_type, field_type)
|
|
772
|
+
|
|
773
|
+
# Copy other attributes
|
|
774
|
+
if "description" in field_def_dict:
|
|
775
|
+
prop["description"] = field_def_dict["description"]
|
|
776
|
+
if "default" in field_def_dict:
|
|
777
|
+
prop["default"] = field_def_dict["default"]
|
|
778
|
+
if "enum" in field_def_dict:
|
|
779
|
+
prop["enum"] = field_def_dict["enum"]
|
|
780
|
+
|
|
781
|
+
# Check if required (default: true unless explicitly false)
|
|
782
|
+
if field_def_dict.get("required", True):
|
|
783
|
+
required.append(field_name)
|
|
784
|
+
|
|
785
|
+
properties[field_name] = prop
|
|
786
|
+
else:
|
|
787
|
+
# Simple type definition (e.g., "age: integer")
|
|
788
|
+
type_mapping_simple: dict[str, str] = {
|
|
789
|
+
"string": "string",
|
|
790
|
+
"str": "string",
|
|
791
|
+
"integer": "integer",
|
|
792
|
+
"int": "integer",
|
|
793
|
+
"number": "number",
|
|
794
|
+
"float": "number",
|
|
795
|
+
"boolean": "boolean",
|
|
796
|
+
"bool": "boolean",
|
|
797
|
+
}
|
|
798
|
+
properties[field_name] = {"type": type_mapping_simple.get(str(field_def), "string")}
|
|
799
|
+
required.append(field_name)
|
|
800
|
+
|
|
801
|
+
schema: dict[str, Any] = {
|
|
802
|
+
"type": "object",
|
|
803
|
+
"properties": properties,
|
|
804
|
+
}
|
|
805
|
+
if required:
|
|
806
|
+
schema["required"] = required
|
|
807
|
+
|
|
808
|
+
return schema
|