fix:merge conflicts clickup branch
This commit is contained in:
commit
c47529ef3b
10 changed files with 230 additions and 39 deletions
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)",
|
||||
|
|
|
|||
Loading…
Reference in a new issue