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,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