fix:merge conflicts clickup branch

This commit is contained in:
Ida 2026-04-17 13:31:38 +02:00
commit c47529ef3b
10 changed files with 230 additions and 39 deletions

View file

@ -9,6 +9,7 @@ from modules.serviceCenter.services.serviceAgent.datamodelAgent import ToolResul
from modules.serviceCenter.services.serviceAgent.toolRegistry import ToolRegistry
from modules.serviceCenter.services.serviceAgent.coreTools._helpers import (
_buildResolverDbFromServices,
_getOrCreateTempFolder,
_looksLikeBinary,
_resolveFileScope,
@ -22,20 +23,6 @@ def _registerConnectionTools(registry: ToolRegistry, services):
"""Auto-extracted from registerCoreTools."""
# ---- Connection tools (external data sources) ----
def _buildResolverDb():
"""Build a DB adapter that ConnectorResolver can use to load UserConnections.
interfaceDbApp has getUserConnectionById; ConnectorResolver expects getUserConnection."""
chatService = services.chat
appIf = getattr(chatService, "interfaceDbApp", None)
if appIf and hasattr(appIf, "getUserConnectionById"):
class _Adapter:
def __init__(self, app):
self._app = app
def getUserConnection(self, connectionId: str):
return self._app.getUserConnectionById(connectionId)
return _Adapter(appIf)
return getattr(chatService, "interfaceDbComponent", None)
async def _listConnections(args: Dict[str, Any], context: Dict[str, Any]):
try:
chatService = services.chat
@ -49,7 +36,12 @@ def _registerConnectionTools(registry: ToolRegistry, services):
authorityVal = authority.value if hasattr(authority, "value") else str(authority)
username = conn.get("externalUsername", "") if isinstance(conn, dict) else getattr(conn, "externalUsername", "")
email = conn.get("externalEmail", "") if isinstance(conn, dict) else getattr(conn, "externalEmail", "")
lines.append(f"- connectionId: {connId} | {authorityVal} | {username} ({email})")
cid = conn.get("id", "") if isinstance(conn, dict) else getattr(conn, "id", "")
ref = f"connection:{authorityVal}:{username}"
lines.append(
f"- {ref} connectionId={cid} ({email}) "
f"(use this full connection: line or connectionId as connectionReference)"
)
return ToolResult(toolCallId="", toolName="listConnections", success=True, data="\n".join(lines))
except Exception as e:
return ToolResult(toolCallId="", toolName="listConnections", success=False, error=str(e))
@ -65,7 +57,7 @@ def _registerConnectionTools(registry: ToolRegistry, services):
from modules.connectors.connectorResolver import ConnectorResolver
resolver = ConnectorResolver(
services.getService("security"),
_buildResolverDb(),
_buildResolverDbFromServices(services),
)
adapter = await resolver.resolveService(connectionId, service)
chatService = services.chat
@ -115,7 +107,7 @@ def _registerConnectionTools(registry: ToolRegistry, services):
from modules.connectors.connectorResolver import ConnectorResolver
resolver = ConnectorResolver(
services.getService("security"),
_buildResolverDb(),
_buildResolverDbFromServices(services),
)
adapter = await resolver.resolveService(connectionId, "outlook")

View file

@ -9,6 +9,7 @@ from modules.serviceCenter.services.serviceAgent.datamodelAgent import ToolResul
from modules.serviceCenter.services.serviceAgent.toolRegistry import ToolRegistry
from modules.serviceCenter.services.serviceAgent.coreTools._helpers import (
_buildResolverDbFromServices,
_getOrCreateTempFolder,
_looksLikeBinary,
_resolveFileScope,
@ -88,7 +89,7 @@ def _registerDataSourceTools(registry: ToolRegistry, services):
from modules.connectors.connectorResolver import ConnectorResolver
resolver = ConnectorResolver(
services.getService("security"),
_buildResolverDb(),
_buildResolverDbFromServices(services),
)
adapter = await resolver.resolveService(connectionId, service)
entries = await adapter.browse(browsePath, filter=args.get("filter"))
@ -124,7 +125,7 @@ def _registerDataSourceTools(registry: ToolRegistry, services):
from modules.connectors.connectorResolver import ConnectorResolver
resolver = ConnectorResolver(
services.getService("security"),
_buildResolverDb(),
_buildResolverDbFromServices(services),
)
adapter = await resolver.resolveService(connectionId, service)
entries = await adapter.search(query, path=basePath)
@ -160,7 +161,7 @@ def _registerDataSourceTools(registry: ToolRegistry, services):
fullPath = filePath if filePath.startswith("/") else f"{basePath.rstrip('/')}/{filePath}"
resolver = ConnectorResolver(
services.getService("security"),
_buildResolverDb(),
_buildResolverDbFromServices(services),
)
adapter = await resolver.resolveService(connectionId, service)
result = await adapter.download(fullPath)

View file

@ -201,13 +201,9 @@ def _registerFeatureSubAgentTools(registry: ToolRegistry, services):
"queryFeatureInstance", _queryFeatureInstance,
description=(
"Query data from a feature instance (e.g. Trustee, CommCoach). "
"Delegates to a specialized sub-agent that knows the feature's data schema "
"and can browse, filter, and aggregate its tables. Use this when the user "
"has attached feature data sources or asks about feature-specific data.\n\n"
"GUIDELINES:\n"
"- Ask a precise, self-contained question (include all context the sub-agent needs).\n"
"- Combine related data needs into ONE call instead of multiple small ones.\n"
"- Avoid calling this tool repeatedly with slight variations of the same question."
"Delegates to a sub-agent that knows the feature schema. "
"Requires the feature instance id from attached feature data sources. "
"Ask one precise, self-contained question per call."
),
parameters={
"type": "object",

View file

@ -3,7 +3,7 @@
"""Shared helpers for core agent tools (file scope, binary detection, temp folder)."""
import logging
from typing import Optional
from typing import Any, Optional
logger = logging.getLogger(__name__)
@ -77,3 +77,23 @@ def _getOrCreateTempFolder(chatService) -> Optional[str]:
logger.warning(f"Could not get/create Temp folder: {e}")
return None
def _buildResolverDbFromServices(services: Any):
"""DB adapter for ConnectorResolver: load UserConnections by id.
interfaceDbApp exposes getUserConnectionById; ConnectorResolver expects getUserConnection.
"""
chatService = services.chat
appIf = getattr(chatService, "interfaceDbApp", None)
if appIf and hasattr(appIf, "getUserConnectionById"):
class _Adapter:
def __init__(self, app):
self._app = app
def getUserConnection(self, connectionId: str):
return self._app.getUserConnectionById(connectionId)
return _Adapter(appIf)
return getattr(chatService, "interfaceDbComponent", None)

View file

@ -3,7 +3,7 @@
"""Agent service: entry point for running AI agents with tool use."""
import logging
from typing import Any, Callable, Dict, List, Optional, AsyncGenerator
from typing import Any, Callable, Dict, List, Optional, Set, AsyncGenerator
from modules.datamodels.datamodelAi import (
AiCallRequest, AiCallOptions, AiCallResponse, OperationTypeEnum
@ -23,6 +23,40 @@ from modules.serviceCenter.services.serviceBilling.mainServiceBilling import (
logger = logging.getLogger(__name__)
def _toolbox_connection_authorities(services: "_ServicesAdapter") -> List[str]:
"""Collect connection authority strings for toolbox gating (requiresConnection).
The optional ``connection`` service is not always registered; fall back to
``chat.getUserConnections()`` (same source as workspace UI).
Toolbox entries use ``microsoft`` while UserConnection may store ``msft``.
"""
seen: Set[str] = set()
try:
conn_svc = services.getService("connection")
if conn_svc and hasattr(conn_svc, "getConnections"):
for c in conn_svc.getConnections() or []:
auth = c.get("authority") if isinstance(c, dict) else getattr(c, "authority", None)
val = auth.value if hasattr(auth, "value") else str(auth or "")
if val:
seen.add(val)
except Exception:
pass
try:
chat = services.chat
if chat and hasattr(chat, "getUserConnections"):
for c in chat.getUserConnections() or []:
auth = c.get("authority") if isinstance(c, dict) else getattr(c, "authority", None)
val = auth.value if hasattr(auth, "value") else str(auth or "")
if val:
seen.add(val)
except Exception as e:
logger.debug("toolbox authorities from chat: %s", e)
if "msft" in seen:
seen.add("microsoft")
return list(seen)
class _ServicesAdapter:
"""Adapter providing service access from (context, get_service)."""
@ -61,10 +95,33 @@ class _ServicesAdapter:
def extraction(self):
return self._getService("extraction")
@property
def rbac(self):
"""Same RbacClass as workflow hub (MethodBase permission checks during discoverMethods)."""
try:
chat_svc = self.chat
app = getattr(chat_svc, "interfaceDbApp", None)
if app is not None:
return getattr(app, "rbac", None)
except Exception:
return None
return None
def getService(self, name: str):
"""Access any service by name."""
return self._getService(name)
def __getattr__(self, name: str):
"""Resolve e.g. services.clickup for MethodClickup / ActionExecutor (discoverMethods)."""
if name.startswith("_"):
raise AttributeError(name)
try:
return self._getService(name)
except KeyError:
raise AttributeError(
f"{type(self).__name__!r} object has no attribute {name!r}"
) from None
@property
def featureCode(self) -> Optional[str]:
w = self.workflow
@ -268,7 +325,12 @@ class AgentService:
try:
from modules.workflows.processing.shared.methodDiscovery import discoverMethods
discoverMethods(self.services)
except Exception as e:
logger.warning("discoverMethods failed before action tools: %s", e)
try:
from modules.workflows.processing.core.actionExecutor import ActionExecutor
actionExecutor = ActionExecutor(self.services)
adapter = ActionToolAdapter(actionExecutor)
@ -293,7 +355,7 @@ class AgentService:
from modules.serviceCenter.services.serviceAgent.toolboxRegistry import getToolboxRegistry
tbRegistry = getToolboxRegistry()
userConnections: List[str] = []
userConnections: List[str] = _toolbox_connection_authorities(self.services)
try:
chatService = self.services.chat if hasattr(self.services, "chat") else None
if chatService and hasattr(chatService, "getUserConnections"):
@ -301,7 +363,7 @@ class AgentService:
for c in connections:
authority = c.get("authority", "") if isinstance(c, dict) else getattr(c, "authority", "")
authorityVal = authority.value if hasattr(authority, "value") else str(authority)
if authorityVal:
if authorityVal and authorityVal not in userConnections:
userConnections.append(authorityVal)
except Exception as e:
logger.debug("Could not resolve user connections for toolbox activation: %s", e)
@ -377,6 +439,7 @@ class AgentService:
activatedCount += 1
continue
try:
<<<<<<< HEAD
from modules.serviceCenter.services.serviceAgent.coreTools import registerCoreTools
registerCoreTools(registry, self.services)
if registry.isValidTool(toolName):
@ -388,6 +451,15 @@ class AgentService:
try:
from modules.serviceCenter.services.serviceAgent.actionToolAdapter import ActionToolAdapter
from modules.workflows.processing.core.actionExecutor import ActionExecutor
=======
from modules.workflows.processing.shared.methodDiscovery import discoverMethods
from modules.workflows.processing.core.actionExecutor import ActionExecutor
from modules.serviceCenter.services.serviceAgent.actionToolAdapter import (
ActionToolAdapter,
)
discoverMethods(self.services)
>>>>>>> origin/fix/click-up-connector
adapter = ActionToolAdapter(ActionExecutor(self.services))
adapter.registerAll(registry)
if registry.isValidTool(toolName):

View file

@ -173,7 +173,13 @@ def _registerDefaultToolboxes() -> None:
requiresConnection="clickup",
isDefault=False,
tools=[
"clickup_searchTasks", "clickup_createTask", "clickup_updateTask",
"clickup_listTasks",
"clickup_listFields",
"clickup_searchTasks",
"clickup_getTask",
"clickup_createTask",
"clickup_updateTask",
"clickup_uploadAttachment",
],
),
ToolboxDefinition(

View file

@ -92,6 +92,7 @@ NAVIGATION_SECTIONS = [
"icon": "FaLink",
"path": "/basedata/connections",
"order": 10,
"public": True,
},
{
"id": "files",
@ -100,6 +101,7 @@ NAVIGATION_SECTIONS = [
"icon": "FaRegFileAlt",
"path": "/basedata/files",
"order": 20,
"public": True,
},
{
"id": "prompts",
@ -108,6 +110,7 @@ NAVIGATION_SECTIONS = [
"icon": "FaLightbulb",
"path": "/basedata/prompts",
"order": 30,
"public": True,
},
],
},

View file

@ -0,0 +1,55 @@
# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
import json
import logging
from typing import Any, Dict
from modules.datamodels.datamodelChat import ActionDocument, ActionResult
from ..helpers.pathparse import parse_team_and_list
logger = logging.getLogger(__name__)
async def list_fields(self, parameters: Dict[str, Any]) -> ActionResult:
"""Return ClickUp custom / built-in field definitions for a list (GET /list/{id}/field)."""
connection_reference = parameters.get("connectionReference")
path_query = (parameters.get("pathQuery") or parameters.get("path") or "").strip()
list_id_param = (parameters.get("listId") or "").strip()
if not connection_reference:
return ActionResult.isFailure(error="connectionReference is required")
conn = self.connection.get_clickup_connection(connection_reference)
if not conn:
return ActionResult.isFailure(error="No valid ClickUp connection")
list_id = list_id_param
team_id = ""
if not list_id:
if not path_query:
return ActionResult.isFailure(
error="Provide listId or pathQuery like /team/{teamId}/list/{listId}"
)
team_id, list_id = parse_team_and_list(path_query)
if not list_id:
return ActionResult.isFailure(
error="path must be /team/{teamId}/list/{listId} (same as list picker / data source path)"
)
data = await self.services.clickup.getListFields(list_id)
if isinstance(data, dict) and data.get("error"):
return ActionResult.isFailure(error=str(data.get("error")) + (data.get("body") or ""))
doc = ActionDocument(
documentName="clickup_list_fields.json",
documentData=json.dumps(data, ensure_ascii=False, indent=2),
mimeType="application/json",
validationMetadata={
"actionType": "clickup.listFields",
"teamId": team_id,
"listId": list_id,
"path": path_query or f"/list/{list_id}",
},
)
return ActionResult.isSuccess(documents=[doc])

View file

@ -3,28 +3,44 @@
"""Resolve ClickUp UserConnection and configure ClickupService."""
import logging
import re
from typing import Any, Dict, Optional
logger = logging.getLogger(__name__)
_UUID_RE = re.compile(
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
re.IGNORECASE,
)
class ClickupConnectionHelper:
def __init__(self, method_instance: Any):
self.method = method_instance
self.services = method_instance.services
def _normalize_connection_reference(self, ref: str) -> str:
"""Match listConnections / getUserConnectionFromConnectionReference formats."""
if ref.startswith("connection:"):
return ref
if _UUID_RE.match(ref):
return ref
# LLM often copies "clickup:username" without the connection: prefix
if ":" in ref:
return f"connection:{ref}"
return ref
def get_clickup_connection(self, connection_reference: str) -> Optional[Dict[str, Any]]:
try:
ref = (connection_reference or "").split(" [")[0].strip()
if not ref:
return None
user_connection = None
if ref.startswith("connection:"):
user_connection = self.services.chat.getUserConnectionFromConnectionReference(ref)
else:
app = getattr(self.services, "interfaceDbApp", None)
if app and hasattr(app, "getUserConnectionById"):
user_connection = app.getUserConnectionById(ref)
ref = self._normalize_connection_reference(ref)
chat = getattr(self.services, "chat", None)
if not chat or not hasattr(chat, "getUserConnectionFromConnectionReference"):
logger.warning("Chat service missing; cannot resolve ClickUp connection")
return None
user_connection = chat.getUserConnectionFromConnectionReference(ref)
if not user_connection:
logger.warning("No user connection for reference/id %s", connection_reference)
return None

View file

@ -10,6 +10,7 @@ from modules.workflows.methods.methodBase import MethodBase
from .helpers.connection import ClickupConnectionHelper
from .actions.list_tasks import list_tasks
from .actions.list_fields import list_fields
from .actions.search_tasks import search_tasks
from .actions.get_task import get_task
from .actions.create_task import create_task
@ -67,6 +68,35 @@ class MethodClickup(MethodBase):
},
execute=list_tasks.__get__(self, self.__class__),
),
"listFields": WorkflowActionDefinition(
actionId="clickup.listFields",
description="List custom and built-in field definitions for a ClickUp list (names, types, ids)",
dynamicMode=True,
parameters={
"connectionReference": WorkflowActionParameter(
name="connectionReference",
type="str",
frontendType=FrontendType.USER_CONNECTION,
required=True,
description="ClickUp connection",
),
"listId": WorkflowActionParameter(
name="listId",
type="str",
frontendType=FrontendType.TEXT,
required=False,
description="ClickUp list ID (if set, pathQuery is optional)",
),
"pathQuery": WorkflowActionParameter(
name="pathQuery",
type="str",
frontendType=FrontendType.TEXT,
required=False,
description="Virtual path /team/{teamId}/list/{listId} (same as data source path)",
),
},
execute=list_fields.__get__(self, self.__class__),
),
"searchTasks": WorkflowActionDefinition(
actionId="clickup.searchTasks",
description="Search tasks in a ClickUp workspace (team)",