181 lines
6.3 KiB
Python
181 lines
6.3 KiB
Python
# Copyright (c) 2025 Patrick Motsch
|
|
# All rights reserved.
|
|
"""ActionToolAdapter: wraps existing workflow actions (dynamicMode=True) as agent tools."""
|
|
|
|
import logging
|
|
from typing import Dict, Any, List, Optional
|
|
|
|
from modules.serviceCenter.services.serviceAgent.datamodelAgent import (
|
|
ToolDefinition, ToolResult
|
|
)
|
|
from modules.serviceCenter.services.serviceAgent.toolRegistry import ToolRegistry
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ActionToolAdapter:
|
|
"""Wraps existing Workflow-Actions as Agent-Tools.
|
|
|
|
Iterates over discovered methods, finds actions with dynamicMode=True,
|
|
and registers them in the ToolRegistry with a compound name (method.action).
|
|
"""
|
|
|
|
def __init__(self, actionExecutor):
|
|
self._actionExecutor = actionExecutor
|
|
self._registeredTools: List[str] = []
|
|
|
|
def registerAll(self, toolRegistry: ToolRegistry):
|
|
"""Discover and register all dynamicMode actions as agent tools."""
|
|
from modules.workflows.processing.shared.methodDiscovery import methods
|
|
|
|
registered = 0
|
|
for methodName, methodInfo in methods.items():
|
|
if not methodName[0].isupper():
|
|
continue
|
|
|
|
shortName = methodName.replace("Method", "").lower()
|
|
methodInstance = methodInfo["instance"]
|
|
|
|
for actionName, actionInfo in methodInfo["actions"].items():
|
|
actionDef = methodInstance._actions.get(actionName)
|
|
if not actionDef or not getattr(actionDef, "dynamicMode", False):
|
|
continue
|
|
|
|
compoundName = f"{shortName}_{actionName}"
|
|
toolDef = _buildToolDefinition(compoundName, actionDef, actionInfo)
|
|
|
|
handler = _createDispatchHandler(self._actionExecutor, shortName, actionName)
|
|
toolRegistry.registerFromDefinition(toolDef, handler)
|
|
self._registeredTools.append(compoundName)
|
|
registered += 1
|
|
|
|
logger.info(f"ActionToolAdapter: registered {registered} tools from workflow actions")
|
|
|
|
@property
|
|
def registeredTools(self) -> List[str]:
|
|
"""Names of all tools registered by this adapter."""
|
|
return list(self._registeredTools)
|
|
|
|
|
|
def _buildToolDefinition(compoundName: str, actionDef, actionInfo: Dict[str, Any]) -> ToolDefinition:
|
|
"""Build a ToolDefinition from a WorkflowActionDefinition."""
|
|
parameters = _convertParameterSchema(actionInfo.get("parameters", {}))
|
|
|
|
return ToolDefinition(
|
|
name=compoundName,
|
|
description=actionDef.description or actionInfo.get("description", ""),
|
|
parameters=parameters,
|
|
readOnly=False
|
|
)
|
|
|
|
|
|
def _convertParameterSchema(actionParams: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Convert workflow action parameter schema to JSON Schema for tool definitions."""
|
|
properties = {}
|
|
required = []
|
|
|
|
for paramName, paramInfo in actionParams.items():
|
|
paramType = paramInfo.get("type", "str") if isinstance(paramInfo, dict) else "str"
|
|
paramDesc = paramInfo.get("description", "") if isinstance(paramInfo, dict) else ""
|
|
paramRequired = paramInfo.get("required", False) if isinstance(paramInfo, dict) else False
|
|
|
|
jsonType = _pythonTypeToJsonType(paramType)
|
|
prop: Dict[str, Any] = {
|
|
"type": jsonType,
|
|
"description": paramDesc,
|
|
}
|
|
if jsonType == "array":
|
|
prop["items"] = _pythonTypeToArrayItems(paramType) or {"type": "string"}
|
|
properties[paramName] = prop
|
|
|
|
if paramRequired:
|
|
required.append(paramName)
|
|
|
|
return {
|
|
"type": "object",
|
|
"properties": properties,
|
|
"required": required
|
|
}
|
|
|
|
|
|
_TYPE_MAPPING = {
|
|
"str": "string",
|
|
"int": "integer",
|
|
"float": "number",
|
|
"bool": "boolean",
|
|
"list": "array",
|
|
"dict": "object",
|
|
"List[str]": "array",
|
|
"List[int]": "array",
|
|
"List[dict]": "array",
|
|
"List[float]": "array",
|
|
"Dict[str, Any]": "object",
|
|
}
|
|
|
|
_ARRAY_ITEMS_MAPPING = {
|
|
"list": {"type": "string"},
|
|
"List[str]": {"type": "string"},
|
|
"List[int]": {"type": "integer"},
|
|
"List[float]": {"type": "number"},
|
|
"List[dict]": {"type": "object"},
|
|
}
|
|
|
|
|
|
def _pythonTypeToJsonType(pythonType: str) -> str:
|
|
"""Map Python type strings to JSON Schema types."""
|
|
return _TYPE_MAPPING.get(pythonType, "string")
|
|
|
|
|
|
def _pythonTypeToArrayItems(pythonType: str) -> Optional[Dict[str, Any]]:
|
|
"""Return the JSON Schema `items` descriptor for array types, or None."""
|
|
return _ARRAY_ITEMS_MAPPING.get(pythonType)
|
|
|
|
|
|
def _createDispatchHandler(actionExecutor, methodName: str, actionName: str):
|
|
"""Create an async handler that dispatches to the ActionExecutor."""
|
|
async def _handler(args: Dict[str, Any], context: Dict[str, Any]) -> ToolResult:
|
|
try:
|
|
result = await actionExecutor.executeAction(methodName, actionName, args)
|
|
data = _formatActionResult(result)
|
|
return ToolResult(
|
|
toolCallId="",
|
|
toolName=f"{methodName}_{actionName}",
|
|
success=result.success,
|
|
data=data,
|
|
error=result.error
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"ActionToolAdapter dispatch failed for {methodName}_{actionName}: {e}")
|
|
return ToolResult(
|
|
toolCallId="",
|
|
toolName=f"{methodName}_{actionName}",
|
|
success=False,
|
|
error=str(e)
|
|
)
|
|
return _handler
|
|
|
|
|
|
def _formatActionResult(result) -> str:
|
|
"""Format an ActionResult into a text representation for the agent."""
|
|
parts = []
|
|
|
|
if result.resultLabel:
|
|
parts.append(f"Result: {result.resultLabel}")
|
|
|
|
if result.error:
|
|
parts.append(f"Error: {result.error}")
|
|
|
|
if result.documents:
|
|
parts.append(f"Documents ({len(result.documents)}):")
|
|
for doc in result.documents:
|
|
docName = getattr(doc, "documentName", "unnamed")
|
|
docType = getattr(doc, "mimeType", "unknown")
|
|
parts.append(f" - {docName} ({docType})")
|
|
docData = getattr(doc, "documentData", None)
|
|
if docData and isinstance(docData, str) and len(docData) < 2000:
|
|
parts.append(f" Content: {docData[:2000]}")
|
|
|
|
if not parts:
|
|
parts.append("Action completed successfully." if result.success else "Action failed.")
|
|
|
|
return "\n".join(parts)
|