gateway/modules/serviceCenter/services/serviceAgent/toolboxRegistry.py
2026-04-20 00:31:05 +02:00

255 lines
9.5 KiB
Python

# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
Toolbox Registry for the Agent service.
Manages thematic tool groupings (toolboxes) and the `requestToolbox` meta-tool.
"""
import logging
from typing import Dict, List, Any, Optional, Set
from pydantic import BaseModel, Field
logger = logging.getLogger(__name__)
class ToolboxDefinition(BaseModel):
"""Definition of a thematic toolbox."""
id: str = Field(description="Unique toolbox identifier (e.g. 'core', 'email', 'workflow')")
label: str = Field(description="Human-readable label")
description: str = Field(default="", description="What this toolbox provides")
featureCode: Optional[str] = Field(default=None, description="Feature code if toolbox is feature-specific")
tools: List[str] = Field(default_factory=list, description="Tool names belonging to this toolbox")
isDefault: bool = Field(default=False, description="If true, toolbox is active by default")
requiresConnection: Optional[str] = Field(
default=None,
description="Connection authority required (e.g. 'microsoft', 'google'). None = always available."
)
class ToolboxRegistry:
"""Registry for toolbox definitions. Manages activation and tool lookup."""
def __init__(self):
self._toolboxes: Dict[str, ToolboxDefinition] = {}
def registerToolbox(self, toolbox: ToolboxDefinition) -> None:
"""Register a toolbox definition."""
if toolbox.id in self._toolboxes:
logger.debug("Toolbox '%s' already registered, updating", toolbox.id)
self._toolboxes[toolbox.id] = toolbox
logger.debug("Registered toolbox: %s (%d tools, default=%s)", toolbox.id, len(toolbox.tools), toolbox.isDefault)
def getToolbox(self, toolboxId: str) -> Optional[ToolboxDefinition]:
"""Get a toolbox by ID."""
return self._toolboxes.get(toolboxId)
def getAllToolboxes(self) -> List[ToolboxDefinition]:
"""Get all registered toolboxes."""
return list(self._toolboxes.values())
def getDefaultToolboxes(self) -> List[ToolboxDefinition]:
"""Get all default toolboxes (active at agent start)."""
return [tb for tb in self._toolboxes.values() if tb.isDefault]
def getActiveToolboxes(self, userConnections: List[str] = None) -> List[ToolboxDefinition]:
"""
Get toolboxes available to the user based on their connections.
Toolboxes without requiresConnection are always available.
Toolboxes with requiresConnection are available only if the user has that connection.
"""
available = []
connectionAuthorities: Set[str] = set(userConnections or [])
for tb in self._toolboxes.values():
if tb.requiresConnection is None:
available.append(tb)
elif tb.requiresConnection in connectionAuthorities:
available.append(tb)
return available
def getToolsForToolboxes(self, toolboxIds: List[str]) -> List[str]:
"""Get the union of all tool names for the given toolbox IDs."""
tools: Set[str] = set()
for tbId in toolboxIds:
tb = self._toolboxes.get(tbId)
if tb:
tools.update(tb.tools)
return sorted(tools)
def getToolboxForTool(self, toolName: str) -> Optional[str]:
"""Find which toolbox a tool belongs to."""
for tb in self._toolboxes.values():
if toolName in tb.tools:
return tb.id
return None
def toApiResponse(self, userConnections: List[str] = None) -> List[Dict[str, Any]]:
"""Serialize available toolboxes for API response."""
available = self.getActiveToolboxes(userConnections)
return [
{
"id": tb.id,
"label": tb.label,
"description": tb.description,
"toolCount": len(tb.tools),
"isDefault": tb.isDefault,
"requiresConnection": tb.requiresConnection,
}
for tb in available
]
# Module-level singleton
_toolboxRegistry = ToolboxRegistry()
def getToolboxRegistry() -> ToolboxRegistry:
"""Get the global toolbox registry singleton."""
return _toolboxRegistry
def _registerDefaultToolboxes() -> None:
"""Register the default set of toolboxes."""
defaults = [
ToolboxDefinition(
id="core",
label="Core Tools",
description="Basic agent tools: search, read, write, web",
isDefault=True,
tools=[
"readFile", "listFiles", "searchInFileContent", "listFolders",
"webSearch", "readUrl", "writeFile", "deleteFile", "renameFile",
"copyFile", "createFolder", "deleteFolder", "moveFile", "moveFolder",
"renameFolder", "tagFile", "replaceInFile", "translateText",
"detectLanguage", "queryFeatureInstance",
],
),
ToolboxDefinition(
id="ai",
label="AI Tools",
description="AI-powered analysis and generation",
isDefault=True,
tools=[
"summarizeContent", "describeImage", "generateImage",
"textToSpeech", "speechToText", "renderDocument",
"createChart", "executeCode", "neutralizeData",
],
),
ToolboxDefinition(
id="datasources",
label="Data Sources",
description="Access external data sources and databases",
isDefault=True,
tools=[
"listConnections", "browseDataSource", "searchDataSource",
"downloadFromDataSource", "uploadToExternal",
"browseContainer", "readContentObjects", "extractContainerItem",
],
),
ToolboxDefinition(
id="email",
label="Email",
description="Send emails or save as draft via Outlook (supports HTML body and file attachments). Use sendMail with draft=true for drafts.",
requiresConnection="msft",
isDefault=False,
tools=[
"sendMail",
],
),
ToolboxDefinition(
id="sharepoint",
label="SharePoint",
description="Access SharePoint sites, lists, and files",
requiresConnection="msft",
isDefault=False,
tools=[
"sharepoint_findDocuments", "sharepoint_readDocuments",
"sharepoint_upload",
],
),
ToolboxDefinition(
id="clickup",
label="ClickUp",
description="Manage ClickUp tasks and projects",
requiresConnection="clickup",
isDefault=False,
tools=[
"clickup_listTasks",
"clickup_listFields",
"clickup_searchTasks",
"clickup_getTask",
"clickup_createTask",
"clickup_updateTask",
"clickup_uploadAttachment",
],
),
ToolboxDefinition(
id="jira",
label="Jira",
description="Manage Jira issues and projects",
requiresConnection="jira",
isDefault=False,
tools=[
"jira_connect", "jira_exportTickets", "jira_importTickets",
],
),
ToolboxDefinition(
id="workflow",
label="Workflow",
description="Graph manipulation tools for the visual editor",
featureCode="graphicalEditor",
isDefault=False,
tools=[
"readWorkflowGraph", "addNode", "removeNode", "connectNodes",
"setNodeParameter", "listAvailableNodeTypes", "describeNodeType",
"autoLayoutWorkflow", "validateGraph",
"listWorkflowHistory", "readWorkflowMessages",
"createWorkflow", "updateWorkflowMetadata", "createWorkflowFromFile",
"exportWorkflowToFile", "deleteWorkflow",
],
),
ToolboxDefinition(
id="trustee",
label="Trustee / Accounting",
description="Trustee accounting tools: refresh data from external system (e.g. Abacus), query positions and journal entries",
featureCode="trustee",
isDefault=False,
tools=[
"trustee_refreshAccountingData",
],
),
]
for tb in defaults:
_toolboxRegistry.registerToolbox(tb)
_registerDefaultToolboxes()
REQUEST_TOOLBOX_TOOL_NAME = "requestToolbox"
def buildRequestToolboxDefinition(availableToolboxIds: List[str]) -> dict:
"""Build the tool definition dict for the requestToolbox meta-tool."""
return {
"name": REQUEST_TOOLBOX_TOOL_NAME,
"description": (
"Request additional specialized tools for the current task. "
"Call this when you need tools from a specific toolbox that is not yet active. "
"After calling, the requested tools will be available in the next round."
),
"parameters": {
"type": "object",
"properties": {
"toolboxId": {
"type": "string",
"enum": availableToolboxIds,
"description": "ID of the toolbox to activate",
},
"reason": {
"type": "string",
"description": "Brief reason why this toolbox is needed",
},
},
"required": ["toolboxId"],
},
}