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,660 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Tool invocation executors for declarative workflows.
|
|
4
|
+
|
|
5
|
+
Provides base abstractions and concrete executors for invoking various tool types
|
|
6
|
+
(functions, APIs, MCP servers, etc.) with support for approval flows and structured output.
|
|
7
|
+
|
|
8
|
+
This module is designed for extensibility:
|
|
9
|
+
- BaseToolExecutor provides common patterns (registry lookup, approval flow, output formatting)
|
|
10
|
+
- Concrete executors (InvokeFunctionToolExecutor) implement tool-specific invocation logic
|
|
11
|
+
- New tool types can be added by subclassing BaseToolExecutor
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
import uuid
|
|
17
|
+
from abc import abstractmethod
|
|
18
|
+
from collections.abc import Callable, Mapping
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from inspect import isawaitable
|
|
21
|
+
from typing import Any, cast
|
|
22
|
+
|
|
23
|
+
from agent_framework import (
|
|
24
|
+
Content,
|
|
25
|
+
Message,
|
|
26
|
+
WorkflowContext,
|
|
27
|
+
handler,
|
|
28
|
+
response_handler,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
from ._declarative_base import (
|
|
32
|
+
ActionComplete,
|
|
33
|
+
DeclarativeActionExecutor,
|
|
34
|
+
DeclarativeWorkflowState,
|
|
35
|
+
)
|
|
36
|
+
from ._executors_agents import TOOL_REGISTRY_KEY
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
# Registry key for function tools in State - reuse existing key so functions registered
|
|
41
|
+
# at runtime are discoverable by both agent-based and function-based tool executors.
|
|
42
|
+
FUNCTION_TOOL_REGISTRY_KEY = TOOL_REGISTRY_KEY
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ============================================================================
|
|
46
|
+
# Request/Response Types for Approval Flow
|
|
47
|
+
# ============================================================================
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class ToolApprovalRequest:
|
|
52
|
+
"""Request for approval before invoking a tool.
|
|
53
|
+
|
|
54
|
+
Emitted when requireApproval=true, signaling that the workflow should yield
|
|
55
|
+
and wait for user approval before invoking the tool.
|
|
56
|
+
|
|
57
|
+
This follows the same pattern as AgentExternalInputRequest from _executors_agents.py,
|
|
58
|
+
allowing consistent handling of human-in-loop scenarios across agents and tools.
|
|
59
|
+
|
|
60
|
+
Attributes:
|
|
61
|
+
request_id: Unique identifier for this approval request.
|
|
62
|
+
function_name: Evaluated function name to be invoked.
|
|
63
|
+
arguments: Evaluated arguments to be passed to the function.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
request_id: str
|
|
67
|
+
function_name: str
|
|
68
|
+
arguments: dict[str, Any]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class ToolApprovalResponse:
|
|
73
|
+
"""Response to a ToolApprovalRequest.
|
|
74
|
+
|
|
75
|
+
Provided by the caller to approve or reject tool invocation.
|
|
76
|
+
|
|
77
|
+
Attributes:
|
|
78
|
+
approved: Whether the tool invocation was approved.
|
|
79
|
+
reason: Optional reason for rejection.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
approved: bool
|
|
83
|
+
reason: str | None = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ============================================================================
|
|
87
|
+
# Result Types
|
|
88
|
+
# ============================================================================
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class ToolInvocationResult:
|
|
93
|
+
"""Result from a tool invocation.
|
|
94
|
+
|
|
95
|
+
Attributes:
|
|
96
|
+
success: Whether the invocation succeeded.
|
|
97
|
+
result: The return value from the tool (if successful).
|
|
98
|
+
error: Error message (if failed).
|
|
99
|
+
messages: Message list format for conversation history.
|
|
100
|
+
rejected: Whether the invocation was rejected during approval.
|
|
101
|
+
rejection_reason: Reason for rejection.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
success: bool
|
|
105
|
+
result: Any = None
|
|
106
|
+
error: str | None = None
|
|
107
|
+
messages: list[Message] = field(default_factory=cast(Callable[..., list[Message]], list))
|
|
108
|
+
rejected: bool = False
|
|
109
|
+
rejection_reason: str | None = None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ============================================================================
|
|
113
|
+
# Helper Functions
|
|
114
|
+
# ============================================================================
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _normalize_variable_path(variable: str) -> str:
|
|
118
|
+
"""Normalize variable names to ensure they have a scope prefix.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
variable: Variable name like 'Local.X' or 'weatherResult'
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
The variable path with a scope prefix (defaults to Local if none provided)
|
|
125
|
+
"""
|
|
126
|
+
if variable.startswith(("Local.", "System.", "Workflow.", "Agent.", "Conversation.")):
|
|
127
|
+
return variable
|
|
128
|
+
if "." in variable:
|
|
129
|
+
return variable
|
|
130
|
+
return "Local." + variable
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ============================================================================
|
|
134
|
+
# Base Tool Executor (Abstract)
|
|
135
|
+
# ============================================================================
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class BaseToolExecutor(DeclarativeActionExecutor):
|
|
139
|
+
"""Base class for tool invocation executors.
|
|
140
|
+
|
|
141
|
+
Provides common functionality for all tool-like executors:
|
|
142
|
+
- Tool registry lookup (State + WorkflowFactory registration)
|
|
143
|
+
- Approval flow (request_info pattern with yield/resume)
|
|
144
|
+
- Output formatting (messages as Message list + result variable)
|
|
145
|
+
- Error handling (stores error in output, doesn't raise)
|
|
146
|
+
|
|
147
|
+
Subclasses must implement:
|
|
148
|
+
- _invoke_tool(): Perform the actual tool invocation
|
|
149
|
+
|
|
150
|
+
YAML Schema (common fields):
|
|
151
|
+
kind: <ToolKind>
|
|
152
|
+
id: unique_id
|
|
153
|
+
functionName: function_to_call # required, supports =expression syntax
|
|
154
|
+
requireApproval: true # optional, default=false
|
|
155
|
+
arguments: # optional dictionary
|
|
156
|
+
param1: value1
|
|
157
|
+
param2: =Local.dynamicValue
|
|
158
|
+
output:
|
|
159
|
+
messages: Local.toolCallMessages # Message list
|
|
160
|
+
result: Local.toolResult
|
|
161
|
+
autoSend: true # optional, default=true
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
def __init__(
|
|
165
|
+
self,
|
|
166
|
+
action_def: dict[str, Any],
|
|
167
|
+
*,
|
|
168
|
+
id: str | None = None,
|
|
169
|
+
tools: dict[str, Any] | None = None,
|
|
170
|
+
):
|
|
171
|
+
"""Initialize the tool executor.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
action_def: The action definition from YAML
|
|
175
|
+
id: Optional executor ID
|
|
176
|
+
tools: Registry of tool instances by name (from WorkflowFactory)
|
|
177
|
+
"""
|
|
178
|
+
super().__init__(action_def, id=id)
|
|
179
|
+
self._tools = tools or {}
|
|
180
|
+
|
|
181
|
+
@abstractmethod
|
|
182
|
+
async def _invoke_tool(
|
|
183
|
+
self,
|
|
184
|
+
tool: Any,
|
|
185
|
+
function_name: str,
|
|
186
|
+
arguments: dict[str, Any],
|
|
187
|
+
state: DeclarativeWorkflowState,
|
|
188
|
+
) -> Any:
|
|
189
|
+
"""Invoke the tool with the given arguments.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
tool: The tool instance to invoke
|
|
193
|
+
function_name: Function/method name to call
|
|
194
|
+
arguments: Arguments to pass
|
|
195
|
+
state: Workflow state
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
The result from the tool invocation
|
|
199
|
+
|
|
200
|
+
Raises:
|
|
201
|
+
Any exception from the tool invocation
|
|
202
|
+
"""
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
def _get_tool(
|
|
206
|
+
self,
|
|
207
|
+
function_name: str,
|
|
208
|
+
ctx: WorkflowContext[Any, Any],
|
|
209
|
+
) -> Any | None:
|
|
210
|
+
"""Get tool from registry.
|
|
211
|
+
|
|
212
|
+
Checks both WorkflowFactory registry (self._tools) and State registry.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
function_name: Name of the function
|
|
216
|
+
ctx: Workflow context
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
The tool/function, or None if not found
|
|
220
|
+
"""
|
|
221
|
+
# Check WorkflowFactory registry first (passed in constructor)
|
|
222
|
+
tool = self._tools.get(function_name)
|
|
223
|
+
if tool is not None:
|
|
224
|
+
return tool
|
|
225
|
+
|
|
226
|
+
# Check State registry (for runtime registration)
|
|
227
|
+
try:
|
|
228
|
+
tool_registry: dict[str, Any] | None = ctx.state.get(FUNCTION_TOOL_REGISTRY_KEY)
|
|
229
|
+
if tool_registry:
|
|
230
|
+
return tool_registry.get(function_name)
|
|
231
|
+
except KeyError:
|
|
232
|
+
logger.debug(
|
|
233
|
+
"%s: tool registry key '%s' not found in state "
|
|
234
|
+
"(this is normal if tools are only registered via WorkflowFactory)",
|
|
235
|
+
self.__class__.__name__,
|
|
236
|
+
FUNCTION_TOOL_REGISTRY_KEY,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
def _get_output_config(self) -> tuple[str | None, str | None, bool]:
|
|
242
|
+
"""Parse output configuration from action definition.
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
Tuple of (messages_var, result_var, auto_send)
|
|
246
|
+
"""
|
|
247
|
+
output_config: dict[str, str | bool] = self._action_def.get("output", {})
|
|
248
|
+
|
|
249
|
+
if not isinstance(output_config, Mapping):
|
|
250
|
+
return None, None, True
|
|
251
|
+
|
|
252
|
+
messages_var = output_config.get("messages")
|
|
253
|
+
result_var = output_config.get("result")
|
|
254
|
+
auto_send = bool(output_config.get("autoSend", True))
|
|
255
|
+
return (
|
|
256
|
+
str(messages_var) if messages_var else None,
|
|
257
|
+
str(result_var) if result_var else None,
|
|
258
|
+
auto_send,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
def _store_result(
|
|
262
|
+
self,
|
|
263
|
+
result: ToolInvocationResult,
|
|
264
|
+
state: DeclarativeWorkflowState,
|
|
265
|
+
messages_var: str | None,
|
|
266
|
+
result_var: str | None,
|
|
267
|
+
) -> None:
|
|
268
|
+
"""Store tool invocation result in workflow state.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
result: The tool invocation result
|
|
272
|
+
state: Workflow state
|
|
273
|
+
messages_var: Variable path for messages output
|
|
274
|
+
result_var: Variable path for result output
|
|
275
|
+
"""
|
|
276
|
+
# Store messages if variable specified
|
|
277
|
+
if messages_var:
|
|
278
|
+
path = _normalize_variable_path(messages_var)
|
|
279
|
+
state.set(path, result.messages)
|
|
280
|
+
|
|
281
|
+
# Store result if variable specified
|
|
282
|
+
if result_var:
|
|
283
|
+
path = _normalize_variable_path(result_var)
|
|
284
|
+
if result.rejected:
|
|
285
|
+
state.set(
|
|
286
|
+
path,
|
|
287
|
+
{
|
|
288
|
+
"approved": False,
|
|
289
|
+
"rejected": True,
|
|
290
|
+
"reason": result.rejection_reason,
|
|
291
|
+
},
|
|
292
|
+
)
|
|
293
|
+
elif result.success:
|
|
294
|
+
state.set(path, result.result)
|
|
295
|
+
else:
|
|
296
|
+
state.set(
|
|
297
|
+
path,
|
|
298
|
+
{
|
|
299
|
+
"error": result.error,
|
|
300
|
+
},
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
async def _format_messages(
|
|
304
|
+
self,
|
|
305
|
+
function_name: str,
|
|
306
|
+
arguments: dict[str, Any],
|
|
307
|
+
result: Any,
|
|
308
|
+
) -> list[Message]:
|
|
309
|
+
"""Format tool invocation as Message list.
|
|
310
|
+
|
|
311
|
+
Creates tool call + tool result message pair for conversation history,
|
|
312
|
+
following the same format as agent tool calls.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
function_name: Function name invoked
|
|
316
|
+
arguments: Arguments passed
|
|
317
|
+
result: Result from invocation
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
List of Message objects [tool_call_message, tool_result_message]
|
|
321
|
+
"""
|
|
322
|
+
call_id = str(uuid.uuid4())
|
|
323
|
+
|
|
324
|
+
# Safely serialize arguments to JSON
|
|
325
|
+
try:
|
|
326
|
+
arguments_str = json.dumps(arguments) if isinstance(arguments, dict) else str(arguments)
|
|
327
|
+
except (TypeError, ValueError) as e:
|
|
328
|
+
logger.warning(f"Failed to serialize arguments to JSON: {e}")
|
|
329
|
+
arguments_str = str(arguments)
|
|
330
|
+
|
|
331
|
+
# Tool call message (from assistant)
|
|
332
|
+
tool_call_content = Content.from_function_call(
|
|
333
|
+
call_id=call_id,
|
|
334
|
+
name=function_name,
|
|
335
|
+
arguments=arguments_str,
|
|
336
|
+
)
|
|
337
|
+
tool_call_message = Message(
|
|
338
|
+
role="assistant",
|
|
339
|
+
contents=[tool_call_content],
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# Safely serialize result to JSON
|
|
343
|
+
try:
|
|
344
|
+
result_str = json.dumps(result) if not isinstance(result, str) else result
|
|
345
|
+
except (TypeError, ValueError) as e:
|
|
346
|
+
logger.warning(f"Failed to serialize result to JSON: {e}")
|
|
347
|
+
result_str = str(result)
|
|
348
|
+
|
|
349
|
+
tool_result_content = Content.from_function_result(
|
|
350
|
+
call_id=call_id,
|
|
351
|
+
result=result_str,
|
|
352
|
+
)
|
|
353
|
+
tool_result_message = Message(
|
|
354
|
+
role="tool",
|
|
355
|
+
contents=[tool_result_content],
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
return [tool_call_message, tool_result_message]
|
|
359
|
+
|
|
360
|
+
async def _execute_tool_invocation(
|
|
361
|
+
self,
|
|
362
|
+
function_name: str,
|
|
363
|
+
arguments: dict[str, Any],
|
|
364
|
+
state: DeclarativeWorkflowState,
|
|
365
|
+
ctx: WorkflowContext[Any, Any],
|
|
366
|
+
) -> ToolInvocationResult:
|
|
367
|
+
"""Execute the tool invocation.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
function_name: Function to invoke
|
|
371
|
+
arguments: Arguments to pass
|
|
372
|
+
state: Workflow state
|
|
373
|
+
ctx: Workflow context
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
ToolInvocationResult with outcome
|
|
377
|
+
"""
|
|
378
|
+
# Get tool from registry
|
|
379
|
+
tool = self._get_tool(function_name, ctx)
|
|
380
|
+
if tool is None:
|
|
381
|
+
error_msg = f"Function '{function_name}' not found in registry"
|
|
382
|
+
logger.error(f"{self.__class__.__name__}: {error_msg}")
|
|
383
|
+
return ToolInvocationResult(
|
|
384
|
+
success=False,
|
|
385
|
+
error=error_msg,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
try:
|
|
389
|
+
# Invoke the tool (subclass implements this)
|
|
390
|
+
result_value = await self._invoke_tool(
|
|
391
|
+
tool=tool,
|
|
392
|
+
function_name=function_name,
|
|
393
|
+
arguments=arguments,
|
|
394
|
+
state=state,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
# Format as messages for conversation history
|
|
398
|
+
messages = await self._format_messages(
|
|
399
|
+
function_name=function_name,
|
|
400
|
+
arguments=arguments,
|
|
401
|
+
result=result_value,
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
return ToolInvocationResult(
|
|
405
|
+
success=True,
|
|
406
|
+
result=result_value,
|
|
407
|
+
messages=messages,
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
except Exception as e:
|
|
411
|
+
logger.error(
|
|
412
|
+
"%s: error invoking function '%s': %s: %s",
|
|
413
|
+
self.__class__.__name__,
|
|
414
|
+
function_name,
|
|
415
|
+
type(e).__name__,
|
|
416
|
+
e,
|
|
417
|
+
exc_info=True,
|
|
418
|
+
)
|
|
419
|
+
return ToolInvocationResult(
|
|
420
|
+
success=False,
|
|
421
|
+
error=f"{type(e).__name__}: {e}",
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
@handler
|
|
425
|
+
async def handle_action(
|
|
426
|
+
self,
|
|
427
|
+
trigger: Any,
|
|
428
|
+
ctx: WorkflowContext[ActionComplete, str],
|
|
429
|
+
) -> None:
|
|
430
|
+
"""Handle the tool invocation with optional approval flow.
|
|
431
|
+
|
|
432
|
+
When requireApproval=true:
|
|
433
|
+
1. Saves invocation state to State (keyed by executor ID)
|
|
434
|
+
2. Emits ToolApprovalRequest via ctx.request_info()
|
|
435
|
+
3. Workflow yields (returns without ActionComplete)
|
|
436
|
+
4. Resumes in handle_approval_response() when user responds
|
|
437
|
+
"""
|
|
438
|
+
state = await self._ensure_state_initialized(ctx, trigger)
|
|
439
|
+
|
|
440
|
+
# Parse output configuration early so we can store errors
|
|
441
|
+
messages_var, result_var, auto_send = self._get_output_config()
|
|
442
|
+
|
|
443
|
+
# Get and evaluate function name (required)
|
|
444
|
+
function_name_expr = self._action_def.get("functionName")
|
|
445
|
+
if not function_name_expr:
|
|
446
|
+
error_msg = f"Action '{self.id}' is missing required 'functionName' field"
|
|
447
|
+
logger.error(f"{self.__class__.__name__}: {error_msg}")
|
|
448
|
+
if result_var:
|
|
449
|
+
state.set(_normalize_variable_path(result_var), {"error": error_msg})
|
|
450
|
+
await ctx.send_message(ActionComplete())
|
|
451
|
+
return
|
|
452
|
+
|
|
453
|
+
function_name = state.eval_if_expression(function_name_expr)
|
|
454
|
+
if not function_name:
|
|
455
|
+
error_msg = f"Action '{self.id}': functionName expression evaluated to empty"
|
|
456
|
+
logger.error(f"{self.__class__.__name__}: {error_msg}")
|
|
457
|
+
if result_var:
|
|
458
|
+
state.set(_normalize_variable_path(result_var), {"error": error_msg})
|
|
459
|
+
await ctx.send_message(ActionComplete())
|
|
460
|
+
return
|
|
461
|
+
function_name = str(function_name)
|
|
462
|
+
|
|
463
|
+
# Evaluate arguments
|
|
464
|
+
arguments_def = self._action_def.get("arguments", {})
|
|
465
|
+
arguments: dict[str, Any] = {}
|
|
466
|
+
if arguments_def is not None and not isinstance(arguments_def, dict):
|
|
467
|
+
logger.warning(
|
|
468
|
+
"%s: 'arguments' must be a dictionary, got %s - ignoring",
|
|
469
|
+
self.__class__.__name__,
|
|
470
|
+
type(arguments_def).__name__,
|
|
471
|
+
)
|
|
472
|
+
elif isinstance(arguments_def, dict):
|
|
473
|
+
for key, value in arguments_def.items(): # type: ignore[reportUnknownVariableType]
|
|
474
|
+
arguments[key] = state.eval_if_expression(value)
|
|
475
|
+
|
|
476
|
+
# Check if approval is required
|
|
477
|
+
require_approval = self._action_def.get("requireApproval", False)
|
|
478
|
+
|
|
479
|
+
if require_approval:
|
|
480
|
+
# Emit approval request - the request payload is the source of
|
|
481
|
+
# truth for resumed invocation; no side-channel state is written.
|
|
482
|
+
request_id = str(uuid.uuid4())
|
|
483
|
+
request = ToolApprovalRequest(
|
|
484
|
+
request_id=request_id,
|
|
485
|
+
function_name=function_name,
|
|
486
|
+
arguments=arguments,
|
|
487
|
+
)
|
|
488
|
+
logger.info(f"{self.__class__.__name__}: requesting approval for '{function_name}'")
|
|
489
|
+
await ctx.request_info(request, ToolApprovalResponse, request_id=request_id)
|
|
490
|
+
# Workflow yields - will resume in handle_approval_response
|
|
491
|
+
return
|
|
492
|
+
|
|
493
|
+
# No approval required - invoke directly
|
|
494
|
+
result = await self._execute_tool_invocation(
|
|
495
|
+
function_name=function_name,
|
|
496
|
+
arguments=arguments,
|
|
497
|
+
state=state,
|
|
498
|
+
ctx=ctx,
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
self._store_result(result, state, messages_var, result_var)
|
|
502
|
+
if auto_send and result.success and result.result is not None:
|
|
503
|
+
await ctx.yield_output(str(result.result))
|
|
504
|
+
await ctx.send_message(ActionComplete())
|
|
505
|
+
|
|
506
|
+
@response_handler
|
|
507
|
+
async def handle_approval_response(
|
|
508
|
+
self,
|
|
509
|
+
original_request: ToolApprovalRequest,
|
|
510
|
+
response: ToolApprovalResponse,
|
|
511
|
+
ctx: WorkflowContext[ActionComplete, str],
|
|
512
|
+
) -> None:
|
|
513
|
+
"""Handle response to a ToolApprovalRequest.
|
|
514
|
+
|
|
515
|
+
Resumes after the workflow yielded for approval. The invocation
|
|
516
|
+
``function_name`` and ``arguments`` are sourced from
|
|
517
|
+
``original_request`` (the payload the reviewer approved); output
|
|
518
|
+
configuration is re-derived from the executor's action definition.
|
|
519
|
+
"""
|
|
520
|
+
state = self._get_state(ctx.state)
|
|
521
|
+
|
|
522
|
+
function_name = original_request.function_name
|
|
523
|
+
arguments = original_request.arguments
|
|
524
|
+
messages_var, result_var, auto_send = self._get_output_config()
|
|
525
|
+
|
|
526
|
+
# Check if approved
|
|
527
|
+
if not response.approved:
|
|
528
|
+
logger.info(f"{self.__class__.__name__}: tool invocation rejected: {response.reason}")
|
|
529
|
+
|
|
530
|
+
# Store rejection status (don't raise error)
|
|
531
|
+
result = ToolInvocationResult(
|
|
532
|
+
success=False,
|
|
533
|
+
rejected=True,
|
|
534
|
+
rejection_reason=response.reason,
|
|
535
|
+
messages=[
|
|
536
|
+
Message(
|
|
537
|
+
role="assistant",
|
|
538
|
+
contents=[
|
|
539
|
+
f"Function '{function_name}' was rejected: {response.reason or 'No reason provided'}"
|
|
540
|
+
],
|
|
541
|
+
)
|
|
542
|
+
],
|
|
543
|
+
)
|
|
544
|
+
self._store_result(result, state, messages_var, result_var)
|
|
545
|
+
await ctx.send_message(ActionComplete())
|
|
546
|
+
return
|
|
547
|
+
|
|
548
|
+
# Approved - execute the invocation
|
|
549
|
+
result = await self._execute_tool_invocation(
|
|
550
|
+
function_name=function_name,
|
|
551
|
+
arguments=arguments,
|
|
552
|
+
state=state,
|
|
553
|
+
ctx=ctx,
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
self._store_result(result, state, messages_var, result_var)
|
|
557
|
+
if auto_send and result.success and result.result is not None:
|
|
558
|
+
await ctx.yield_output(str(result.result))
|
|
559
|
+
await ctx.send_message(ActionComplete())
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
# ============================================================================
|
|
563
|
+
# Function Tool Executor (Concrete)
|
|
564
|
+
# ============================================================================
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
class InvokeFunctionToolExecutor(BaseToolExecutor):
|
|
568
|
+
"""Executor that invokes a Python function as a tool.
|
|
569
|
+
|
|
570
|
+
This executor supports invoking registered Python functions with:
|
|
571
|
+
- Expression evaluation for functionName and arguments
|
|
572
|
+
- Optional approval flow (yield/resume pattern)
|
|
573
|
+
- Async function support
|
|
574
|
+
- Message list output for conversation history
|
|
575
|
+
|
|
576
|
+
YAML Schema:
|
|
577
|
+
kind: InvokeFunctionTool
|
|
578
|
+
id: invoke_function_example
|
|
579
|
+
functionName: get_weather # required, supports =expression syntax
|
|
580
|
+
requireApproval: true # optional, default=false
|
|
581
|
+
arguments: # optional dictionary
|
|
582
|
+
location: =Local.location
|
|
583
|
+
unit: F
|
|
584
|
+
output:
|
|
585
|
+
messages: Local.weatherToolCallItems # Message list
|
|
586
|
+
result: Local.WeatherInfo
|
|
587
|
+
autoSend: true # optional, default=true
|
|
588
|
+
|
|
589
|
+
Tool Registration:
|
|
590
|
+
Tools can be registered via:
|
|
591
|
+
1. WorkflowFactory.register_tool("name", func) - preferred
|
|
592
|
+
2. Setting FUNCTION_TOOL_REGISTRY_KEY in State at runtime
|
|
593
|
+
|
|
594
|
+
Examples:
|
|
595
|
+
.. code-block:: python
|
|
596
|
+
|
|
597
|
+
from agent_framework_declarative import WorkflowFactory
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def get_weather(location: str, unit: str = "F") -> dict:
|
|
601
|
+
return {"temp": 72, "unit": unit, "location": location}
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
async def fetch_data(url: str) -> dict:
|
|
605
|
+
# async function example
|
|
606
|
+
return {"data": "..."}
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
factory = (
|
|
610
|
+
WorkflowFactory().register_tool("get_weather", get_weather).register_tool("fetch_data", fetch_data)
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
workflow = factory.create_workflow_from_yaml_path("workflow.yaml")
|
|
614
|
+
"""
|
|
615
|
+
|
|
616
|
+
async def _invoke_tool(
|
|
617
|
+
self,
|
|
618
|
+
tool: Any,
|
|
619
|
+
function_name: str,
|
|
620
|
+
arguments: dict[str, Any],
|
|
621
|
+
state: DeclarativeWorkflowState,
|
|
622
|
+
) -> Any:
|
|
623
|
+
"""Invoke the function tool.
|
|
624
|
+
|
|
625
|
+
Supports:
|
|
626
|
+
- Direct callable functions
|
|
627
|
+
- Async functions (via inspect.isawaitable)
|
|
628
|
+
|
|
629
|
+
Args:
|
|
630
|
+
tool: The tool/function to invoke
|
|
631
|
+
function_name: Name of the function (for error messages)
|
|
632
|
+
arguments: Arguments to pass to the function
|
|
633
|
+
state: Workflow state (not used for function tools)
|
|
634
|
+
|
|
635
|
+
Returns:
|
|
636
|
+
The result from the function invocation
|
|
637
|
+
|
|
638
|
+
Raises:
|
|
639
|
+
ValueError: If the tool is not callable
|
|
640
|
+
"""
|
|
641
|
+
if not callable(tool):
|
|
642
|
+
raise ValueError(f"Function '{function_name}' is not callable")
|
|
643
|
+
|
|
644
|
+
# Invoke the function
|
|
645
|
+
result = tool(**arguments)
|
|
646
|
+
|
|
647
|
+
# Handle async functions
|
|
648
|
+
if isawaitable(result):
|
|
649
|
+
result = await result
|
|
650
|
+
|
|
651
|
+
return result
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
# ============================================================================
|
|
655
|
+
# Executor Registry Export
|
|
656
|
+
# ============================================================================
|
|
657
|
+
|
|
658
|
+
TOOL_ACTION_EXECUTORS: dict[str, type[DeclarativeActionExecutor]] = {
|
|
659
|
+
"InvokeFunctionTool": InvokeFunctionToolExecutor,
|
|
660
|
+
}
|