Files
metabuilder/workflow/executor/python/tool_calls_handler.py
johndoe6345789 3d6ae4cbf7 feat: Add complete Python workflow executor from AutoMetabuilder
Add full Python workflow execution engine with:

Core Executor:
- engine.py: WorkflowEngine for running n8n configs
- n8n_executor.py: N8N-style workflow execution with connections
- node_executor.py: Individual node execution with plugin dispatch
- loop_executor.py: Loop node execution with iteration control
- execution_order.py: Topological sort for node ordering

Schema & Validation:
- n8n_schema.py: N8N workflow schema types and validation
- n8n_converter.py: Legacy to n8n schema conversion

Plugin System:
- plugin_loader.py: Dynamic plugin loading
- plugin_registry.py: Plugin discovery and registration
- plugin_map.json: 116 plugin type mappings

Runtime & Context:
- runtime.py: Workflow runtime container
- input_resolver.py: Binding and coercion resolution
- value_helpers.py: Value normalization helpers
- workflow_context_builder.py: Runtime context assembly
- workflow_config_loader.py: Configuration loading
- workflow_engine_builder.py: Engine assembly with dependencies

Utilities:
- tool_calls_handler.py: LLM tool call handling
- tool_runner.py: Tool execution with logging
- notification_helpers.py: Slack/Discord notifications
- workflow_adapter.py: N8N format handling
- workflow_graph.py: Node/edge graph for visualization

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 16:42:30 +00:00

97 lines
3.5 KiB
Python

"""Handle tool calls from LLM responses."""
import json
def handle_tool_calls(resp_msg, tool_map: dict, msgs: dict, args, policies: dict, logger) -> list:
"""Execute tool calls and return tool result messages."""
if not resp_msg.tool_calls:
return []
modifying_tools = set(policies.get("modifying_tools", []))
tool_results = []
for tool_call in resp_msg.tool_calls:
function_name = tool_call.function.name
call_id = tool_call.id
payload = json.loads(tool_call.function.arguments)
logger.trace("Tool call %s payload: %s", function_name, payload)
handler = tool_map.get(function_name)
if not handler:
msg_template = msgs.get(
"error_tool_not_found",
"Tool {name} not found or unavailable."
)
msg = msg_template.format(name=function_name)
logger.error(msg)
tool_results.append({
"tool_call_id": call_id,
"role": "tool",
"name": function_name,
"content": msg,
})
continue
if not args.yolo:
confirm = input(
msgs.get(
"confirm_tool_execution",
"Do you want to execute {name} with {args}? [y/N]: "
).format(name=function_name, args=payload)
)
if confirm.lower() != "y":
skipped_template = msgs.get("info_tool_skipped", "Skipping tool: {name}")
logger.info(skipped_template.format(name=function_name))
tool_results.append({
"tool_call_id": call_id,
"role": "tool",
"name": function_name,
"content": "Skipped by user.",
})
continue
if args.dry_run and function_name in modifying_tools:
logger.info(
msgs.get(
"info_dry_run_skipping",
"DRY RUN: Skipping state-modifying tool {name}"
).format(name=function_name)
)
tool_results.append({
"tool_call_id": call_id,
"role": "tool",
"name": function_name,
"content": "Skipped due to dry-run.",
})
continue
exec_template = msgs.get("info_executing_tool", "Executing tool: {name}")
logger.info(exec_template.format(name=function_name))
try:
result = handler(**payload)
content = str(result) if result is not None else "Success"
if hasattr(result, "__iter__") and not isinstance(result, str):
items = list(result)[:5]
content = "\n".join([f"- {item}" for item in items])
logger.info(content)
elif result is not None:
logger.info(result)
tool_results.append({
"tool_call_id": call_id,
"role": "tool",
"name": function_name,
"content": content,
})
except Exception as error: # pylint: disable=broad-exception-caught
error_msg = f"Error executing {function_name}: {error}"
logger.error(error_msg)
tool_results.append({
"tool_call_id": call_id,
"role": "tool",
"name": function_name,
"content": error_msg,
})
return tool_results