mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-06 20:15:17 +02:00
refactor(agents): split tool registry into pure-data catalog, decouple connectors
Replace the connector-coupled BUILTIN_TOOLS registry with a pure-data catalog so shared/tools no longer imports any connector module, making the connector packages independently deletable. - add shared/tools/catalog.py (ToolMetadata + TOOL_CATALOG, 41 tools, no imports) - point GET /agent/tools (the only live consumer) at the catalog - relocate ToolDefinition into action_log middleware (its sole consumer); drop the inert tool_definitions wiring (no tool defines reverse) - delete shared/tools/registry.py: connector imports, dead factories, dead get_connector_gated_tools, and BUILTIN_TOOLS - drop stale dedup-propagation test (path removed in C1) + refresh docstrings import-all guardrail + agents unit suite green (987 passed).
This commit is contained in:
parent
c3238d8840
commit
003924062d
13 changed files with 154 additions and 826 deletions
|
|
@ -6,7 +6,6 @@ import logging
|
|||
|
||||
from app.agents.shared.feature_flags import AgentFeatureFlags
|
||||
from app.agents.shared.middleware import ActionLogMiddleware
|
||||
from app.agents.shared.tools.registry import BUILTIN_TOOLS
|
||||
|
||||
from app.agents.multi_agent_chat.shared.middleware.flags import enabled
|
||||
|
||||
|
|
@ -21,12 +20,13 @@ def build_action_log_mw(
|
|||
if not enabled(flags, "enable_action_log") or thread_id is None:
|
||||
return None
|
||||
try:
|
||||
tool_defs_by_name = {td.name: td for td in BUILTIN_TOOLS}
|
||||
# No built-in tool declares a ``reverse`` callable yet, so the action
|
||||
# log runs without a tool_definitions map. Reversibility is opt-in per
|
||||
# tool via ``ToolDefinition.reverse`` and can be wired here when used.
|
||||
return ActionLogMiddleware(
|
||||
thread_id=thread_id,
|
||||
search_space_id=search_space_id,
|
||||
user_id=user_id,
|
||||
tool_definitions=tool_defs_by_name,
|
||||
)
|
||||
except Exception: # pragma: no cover - defensive
|
||||
logging.warning(
|
||||
|
|
|
|||
|
|
@ -5,15 +5,13 @@ connector integrations, MCP, and deliverables are delegated to ``task``
|
|||
subagents (see :mod:`app.agents.multi_agent_chat.main_agent.tools.index`).
|
||||
|
||||
This module is the *building* counterpart to that name list: it owns the
|
||||
factories for those few tools and nothing else. It is deliberately decoupled
|
||||
from :mod:`app.agents.shared.tools.registry` (the app-wide ``BUILTIN_TOOLS``
|
||||
metadata catalog, which imports every connector) so the main agent's tool
|
||||
factories for those few tools and nothing else, so the main agent's tool
|
||||
surface stays self-contained and connector-free.
|
||||
|
||||
The ``BUILTIN_TOOLS`` catalog still exists and is still used elsewhere for
|
||||
tool *metadata* — the ``/agent/tools`` listing endpoint and the action-log
|
||||
revert/dedup resolvers (which must cover subagent-executed connector tools).
|
||||
This registry only governs what the main agent actually builds and binds.
|
||||
Tool *display* metadata for the whole app (the ``/agent/tools`` listing
|
||||
endpoint) lives separately in :mod:`app.agents.shared.tools.catalog`, a
|
||||
pure-data module that imports no connectors. This registry only governs what
|
||||
the main agent actually builds and binds.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -71,8 +69,7 @@ def _build_update_memory_tool(deps: dict[str, Any]) -> BaseTool:
|
|||
)
|
||||
|
||||
|
||||
# Ordered to match the historical binding order produced by the shared
|
||||
# ``build_tools`` (which iterated ``BUILTIN_TOOLS`` in declaration order):
|
||||
# Ordered to match the historical main-agent binding order:
|
||||
# scrape_webpage, web_search, create_automation, update_memory.
|
||||
# Each entry is ``(factory, required_dependency_names)``.
|
||||
_MAIN_AGENT_TOOL_FACTORIES: dict[
|
||||
|
|
|
|||
|
|
@ -113,12 +113,11 @@ def tools_signature(
|
|||
MCP tools loaded for the user changes, gating rules flip, etc.).
|
||||
* The available connectors / document types for the search space
|
||||
change (new connector added, last connector removed, new document
|
||||
type indexed). Because :func:`get_connector_gated_tools` derives
|
||||
``modified_disabled_tools`` from ``available_connectors``, the
|
||||
tool surface is technically already covered — but we hash the
|
||||
connector list separately so an empty-list "no tools changed"
|
||||
situation still rotates the key when, say, the user re-adds a
|
||||
connector that gates a tool we were already not exposing.
|
||||
type indexed). Connector gating derives disabled tools from
|
||||
``available_connectors``, so the tool surface is technically already
|
||||
covered — but we hash the connector list separately so an empty-list
|
||||
"no tools changed" situation still rotates the key when, say, the user
|
||||
re-adds a connector that gates a tool we were already not exposing.
|
||||
|
||||
Stays stable across:
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
"""Middleware components for the SurfSense new chat agent."""
|
||||
|
||||
from app.agents.shared.middleware.action_log import ActionLogMiddleware
|
||||
from app.agents.shared.middleware.action_log import (
|
||||
ActionLogMiddleware,
|
||||
ToolDefinition,
|
||||
)
|
||||
from app.agents.shared.middleware.anonymous_document import (
|
||||
AnonymousDocumentMiddleware,
|
||||
)
|
||||
|
|
@ -76,6 +79,7 @@ __all__ = [
|
|||
"SpillingContextEditingMiddleware",
|
||||
"SurfSenseCompactionMiddleware",
|
||||
"ToolCallNameRepairMiddleware",
|
||||
"ToolDefinition",
|
||||
"build_skills_backend_factory",
|
||||
"commit_staged_filesystem_state",
|
||||
"create_surfsense_compaction_middleware",
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
Wraps every tool call via :meth:`AgentMiddleware.awrap_tool_call` and writes
|
||||
a row to :class:`~app.db.AgentActionLog` after the tool returns. Tools opt
|
||||
into reversibility by declaring a ``reverse`` callable on their
|
||||
:class:`~app.agents.shared.tools.registry.ToolDefinition`; the rendered
|
||||
descriptor is persisted in ``reverse_descriptor`` for use by
|
||||
:class:`ToolDefinition`; the rendered descriptor is persisted in
|
||||
``reverse_descriptor`` for use by
|
||||
``/api/threads/{thread_id}/revert/{action_id}``.
|
||||
|
||||
Design points:
|
||||
|
|
@ -27,6 +27,7 @@ from __future__ import annotations
|
|||
import json
|
||||
import logging
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from langchain.agents.middleware import AgentMiddleware
|
||||
|
|
@ -39,15 +40,35 @@ if TYPE_CHECKING: # pragma: no cover - type-only
|
|||
from langchain.agents.middleware.types import ToolCallRequest
|
||||
from langgraph.types import Command
|
||||
|
||||
# Type-only import: ToolDefinition is only referenced in annotations, and a
|
||||
# runtime import would close a module-load cycle (tools.registry imports
|
||||
# shared.middleware.dedup_tool_calls).
|
||||
from app.agents.shared.tools.registry import ToolDefinition
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolDefinition:
|
||||
"""Reversibility descriptor consumed by :class:`ActionLogMiddleware`.
|
||||
|
||||
Only ``name`` and ``reverse`` are read by the middleware; the remaining
|
||||
fields let callers and tests describe a tool declaratively. A tool is
|
||||
marked reversible in the action log when ``reverse`` is set and renders a
|
||||
descriptor without raising.
|
||||
|
||||
Attributes:
|
||||
name: Unique identifier for the tool.
|
||||
description: Human-readable description of what the tool does.
|
||||
factory: Optional callable that builds the tool (unused by the
|
||||
middleware; retained for declarative call sites/tests).
|
||||
reverse: Optional callable that, given the tool's ``(args, result)``,
|
||||
returns a ``ReverseDescriptor`` describing the inverse invocation.
|
||||
|
||||
"""
|
||||
|
||||
name: str
|
||||
description: str = ""
|
||||
factory: Callable[[dict[str, Any]], Any] | None = None
|
||||
reverse: Callable[[dict[str, Any], Any], dict[str, Any]] | None = None
|
||||
|
||||
|
||||
# Cap for the persisted ``args`` JSON to avoid bloating the action log with
|
||||
# accidentally-huge inputs. Values are truncated and a flag is set in the
|
||||
# stored payload so consumers can detect truncation.
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@ the duplicate call is stripped from the AIMessage that gets checkpointed.
|
|||
That means it is also safe across LangGraph ``interrupt()`` boundaries:
|
||||
the removed call will never appear on graph resume.
|
||||
|
||||
Dedup-key resolution order:
|
||||
Dedup-key resolution order (read from each tool's own ``metadata``):
|
||||
|
||||
1. :class:`ToolDefinition.dedup_key` — callable provided by the registry
|
||||
entry. This is the canonical mechanism.
|
||||
2. ``tool.metadata["hitl_dedup_key"]`` — string with a primary arg name;
|
||||
used by MCP / Composio tools whose schemas the registry doesn't see.
|
||||
1. ``tool.metadata["dedup_key"]`` — callable mapping the args dict to a
|
||||
stable signature string. This is the canonical mechanism.
|
||||
2. ``tool.metadata["hitl_dedup_key"]`` — string naming a primary arg;
|
||||
used by MCP / Composio tools that only expose a single key field.
|
||||
|
||||
A tool with no resolver from either path simply opts out of dedup.
|
||||
"""
|
||||
|
|
@ -39,17 +39,10 @@ DedupResolver = Callable[[dict[str, Any]], str]
|
|||
def wrap_dedup_key_by_arg_name(arg_name: str) -> DedupResolver:
|
||||
"""Adapt a string-arg name into a :data:`DedupResolver`.
|
||||
|
||||
Convenience helper used by registry entries that just want to dedupe
|
||||
on a single arg's lowercased value (the most common case for native
|
||||
HITL tools like ``send_gmail_email`` keyed on ``subject``).
|
||||
|
||||
Example::
|
||||
|
||||
ToolDefinition(
|
||||
name="send_gmail_email",
|
||||
...,
|
||||
dedup_key=wrap_dedup_key_by_arg_name("subject"),
|
||||
)
|
||||
Convenience helper for tools that just want to dedupe on a single arg's
|
||||
lowercased value (the most common case for HITL tools like
|
||||
``send_gmail_email`` keyed on ``subject``). Set the result on the tool's
|
||||
``metadata["dedup_key"]``.
|
||||
"""
|
||||
|
||||
def _resolver(args: dict[str, Any]) -> str:
|
||||
|
|
@ -84,9 +77,8 @@ class DedupHITLToolCallsMiddleware(AgentMiddleware): # type: ignore[type-arg]
|
|||
|
||||
The dedup-resolver map is built from two sources, in priority order:
|
||||
|
||||
1. ``tool.metadata["dedup_key"]`` — callable provided by the registry's
|
||||
``ToolDefinition.dedup_key``. Receives the args dict and returns
|
||||
a string signature. This is the canonical mechanism.
|
||||
1. ``tool.metadata["dedup_key"]`` — callable that receives the args dict
|
||||
and returns a string signature. This is the canonical mechanism.
|
||||
2. ``tool.metadata["hitl_dedup_key"]`` — string with a primary arg
|
||||
name; primarily used by MCP / Composio tools.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -118,9 +118,8 @@ class ToolCallNameRepairMiddleware(
|
|||
return call
|
||||
|
||||
# Stage 2 — invalid fallback
|
||||
# Local import avoids a module-load cycle: tools.registry imports
|
||||
# shared.middleware (dedup_tool_calls), so importing tools at module
|
||||
# scope here would close the loop.
|
||||
# Local import keeps the middleware module import-light and avoids any
|
||||
# tools <-> middleware import-order coupling at module scope.
|
||||
from app.agents.shared.tools.invalid_tool import INVALID_TOOL_NAME
|
||||
|
||||
if INVALID_TOOL_NAME in registered:
|
||||
|
|
|
|||
|
|
@ -18,19 +18,16 @@ from .knowledge_base import (
|
|||
format_documents_for_context,
|
||||
search_knowledge_base_async,
|
||||
)
|
||||
from .catalog import TOOL_CATALOG, ToolMetadata
|
||||
from .podcast import create_generate_podcast_tool
|
||||
from .registry import (
|
||||
BUILTIN_TOOLS,
|
||||
ToolDefinition,
|
||||
)
|
||||
from .video_presentation import create_generate_video_presentation_tool
|
||||
|
||||
__all__ = [
|
||||
# Registry
|
||||
"BUILTIN_TOOLS",
|
||||
# Tool catalog (display metadata)
|
||||
"TOOL_CATALOG",
|
||||
# Knowledge base utilities
|
||||
"CONNECTOR_DESCRIPTIONS",
|
||||
"ToolDefinition",
|
||||
"ToolMetadata",
|
||||
# Tool factories
|
||||
"create_generate_image_tool",
|
||||
"create_generate_podcast_tool",
|
||||
|
|
|
|||
84
surfsense_backend/app/agents/shared/tools/catalog.py
Normal file
84
surfsense_backend/app/agents/shared/tools/catalog.py
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
"""Pure-data catalog of built-in agent tools.
|
||||
|
||||
This module advertises *what* tools exist and their display metadata. It is
|
||||
intentionally free of any tool implementation imports (no connectors, no
|
||||
factories) so it can be consumed without pulling the whole tool dependency
|
||||
graph — and so connector packages stay independently deletable.
|
||||
|
||||
The single live consumer is the ``GET /agent/tools`` endpoint, which renders
|
||||
the tool picker in the web UI. Tool *construction* lives elsewhere:
|
||||
|
||||
* main-agent tools -> ``app.agents.multi_agent_chat.main_agent.tools.registry``
|
||||
* subagent / connector tools -> ``app.agents.multi_agent_chat.subagents.*``
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ToolMetadata:
|
||||
"""Display metadata for a single built-in tool.
|
||||
|
||||
Attributes:
|
||||
name: Unique identifier for the tool.
|
||||
description: Human-readable description of what the tool does.
|
||||
enabled_by_default: Whether the tool is on when no explicit config
|
||||
is provided.
|
||||
hidden: WIP tools that should be excluded from public listings.
|
||||
|
||||
"""
|
||||
|
||||
name: str
|
||||
description: str
|
||||
enabled_by_default: bool = True
|
||||
hidden: bool = False
|
||||
|
||||
|
||||
# Catalog of all built-in tools. Contributors: add new tools here so they show
|
||||
# up in the UI tool picker. This list carries metadata only — wire the actual
|
||||
# implementation in the relevant builder/registry module.
|
||||
TOOL_CATALOG: list[ToolMetadata] = [
|
||||
ToolMetadata(name="generate_podcast", description="Generate an audio podcast from provided content"),
|
||||
ToolMetadata(name="generate_video_presentation", description="Generate a video presentation with slides and narration from provided content"),
|
||||
ToolMetadata(name="generate_report", description="Generate a structured report from provided content and export it"),
|
||||
ToolMetadata(name="generate_resume", description="Generate a professional resume as a Typst document"),
|
||||
ToolMetadata(name="generate_image", description="Generate images from text descriptions using AI image models"),
|
||||
ToolMetadata(name="scrape_webpage", description="Scrape and extract the main content from a webpage"),
|
||||
ToolMetadata(name="web_search", description="Search the web for real-time information using configured search engines"),
|
||||
ToolMetadata(name="get_connected_accounts", description="Discover connected accounts for a service and their metadata"),
|
||||
ToolMetadata(name="create_automation", description="Draft an automation from an NL intent; user approves the card; tool saves"),
|
||||
ToolMetadata(name="update_memory", description="Save important long-term facts, preferences, and instructions to the (personal or team) memory"),
|
||||
ToolMetadata(name="create_notion_page", description="Create a new page in the user's Notion workspace"),
|
||||
ToolMetadata(name="update_notion_page", description="Append new content to an existing Notion page"),
|
||||
ToolMetadata(name="delete_notion_page", description="Delete an existing Notion page"),
|
||||
ToolMetadata(name="create_google_drive_file", description="Create a new Google Doc or Google Sheet in Google Drive"),
|
||||
ToolMetadata(name="delete_google_drive_file", description="Move an indexed Google Drive file to trash"),
|
||||
ToolMetadata(name="create_dropbox_file", description="Create a new file in Dropbox"),
|
||||
ToolMetadata(name="delete_dropbox_file", description="Delete a file from Dropbox"),
|
||||
ToolMetadata(name="create_onedrive_file", description="Create a new file in Microsoft OneDrive"),
|
||||
ToolMetadata(name="delete_onedrive_file", description="Move a OneDrive file to the recycle bin"),
|
||||
ToolMetadata(name="search_calendar_events", description="Search Google Calendar events within a date range"),
|
||||
ToolMetadata(name="create_calendar_event", description="Create a new event on Google Calendar"),
|
||||
ToolMetadata(name="update_calendar_event", description="Update an existing indexed Google Calendar event"),
|
||||
ToolMetadata(name="delete_calendar_event", description="Delete an existing indexed Google Calendar event"),
|
||||
ToolMetadata(name="search_gmail", description="Search emails in Gmail using Gmail search syntax"),
|
||||
ToolMetadata(name="read_gmail_email", description="Read the full content of a specific Gmail email"),
|
||||
ToolMetadata(name="create_gmail_draft", description="Create a draft email in Gmail"),
|
||||
ToolMetadata(name="send_gmail_email", description="Send an email via Gmail"),
|
||||
ToolMetadata(name="trash_gmail_email", description="Move an indexed email to trash in Gmail"),
|
||||
ToolMetadata(name="update_gmail_draft", description="Update an existing Gmail draft"),
|
||||
ToolMetadata(name="create_confluence_page", description="Create a new page in the user's Confluence space"),
|
||||
ToolMetadata(name="update_confluence_page", description="Update an existing indexed Confluence page"),
|
||||
ToolMetadata(name="delete_confluence_page", description="Delete an existing indexed Confluence page"),
|
||||
ToolMetadata(name="list_discord_channels", description="List text channels in the connected Discord server"),
|
||||
ToolMetadata(name="read_discord_messages", description="Read recent messages from a Discord text channel"),
|
||||
ToolMetadata(name="send_discord_message", description="Send a message to a Discord text channel"),
|
||||
ToolMetadata(name="list_teams_channels", description="List Microsoft Teams and their channels"),
|
||||
ToolMetadata(name="read_teams_messages", description="Read recent messages from a Microsoft Teams channel"),
|
||||
ToolMetadata(name="send_teams_message", description="Send a message to a Microsoft Teams channel"),
|
||||
ToolMetadata(name="list_luma_events", description="List upcoming and recent Luma events"),
|
||||
ToolMetadata(name="read_luma_event", description="Read detailed information about a specific Luma event"),
|
||||
ToolMetadata(name="create_luma_event", description="Create a new event on Luma"),
|
||||
]
|
||||
|
|
@ -1,743 +0,0 @@
|
|||
"""Tools registry for SurfSense deep agent.
|
||||
|
||||
This module provides a registry pattern for managing tools in the SurfSense agent.
|
||||
It makes it easy for OSS contributors to add new tools by:
|
||||
1. Creating a tool factory function in a new file in this directory
|
||||
2. Registering the tool in the BUILTIN_TOOLS list below
|
||||
|
||||
Example of adding a new tool:
|
||||
------------------------------
|
||||
1. Create your tool file (e.g., `tools/my_tool.py`):
|
||||
|
||||
from langchain_core.tools import tool
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
def create_my_tool(search_space_id: int, db_session: AsyncSession):
|
||||
@tool
|
||||
async def my_tool(param: str) -> dict:
|
||||
'''My tool description.'''
|
||||
# Your implementation
|
||||
return {"result": "success"}
|
||||
return my_tool
|
||||
|
||||
2. Import and register in this file:
|
||||
|
||||
from .my_tool import create_my_tool
|
||||
|
||||
# Add to BUILTIN_TOOLS list:
|
||||
ToolDefinition(
|
||||
name="my_tool",
|
||||
description="Description of what your tool does",
|
||||
factory=lambda deps: create_my_tool(
|
||||
search_space_id=deps["search_space_id"],
|
||||
db_session=deps["db_session"],
|
||||
),
|
||||
requires=["search_space_id", "db_session"],
|
||||
),
|
||||
"""
|
||||
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.tools import BaseTool
|
||||
|
||||
from app.agents.shared.middleware.dedup_tool_calls import (
|
||||
wrap_dedup_key_by_arg_name,
|
||||
)
|
||||
from app.db import ChatVisibility
|
||||
|
||||
from .confluence import (
|
||||
create_create_confluence_page_tool,
|
||||
create_delete_confluence_page_tool,
|
||||
create_update_confluence_page_tool,
|
||||
)
|
||||
from .connected_accounts import create_get_connected_accounts_tool
|
||||
from .discord import (
|
||||
create_list_discord_channels_tool,
|
||||
create_read_discord_messages_tool,
|
||||
create_send_discord_message_tool,
|
||||
)
|
||||
from .dropbox import (
|
||||
create_create_dropbox_file_tool,
|
||||
create_delete_dropbox_file_tool,
|
||||
)
|
||||
from .generate_image import create_generate_image_tool
|
||||
from .gmail import (
|
||||
create_create_gmail_draft_tool,
|
||||
create_read_gmail_email_tool,
|
||||
create_search_gmail_tool,
|
||||
create_send_gmail_email_tool,
|
||||
create_trash_gmail_email_tool,
|
||||
create_update_gmail_draft_tool,
|
||||
)
|
||||
from .google_calendar import (
|
||||
create_create_calendar_event_tool,
|
||||
create_delete_calendar_event_tool,
|
||||
create_search_calendar_events_tool,
|
||||
create_update_calendar_event_tool,
|
||||
)
|
||||
from .google_drive import (
|
||||
create_create_google_drive_file_tool,
|
||||
create_delete_google_drive_file_tool,
|
||||
)
|
||||
from .luma import (
|
||||
create_create_luma_event_tool,
|
||||
create_list_luma_events_tool,
|
||||
create_read_luma_event_tool,
|
||||
)
|
||||
from .notion import (
|
||||
create_create_notion_page_tool,
|
||||
create_delete_notion_page_tool,
|
||||
create_update_notion_page_tool,
|
||||
)
|
||||
from .onedrive import (
|
||||
create_create_onedrive_file_tool,
|
||||
create_delete_onedrive_file_tool,
|
||||
)
|
||||
from .podcast import create_generate_podcast_tool
|
||||
from .report import create_generate_report_tool
|
||||
from .resume import create_generate_resume_tool
|
||||
from .teams import (
|
||||
create_list_teams_channels_tool,
|
||||
create_read_teams_messages_tool,
|
||||
create_send_teams_message_tool,
|
||||
)
|
||||
from .video_presentation import create_generate_video_presentation_tool
|
||||
from .web_search import create_web_search_tool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# =============================================================================
|
||||
# Tool Definition
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolDefinition:
|
||||
"""Definition of a tool that can be added to the agent.
|
||||
|
||||
Attributes:
|
||||
name: Unique identifier for the tool
|
||||
description: Human-readable description of what the tool does
|
||||
factory: Callable that creates the tool. Receives a dict of dependencies.
|
||||
requires: List of dependency names this tool needs (e.g., "search_space_id", "db_session")
|
||||
enabled_by_default: Whether the tool is enabled when no explicit config is provided
|
||||
required_connector: Searchable type string (e.g. ``"LINEAR_CONNECTOR"``)
|
||||
that must be in ``available_connectors`` for the tool to be enabled.
|
||||
dedup_key: Optional callable that maps a tool's ``args`` dict to a
|
||||
string signature used by :class:`DedupHITLToolCallsMiddleware`
|
||||
to drop duplicate calls within a single LLM response.
|
||||
reverse: Optional callable that, given the tool's ``(args, result)``,
|
||||
returns a ``ReverseDescriptor`` describing the inverse tool
|
||||
invocation. Consumed by the snapshot/revert pipeline.
|
||||
|
||||
"""
|
||||
|
||||
name: str
|
||||
description: str
|
||||
factory: Callable[[dict[str, Any]], BaseTool]
|
||||
requires: list[str] = field(default_factory=list)
|
||||
enabled_by_default: bool = True
|
||||
hidden: bool = False
|
||||
required_connector: str | None = None
|
||||
dedup_key: Callable[[dict[str, Any]], str] | None = None
|
||||
reverse: Callable[[dict[str, Any], Any], dict[str, Any]] | None = None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Deferred-import factories
|
||||
# =============================================================================
|
||||
# Used for tools whose impls live under ``multi_agent_chat``. Importing those
|
||||
# at module-load time would cycle (``multi_agent_chat`` middleware imports
|
||||
# this registry). The import inside the factory runs only when
|
||||
# ``build_tools`` is called, by which point ``multi_agent_chat`` is fully
|
||||
# initialised.
|
||||
|
||||
|
||||
def _build_create_automation_tool(deps: dict[str, Any]) -> BaseTool:
|
||||
from app.agents.multi_agent_chat.main_agent.tools.automation import (
|
||||
create_create_automation_tool,
|
||||
)
|
||||
|
||||
return create_create_automation_tool(
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
llm=deps["llm"],
|
||||
)
|
||||
|
||||
|
||||
def _build_scrape_webpage_tool(deps: dict[str, Any]) -> BaseTool:
|
||||
# ``scrape_webpage`` is owned by the main agent (its sole live consumer);
|
||||
# deferred import keeps this catalog free of a ``multi_agent_chat`` cycle.
|
||||
from app.agents.multi_agent_chat.main_agent.tools.scrape_webpage import (
|
||||
create_scrape_webpage_tool,
|
||||
)
|
||||
|
||||
return create_scrape_webpage_tool(firecrawl_api_key=deps.get("firecrawl_api_key"))
|
||||
|
||||
|
||||
def _build_update_memory_tool(deps: dict[str, Any]) -> BaseTool:
|
||||
# ``update_memory`` is owned by the main agent; deferred import (see above).
|
||||
from app.agents.multi_agent_chat.main_agent.tools.update_memory import (
|
||||
create_update_memory_tool,
|
||||
create_update_team_memory_tool,
|
||||
)
|
||||
|
||||
if deps["thread_visibility"] == ChatVisibility.SEARCH_SPACE:
|
||||
return create_update_team_memory_tool(
|
||||
search_space_id=deps["search_space_id"],
|
||||
db_session=deps["db_session"],
|
||||
llm=deps.get("llm"),
|
||||
)
|
||||
return create_update_memory_tool(
|
||||
user_id=deps["user_id"],
|
||||
db_session=deps["db_session"],
|
||||
llm=deps.get("llm"),
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Built-in Tools Registry
|
||||
# =============================================================================
|
||||
|
||||
# Registry of all built-in tools
|
||||
# Contributors: Add your new tools here!
|
||||
BUILTIN_TOOLS: list[ToolDefinition] = [
|
||||
# Podcast generation tool
|
||||
ToolDefinition(
|
||||
name="generate_podcast",
|
||||
description="Generate an audio podcast from provided content",
|
||||
factory=lambda deps: create_generate_podcast_tool(
|
||||
search_space_id=deps["search_space_id"],
|
||||
db_session=deps["db_session"],
|
||||
thread_id=deps["thread_id"],
|
||||
),
|
||||
requires=["search_space_id", "db_session", "thread_id"],
|
||||
),
|
||||
# Video presentation generation tool
|
||||
ToolDefinition(
|
||||
name="generate_video_presentation",
|
||||
description="Generate a video presentation with slides and narration from provided content",
|
||||
factory=lambda deps: create_generate_video_presentation_tool(
|
||||
search_space_id=deps["search_space_id"],
|
||||
db_session=deps["db_session"],
|
||||
thread_id=deps["thread_id"],
|
||||
),
|
||||
requires=["search_space_id", "db_session", "thread_id"],
|
||||
),
|
||||
# Report generation tool (inline, short-lived sessions for DB ops)
|
||||
# Supports internal KB search via source_strategy so the agent does not
|
||||
# need a separate search step before generating.
|
||||
ToolDefinition(
|
||||
name="generate_report",
|
||||
description="Generate a structured report from provided content and export it",
|
||||
factory=lambda deps: create_generate_report_tool(
|
||||
search_space_id=deps["search_space_id"],
|
||||
thread_id=deps["thread_id"],
|
||||
connector_service=deps.get("connector_service"),
|
||||
available_connectors=deps.get("available_connectors"),
|
||||
available_document_types=deps.get("available_document_types"),
|
||||
),
|
||||
requires=["search_space_id", "thread_id"],
|
||||
# connector_service, available_connectors, and available_document_types
|
||||
# are optional — when missing, source_strategy="kb_search" degrades
|
||||
# gracefully to "provided"
|
||||
),
|
||||
# Resume generation tool (Typst-based, uses rendercv package)
|
||||
ToolDefinition(
|
||||
name="generate_resume",
|
||||
description="Generate a professional resume as a Typst document",
|
||||
factory=lambda deps: create_generate_resume_tool(
|
||||
search_space_id=deps["search_space_id"],
|
||||
thread_id=deps["thread_id"],
|
||||
),
|
||||
requires=["search_space_id", "thread_id"],
|
||||
),
|
||||
# Generate image tool - creates images using AI models (DALL-E, GPT Image, etc.)
|
||||
ToolDefinition(
|
||||
name="generate_image",
|
||||
description="Generate images from text descriptions using AI image models",
|
||||
factory=lambda deps: create_generate_image_tool(
|
||||
search_space_id=deps["search_space_id"],
|
||||
db_session=deps["db_session"],
|
||||
),
|
||||
requires=["search_space_id", "db_session"],
|
||||
),
|
||||
# Web scraping tool - extracts content from webpages
|
||||
ToolDefinition(
|
||||
name="scrape_webpage",
|
||||
description="Scrape and extract the main content from a webpage",
|
||||
factory=_build_scrape_webpage_tool,
|
||||
requires=[], # firecrawl_api_key is optional
|
||||
),
|
||||
# Web search tool — real-time web search via SearXNG + user-configured engines
|
||||
ToolDefinition(
|
||||
name="web_search",
|
||||
description="Search the web for real-time information using configured search engines",
|
||||
factory=lambda deps: create_web_search_tool(
|
||||
search_space_id=deps.get("search_space_id"),
|
||||
available_connectors=deps.get("available_connectors"),
|
||||
),
|
||||
requires=[],
|
||||
),
|
||||
# =========================================================================
|
||||
# SERVICE ACCOUNT DISCOVERY
|
||||
# Generic tool for the LLM to discover connected accounts and resolve
|
||||
# service-specific identifiers (e.g. Jira cloudId, Slack team, etc.)
|
||||
# =========================================================================
|
||||
ToolDefinition(
|
||||
name="get_connected_accounts",
|
||||
description="Discover connected accounts for a service and their metadata",
|
||||
factory=lambda deps: create_get_connected_accounts_tool(
|
||||
db_session=deps["db_session"],
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
),
|
||||
requires=["db_session", "search_space_id", "user_id"],
|
||||
),
|
||||
# =========================================================================
|
||||
# AUTOMATION AUTHORING - single HITL tool. The tool takes an NL ``intent``
|
||||
# from the main agent, drafts the full AutomationCreate JSON via a focused
|
||||
# sub-LLM, surfaces it on an approval card, and persists on approval. The
|
||||
# factory defers its import because the impl lives under ``multi_agent_chat``
|
||||
# and that package transitively pulls this registry via middleware;
|
||||
# deferring to ``build_tools`` call-time breaks the cycle without a
|
||||
# parallel registry.
|
||||
# =========================================================================
|
||||
ToolDefinition(
|
||||
name="create_automation",
|
||||
description="Draft an automation from an NL intent; user approves the card; tool saves",
|
||||
factory=_build_create_automation_tool,
|
||||
requires=["search_space_id", "user_id", "llm"],
|
||||
),
|
||||
# =========================================================================
|
||||
# MEMORY TOOL - single update_memory, private or team by thread_visibility
|
||||
# =========================================================================
|
||||
ToolDefinition(
|
||||
name="update_memory",
|
||||
description="Save important long-term facts, preferences, and instructions to the (personal or team) memory",
|
||||
factory=_build_update_memory_tool,
|
||||
requires=[
|
||||
"user_id",
|
||||
"search_space_id",
|
||||
"db_session",
|
||||
"thread_visibility",
|
||||
"llm",
|
||||
],
|
||||
),
|
||||
# =========================================================================
|
||||
# NOTION TOOLS - create, update, delete pages
|
||||
# Auto-disabled when no Notion connector is configured # =========================================================================
|
||||
ToolDefinition(
|
||||
name="create_notion_page",
|
||||
description="Create a new page in the user's Notion workspace",
|
||||
factory=lambda deps: create_create_notion_page_tool(
|
||||
db_session=deps["db_session"],
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
),
|
||||
requires=["db_session", "search_space_id", "user_id"],
|
||||
required_connector="NOTION_CONNECTOR",
|
||||
dedup_key=wrap_dedup_key_by_arg_name("title"),
|
||||
),
|
||||
ToolDefinition(
|
||||
name="update_notion_page",
|
||||
description="Append new content to an existing Notion page",
|
||||
factory=lambda deps: create_update_notion_page_tool(
|
||||
db_session=deps["db_session"],
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
),
|
||||
requires=["db_session", "search_space_id", "user_id"],
|
||||
required_connector="NOTION_CONNECTOR",
|
||||
dedup_key=wrap_dedup_key_by_arg_name("page_title"),
|
||||
),
|
||||
ToolDefinition(
|
||||
name="delete_notion_page",
|
||||
description="Delete an existing Notion page",
|
||||
factory=lambda deps: create_delete_notion_page_tool(
|
||||
db_session=deps["db_session"],
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
),
|
||||
requires=["db_session", "search_space_id", "user_id"],
|
||||
required_connector="NOTION_CONNECTOR",
|
||||
dedup_key=wrap_dedup_key_by_arg_name("page_title"),
|
||||
),
|
||||
# =========================================================================
|
||||
# GOOGLE DRIVE TOOLS - create files, delete files
|
||||
# Auto-disabled when no Google Drive connector is configured # =========================================================================
|
||||
ToolDefinition(
|
||||
name="create_google_drive_file",
|
||||
description="Create a new Google Doc or Google Sheet in Google Drive",
|
||||
factory=lambda deps: create_create_google_drive_file_tool(
|
||||
db_session=deps["db_session"],
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
),
|
||||
requires=["db_session", "search_space_id", "user_id"],
|
||||
required_connector="GOOGLE_DRIVE_FILE",
|
||||
dedup_key=wrap_dedup_key_by_arg_name("file_name"),
|
||||
),
|
||||
ToolDefinition(
|
||||
name="delete_google_drive_file",
|
||||
description="Move an indexed Google Drive file to trash",
|
||||
factory=lambda deps: create_delete_google_drive_file_tool(
|
||||
db_session=deps["db_session"],
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
),
|
||||
requires=["db_session", "search_space_id", "user_id"],
|
||||
required_connector="GOOGLE_DRIVE_FILE",
|
||||
dedup_key=wrap_dedup_key_by_arg_name("file_name"),
|
||||
),
|
||||
# =========================================================================
|
||||
# DROPBOX TOOLS - create and trash files
|
||||
# Auto-disabled when no Dropbox connector is configured # =========================================================================
|
||||
ToolDefinition(
|
||||
name="create_dropbox_file",
|
||||
description="Create a new file in Dropbox",
|
||||
factory=lambda deps: create_create_dropbox_file_tool(
|
||||
db_session=deps["db_session"],
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
),
|
||||
requires=["db_session", "search_space_id", "user_id"],
|
||||
required_connector="DROPBOX_FILE",
|
||||
dedup_key=wrap_dedup_key_by_arg_name("file_name"),
|
||||
),
|
||||
ToolDefinition(
|
||||
name="delete_dropbox_file",
|
||||
description="Delete a file from Dropbox",
|
||||
factory=lambda deps: create_delete_dropbox_file_tool(
|
||||
db_session=deps["db_session"],
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
),
|
||||
requires=["db_session", "search_space_id", "user_id"],
|
||||
required_connector="DROPBOX_FILE",
|
||||
dedup_key=wrap_dedup_key_by_arg_name("file_name"),
|
||||
),
|
||||
# =========================================================================
|
||||
# ONEDRIVE TOOLS - create and trash files
|
||||
# Auto-disabled when no OneDrive connector is configured # =========================================================================
|
||||
ToolDefinition(
|
||||
name="create_onedrive_file",
|
||||
description="Create a new file in Microsoft OneDrive",
|
||||
factory=lambda deps: create_create_onedrive_file_tool(
|
||||
db_session=deps["db_session"],
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
),
|
||||
requires=["db_session", "search_space_id", "user_id"],
|
||||
required_connector="ONEDRIVE_FILE",
|
||||
dedup_key=wrap_dedup_key_by_arg_name("file_name"),
|
||||
),
|
||||
ToolDefinition(
|
||||
name="delete_onedrive_file",
|
||||
description="Move a OneDrive file to the recycle bin",
|
||||
factory=lambda deps: create_delete_onedrive_file_tool(
|
||||
db_session=deps["db_session"],
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
),
|
||||
requires=["db_session", "search_space_id", "user_id"],
|
||||
required_connector="ONEDRIVE_FILE",
|
||||
dedup_key=wrap_dedup_key_by_arg_name("file_name"),
|
||||
),
|
||||
# =========================================================================
|
||||
# GOOGLE CALENDAR TOOLS - search, create, update, delete events
|
||||
# Auto-disabled when no Google Calendar connector is configured
|
||||
# =========================================================================
|
||||
ToolDefinition(
|
||||
name="search_calendar_events",
|
||||
description="Search Google Calendar events within a date range",
|
||||
factory=lambda deps: create_search_calendar_events_tool(
|
||||
db_session=deps["db_session"],
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
),
|
||||
requires=["db_session", "search_space_id", "user_id"],
|
||||
required_connector="GOOGLE_CALENDAR_CONNECTOR",
|
||||
),
|
||||
ToolDefinition(
|
||||
name="create_calendar_event",
|
||||
description="Create a new event on Google Calendar",
|
||||
factory=lambda deps: create_create_calendar_event_tool(
|
||||
db_session=deps["db_session"],
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
),
|
||||
requires=["db_session", "search_space_id", "user_id"],
|
||||
required_connector="GOOGLE_CALENDAR_CONNECTOR",
|
||||
dedup_key=wrap_dedup_key_by_arg_name("title"),
|
||||
),
|
||||
ToolDefinition(
|
||||
name="update_calendar_event",
|
||||
description="Update an existing indexed Google Calendar event",
|
||||
factory=lambda deps: create_update_calendar_event_tool(
|
||||
db_session=deps["db_session"],
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
),
|
||||
requires=["db_session", "search_space_id", "user_id"],
|
||||
required_connector="GOOGLE_CALENDAR_CONNECTOR",
|
||||
dedup_key=wrap_dedup_key_by_arg_name("event_title_or_id"),
|
||||
),
|
||||
ToolDefinition(
|
||||
name="delete_calendar_event",
|
||||
description="Delete an existing indexed Google Calendar event",
|
||||
factory=lambda deps: create_delete_calendar_event_tool(
|
||||
db_session=deps["db_session"],
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
),
|
||||
requires=["db_session", "search_space_id", "user_id"],
|
||||
required_connector="GOOGLE_CALENDAR_CONNECTOR",
|
||||
dedup_key=wrap_dedup_key_by_arg_name("event_title_or_id"),
|
||||
),
|
||||
# =========================================================================
|
||||
# GMAIL TOOLS - search, read, create drafts, update drafts, send, trash
|
||||
# Auto-disabled when no Gmail connector is configured
|
||||
# =========================================================================
|
||||
ToolDefinition(
|
||||
name="search_gmail",
|
||||
description="Search emails in Gmail using Gmail search syntax",
|
||||
factory=lambda deps: create_search_gmail_tool(
|
||||
db_session=deps["db_session"],
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
),
|
||||
requires=["db_session", "search_space_id", "user_id"],
|
||||
required_connector="GOOGLE_GMAIL_CONNECTOR",
|
||||
),
|
||||
ToolDefinition(
|
||||
name="read_gmail_email",
|
||||
description="Read the full content of a specific Gmail email",
|
||||
factory=lambda deps: create_read_gmail_email_tool(
|
||||
db_session=deps["db_session"],
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
),
|
||||
requires=["db_session", "search_space_id", "user_id"],
|
||||
required_connector="GOOGLE_GMAIL_CONNECTOR",
|
||||
),
|
||||
ToolDefinition(
|
||||
name="create_gmail_draft",
|
||||
description="Create a draft email in Gmail",
|
||||
factory=lambda deps: create_create_gmail_draft_tool(
|
||||
db_session=deps["db_session"],
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
),
|
||||
requires=["db_session", "search_space_id", "user_id"],
|
||||
required_connector="GOOGLE_GMAIL_CONNECTOR",
|
||||
dedup_key=wrap_dedup_key_by_arg_name("subject"),
|
||||
),
|
||||
ToolDefinition(
|
||||
name="send_gmail_email",
|
||||
description="Send an email via Gmail",
|
||||
factory=lambda deps: create_send_gmail_email_tool(
|
||||
db_session=deps["db_session"],
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
),
|
||||
requires=["db_session", "search_space_id", "user_id"],
|
||||
required_connector="GOOGLE_GMAIL_CONNECTOR",
|
||||
dedup_key=wrap_dedup_key_by_arg_name("subject"),
|
||||
),
|
||||
ToolDefinition(
|
||||
name="trash_gmail_email",
|
||||
description="Move an indexed email to trash in Gmail",
|
||||
factory=lambda deps: create_trash_gmail_email_tool(
|
||||
db_session=deps["db_session"],
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
),
|
||||
requires=["db_session", "search_space_id", "user_id"],
|
||||
required_connector="GOOGLE_GMAIL_CONNECTOR",
|
||||
dedup_key=wrap_dedup_key_by_arg_name("email_subject_or_id"),
|
||||
),
|
||||
ToolDefinition(
|
||||
name="update_gmail_draft",
|
||||
description="Update an existing Gmail draft",
|
||||
factory=lambda deps: create_update_gmail_draft_tool(
|
||||
db_session=deps["db_session"],
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
),
|
||||
requires=["db_session", "search_space_id", "user_id"],
|
||||
required_connector="GOOGLE_GMAIL_CONNECTOR",
|
||||
dedup_key=wrap_dedup_key_by_arg_name("draft_subject_or_id"),
|
||||
),
|
||||
# =========================================================================
|
||||
# CONFLUENCE TOOLS - create, update, delete pages
|
||||
# Auto-disabled when no Confluence connector is configured # =========================================================================
|
||||
ToolDefinition(
|
||||
name="create_confluence_page",
|
||||
description="Create a new page in the user's Confluence space",
|
||||
factory=lambda deps: create_create_confluence_page_tool(
|
||||
db_session=deps["db_session"],
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
),
|
||||
requires=["db_session", "search_space_id", "user_id"],
|
||||
required_connector="CONFLUENCE_CONNECTOR",
|
||||
dedup_key=wrap_dedup_key_by_arg_name("title"),
|
||||
),
|
||||
ToolDefinition(
|
||||
name="update_confluence_page",
|
||||
description="Update an existing indexed Confluence page",
|
||||
factory=lambda deps: create_update_confluence_page_tool(
|
||||
db_session=deps["db_session"],
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
),
|
||||
requires=["db_session", "search_space_id", "user_id"],
|
||||
required_connector="CONFLUENCE_CONNECTOR",
|
||||
dedup_key=wrap_dedup_key_by_arg_name("page_title_or_id"),
|
||||
),
|
||||
ToolDefinition(
|
||||
name="delete_confluence_page",
|
||||
description="Delete an existing indexed Confluence page",
|
||||
factory=lambda deps: create_delete_confluence_page_tool(
|
||||
db_session=deps["db_session"],
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
),
|
||||
requires=["db_session", "search_space_id", "user_id"],
|
||||
required_connector="CONFLUENCE_CONNECTOR",
|
||||
dedup_key=wrap_dedup_key_by_arg_name("page_title_or_id"),
|
||||
),
|
||||
# =========================================================================
|
||||
# DISCORD TOOLS - list channels, read messages, send messages
|
||||
# Auto-disabled when no Discord connector is configured
|
||||
# =========================================================================
|
||||
ToolDefinition(
|
||||
name="list_discord_channels",
|
||||
description="List text channels in the connected Discord server",
|
||||
factory=lambda deps: create_list_discord_channels_tool(
|
||||
db_session=deps["db_session"],
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
),
|
||||
requires=["db_session", "search_space_id", "user_id"],
|
||||
required_connector="DISCORD_CONNECTOR",
|
||||
),
|
||||
ToolDefinition(
|
||||
name="read_discord_messages",
|
||||
description="Read recent messages from a Discord text channel",
|
||||
factory=lambda deps: create_read_discord_messages_tool(
|
||||
db_session=deps["db_session"],
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
),
|
||||
requires=["db_session", "search_space_id", "user_id"],
|
||||
required_connector="DISCORD_CONNECTOR",
|
||||
),
|
||||
ToolDefinition(
|
||||
name="send_discord_message",
|
||||
description="Send a message to a Discord text channel",
|
||||
factory=lambda deps: create_send_discord_message_tool(
|
||||
db_session=deps["db_session"],
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
),
|
||||
requires=["db_session", "search_space_id", "user_id"],
|
||||
required_connector="DISCORD_CONNECTOR",
|
||||
),
|
||||
# =========================================================================
|
||||
# TEAMS TOOLS - list channels, read messages, send messages
|
||||
# Auto-disabled when no Teams connector is configured
|
||||
# =========================================================================
|
||||
ToolDefinition(
|
||||
name="list_teams_channels",
|
||||
description="List Microsoft Teams and their channels",
|
||||
factory=lambda deps: create_list_teams_channels_tool(
|
||||
db_session=deps["db_session"],
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
),
|
||||
requires=["db_session", "search_space_id", "user_id"],
|
||||
required_connector="TEAMS_CONNECTOR",
|
||||
),
|
||||
ToolDefinition(
|
||||
name="read_teams_messages",
|
||||
description="Read recent messages from a Microsoft Teams channel",
|
||||
factory=lambda deps: create_read_teams_messages_tool(
|
||||
db_session=deps["db_session"],
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
),
|
||||
requires=["db_session", "search_space_id", "user_id"],
|
||||
required_connector="TEAMS_CONNECTOR",
|
||||
),
|
||||
ToolDefinition(
|
||||
name="send_teams_message",
|
||||
description="Send a message to a Microsoft Teams channel",
|
||||
factory=lambda deps: create_send_teams_message_tool(
|
||||
db_session=deps["db_session"],
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
),
|
||||
requires=["db_session", "search_space_id", "user_id"],
|
||||
required_connector="TEAMS_CONNECTOR",
|
||||
),
|
||||
# =========================================================================
|
||||
# LUMA TOOLS - list events, read event details, create events
|
||||
# Auto-disabled when no Luma connector is configured
|
||||
# =========================================================================
|
||||
ToolDefinition(
|
||||
name="list_luma_events",
|
||||
description="List upcoming and recent Luma events",
|
||||
factory=lambda deps: create_list_luma_events_tool(
|
||||
db_session=deps["db_session"],
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
),
|
||||
requires=["db_session", "search_space_id", "user_id"],
|
||||
required_connector="LUMA_CONNECTOR",
|
||||
),
|
||||
ToolDefinition(
|
||||
name="read_luma_event",
|
||||
description="Read detailed information about a specific Luma event",
|
||||
factory=lambda deps: create_read_luma_event_tool(
|
||||
db_session=deps["db_session"],
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
),
|
||||
requires=["db_session", "search_space_id", "user_id"],
|
||||
required_connector="LUMA_CONNECTOR",
|
||||
),
|
||||
ToolDefinition(
|
||||
name="create_luma_event",
|
||||
description="Create a new event on Luma",
|
||||
factory=lambda deps: create_create_luma_event_tool(
|
||||
db_session=deps["db_session"],
|
||||
search_space_id=deps["search_space_id"],
|
||||
user_id=deps["user_id"],
|
||||
),
|
||||
requires=["db_session", "search_space_id", "user_id"],
|
||||
required_connector="LUMA_CONNECTOR",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Registry Functions
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def get_connector_gated_tools(
|
||||
available_connectors: list[str] | None,
|
||||
) -> list[str]:
|
||||
"""Return tool names to disable"""
|
||||
available = set() if available_connectors is None else set(available_connectors)
|
||||
|
||||
disabled: list[str] = []
|
||||
for tool_def in BUILTIN_TOOLS:
|
||||
if tool_def.required_connector and tool_def.required_connector not in available:
|
||||
disabled.append(tool_def.name)
|
||||
return disabled
|
||||
|
|
@ -1668,7 +1668,7 @@ async def list_agent_tools(
|
|||
|
||||
Hidden (WIP) tools are excluded from the response.
|
||||
"""
|
||||
from app.agents.shared.tools.registry import BUILTIN_TOOLS
|
||||
from app.agents.shared.tools.catalog import TOOL_CATALOG
|
||||
|
||||
return [
|
||||
AgentToolInfo(
|
||||
|
|
@ -1676,7 +1676,7 @@ async def list_agent_tools(
|
|||
description=t.description,
|
||||
enabled_by_default=t.enabled_by_default,
|
||||
)
|
||||
for t in BUILTIN_TOOLS
|
||||
for t in TOOL_CATALOG
|
||||
if not t.hidden
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from langchain_core.tools import tool
|
|||
|
||||
from app.agents.shared.feature_flags import AgentFeatureFlags
|
||||
from app.agents.shared.middleware.action_log import ActionLogMiddleware
|
||||
from app.agents.shared.tools.registry import ToolDefinition
|
||||
from app.agents.shared.middleware.action_log import ToolDefinition
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
|
|||
|
|
@ -91,10 +91,9 @@ def test_no_agent_tools_means_no_dedup() -> None:
|
|||
"""After the cleanup tier removed the legacy ``_NATIVE_HITL_TOOL_DEDUP_KEYS``
|
||||
map, dedup is purely declarative — no resolvers means no dedup runs.
|
||||
|
||||
Coverage for the previously hardcoded native HITL tools now lives on
|
||||
each :class:`ToolDefinition.dedup_key` in
|
||||
:mod:`app.agents.shared.tools.registry`, which is wired through to
|
||||
``tool.metadata`` by :func:`build_tools`.
|
||||
Dedup is purely declarative: tools opt in by carrying a ``dedup_key``
|
||||
(callable) or ``hitl_dedup_key`` (arg name) in their ``metadata``. With no
|
||||
agent tools, there are no resolvers and dedup is a no-op.
|
||||
"""
|
||||
mw = DedupHITLToolCallsMiddleware(agent_tools=None)
|
||||
state = {
|
||||
|
|
@ -109,27 +108,6 @@ def test_no_agent_tools_means_no_dedup() -> None:
|
|||
assert out is None
|
||||
|
||||
|
||||
def test_registry_propagates_dedup_key_to_tool_metadata() -> None:
|
||||
"""Smoke-check the wiring path that replaced the legacy native map.
|
||||
|
||||
``ToolDefinition.dedup_key`` set in the registry must be copied onto
|
||||
the constructed tool's ``metadata`` so :class:`DedupHITLToolCallsMiddleware`
|
||||
can pick it up at agent build time.
|
||||
"""
|
||||
from app.agents.shared.tools.registry import (
|
||||
BUILTIN_TOOLS,
|
||||
wrap_dedup_key_by_arg_name,
|
||||
)
|
||||
|
||||
notion_tool_defs = [t for t in BUILTIN_TOOLS if t.name == "create_notion_page"]
|
||||
assert notion_tool_defs, "registry should still expose create_notion_page"
|
||||
tool_def = notion_tool_defs[0]
|
||||
assert tool_def.dedup_key is not None
|
||||
# Same wrapping helper used in the registry — sanity check identity
|
||||
sample = wrap_dedup_key_by_arg_name("title")({"title": "Plan"})
|
||||
assert sample == "plan"
|
||||
|
||||
|
||||
def test_full_args_dedup_keeps_distinct_calls_sharing_a_field() -> None:
|
||||
"""Regression: MCP tools (e.g. ``createJiraIssue``) used to dedup on
|
||||
the schema's first required field, which is often the workspace /
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue