mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-06 20:15:17 +02:00
refactor(agents): move tools package to app/agents/shared (slice 6)
Relocate the entire new_chat/tools/ package (62 files incl. registry, hitl, MCP cluster, and all connector subpackages: gmail/slack/discord/teams/drive/etc.) to the shared kernel. The package turned out to be a clean cohesive cluster: its only references to non-tools new_chat modules were comments, and its middleware deps were already flipped to shared in slice 5c. Flip 33 live importers (multi-agent, flows, routes, services, anonymous_agent, tests). Re-export shims remain for the frozen single-agent stack: a package __init__ mirroring the public surface (new_chat.__init__ imports it) plus invalid_tool + registry submodule shims (chat_deepagent imports those). Resolves slice 5c's two transient back-edges: shared/middleware/action_log (TYPE_CHECKING ToolDefinition) and tool_call_repair (local INVALID_TOOL_NAME) now point at app.agents.shared.tools.
This commit is contained in:
parent
a7fde2a48e
commit
aab95b9130
98 changed files with 1232 additions and 1152 deletions
|
|
@ -27,8 +27,8 @@ from app.agents.shared.filesystem_backends import build_backend_resolver
|
|||
from app.agents.shared.filesystem_selection import FilesystemMode, FilesystemSelection
|
||||
from app.agents.shared.llm_config import AgentConfig
|
||||
from app.agents.shared.prompt_caching import apply_litellm_prompt_caching
|
||||
from app.agents.new_chat.tools.invalid_tool import INVALID_TOOL_NAME, invalid_tool
|
||||
from app.agents.new_chat.tools.registry import build_tools_async
|
||||
from app.agents.shared.tools.invalid_tool import INVALID_TOOL_NAME, invalid_tool
|
||||
from app.agents.shared.tools.registry import build_tools_async
|
||||
from app.db import ChatVisibility
|
||||
from app.services.connector_service import ConnectorService
|
||||
from app.services.user_tool_allowlist import (
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import logging
|
|||
|
||||
from app.agents.shared.feature_flags import AgentFeatureFlags
|
||||
from app.agents.shared.middleware import ActionLogMiddleware
|
||||
from app.agents.new_chat.tools.registry import BUILTIN_TOOLS
|
||||
from app.agents.shared.tools.registry import BUILTIN_TOOLS
|
||||
|
||||
from ..shared.flags import enabled
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
from app.agents.new_chat.tools.google_calendar.create_event import (
|
||||
from app.agents.shared.tools.google_calendar.create_event import (
|
||||
create_create_calendar_event_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.google_calendar.delete_event import (
|
||||
from app.agents.shared.tools.google_calendar.delete_event import (
|
||||
create_delete_calendar_event_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.google_calendar.search_events import (
|
||||
from app.agents.shared.tools.google_calendar.search_events import (
|
||||
create_search_calendar_events_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.google_calendar.update_event import (
|
||||
from app.agents.shared.tools.google_calendar.update_event import (
|
||||
create_update_calendar_event_tool,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from langchain_core.tools import tool
|
|||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
|
||||
from app.agents.new_chat.tools.gmail.search_emails import _build_credentials
|
||||
from app.agents.shared.tools.gmail.search_emails import _build_credentials
|
||||
from app.db import SearchSourceConnector, SearchSourceConnectorType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
from app.agents.new_chat.tools.discord.list_channels import (
|
||||
from app.agents.shared.tools.discord.list_channels import (
|
||||
create_list_discord_channels_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.discord.read_messages import (
|
||||
from app.agents.shared.tools.discord.read_messages import (
|
||||
create_read_discord_messages_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.discord.send_message import (
|
||||
from app.agents.shared.tools.discord.send_message import (
|
||||
create_send_discord_message_tool,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from app.agents.new_chat.tools.dropbox.create_file import (
|
||||
from app.agents.shared.tools.dropbox.create_file import (
|
||||
create_create_dropbox_file_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.dropbox.trash_file import (
|
||||
from app.agents.shared.tools.dropbox.trash_file import (
|
||||
create_delete_dropbox_file_tool,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
from app.agents.new_chat.tools.gmail.create_draft import (
|
||||
from app.agents.shared.tools.gmail.create_draft import (
|
||||
create_create_gmail_draft_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.gmail.read_email import (
|
||||
from app.agents.shared.tools.gmail.read_email import (
|
||||
create_read_gmail_email_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.gmail.search_emails import (
|
||||
from app.agents.shared.tools.gmail.search_emails import (
|
||||
create_search_gmail_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.gmail.send_email import (
|
||||
from app.agents.shared.tools.gmail.send_email import (
|
||||
create_send_gmail_email_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.gmail.trash_email import (
|
||||
from app.agents.shared.tools.gmail.trash_email import (
|
||||
create_trash_gmail_email_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.gmail.update_draft import (
|
||||
from app.agents.shared.tools.gmail.update_draft import (
|
||||
create_update_gmail_draft_tool,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ def create_read_gmail_email_tool(
|
|||
"message": "Composio connected account ID not found for this Gmail connector.",
|
||||
}
|
||||
|
||||
from app.agents.new_chat.tools.gmail.search_emails import (
|
||||
from app.agents.shared.tools.gmail.search_emails import (
|
||||
_format_gmail_summary,
|
||||
)
|
||||
from app.services.composio_service import ComposioService
|
||||
|
|
@ -97,7 +97,7 @@ def create_read_gmail_email_tool(
|
|||
"content": content,
|
||||
}
|
||||
|
||||
from app.agents.new_chat.tools.gmail.search_emails import (
|
||||
from app.agents.shared.tools.gmail.search_emails import (
|
||||
_build_credentials,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ def create_search_gmail_tool(
|
|||
"message": "Composio connected account ID not found for this Gmail connector.",
|
||||
}
|
||||
|
||||
from app.agents.new_chat.tools.gmail.search_emails import (
|
||||
from app.agents.shared.tools.gmail.search_emails import (
|
||||
_format_gmail_summary,
|
||||
)
|
||||
from app.services.composio_service import ComposioService
|
||||
|
|
@ -98,7 +98,7 @@ def create_search_gmail_tool(
|
|||
}
|
||||
return {"status": "success", "emails": emails, "total": len(emails)}
|
||||
|
||||
from app.agents.new_chat.tools.gmail.search_emails import (
|
||||
from app.agents.shared.tools.gmail.search_emails import (
|
||||
_build_credentials,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from app.agents.new_chat.tools.google_drive.create_file import (
|
||||
from app.agents.shared.tools.google_drive.create_file import (
|
||||
create_create_google_drive_file_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.google_drive.trash_file import (
|
||||
from app.agents.shared.tools.google_drive.trash_file import (
|
||||
create_delete_google_drive_file_tool,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
from app.agents.new_chat.tools.luma.create_event import (
|
||||
from app.agents.shared.tools.luma.create_event import (
|
||||
create_create_luma_event_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.luma.list_events import (
|
||||
from app.agents.shared.tools.luma.list_events import (
|
||||
create_list_luma_events_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.luma.read_event import (
|
||||
from app.agents.shared.tools.luma.read_event import (
|
||||
create_read_luma_event_tool,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from app.agents.new_chat.tools.onedrive.create_file import (
|
||||
from app.agents.shared.tools.onedrive.create_file import (
|
||||
create_create_onedrive_file_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.onedrive.trash_file import (
|
||||
from app.agents.shared.tools.onedrive.trash_file import (
|
||||
create_delete_onedrive_file_tool,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
from app.agents.new_chat.tools.teams.list_channels import (
|
||||
from app.agents.shared.tools.teams.list_channels import (
|
||||
create_list_teams_channels_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.teams.read_messages import (
|
||||
from app.agents.shared.tools.teams.read_messages import (
|
||||
create_read_teams_messages_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.teams.send_message import (
|
||||
from app.agents.shared.tools.teams.send_message import (
|
||||
create_send_teams_message_tool,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||
from app.agents.multi_agent_chat.constants import (
|
||||
CONNECTOR_TYPE_TO_CONNECTOR_AGENT_MAPS,
|
||||
)
|
||||
from app.agents.new_chat.tools.mcp_tool import load_mcp_tools
|
||||
from app.agents.shared.tools.mcp_tool import load_mcp_tools
|
||||
from app.db import SearchSourceConnector
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ from app.agents.shared.middleware import (
|
|||
RetryAfterMiddleware,
|
||||
create_surfsense_compaction_middleware,
|
||||
)
|
||||
from app.agents.new_chat.tools.web_search import create_web_search_tool
|
||||
from app.agents.shared.tools.web_search import create_web_search_tool
|
||||
|
||||
# Cap how much of an uploaded document we inline into the system prompt. The
|
||||
# upload endpoint allows files up to several MB, but the doc is re-sent on
|
||||
|
|
|
|||
|
|
@ -1,46 +1,35 @@
|
|||
"""
|
||||
Tools module for SurfSense deep agent.
|
||||
"""Backward-compatible shim package.
|
||||
|
||||
This module contains all the tools available to the SurfSense agent.
|
||||
To add a new tool, see the documentation in registry.py.
|
||||
|
||||
Available tools:
|
||||
- generate_podcast: Generate audio podcasts from content
|
||||
- generate_video_presentation: Generate video presentations with slides and narration
|
||||
- generate_image: Generate images from text descriptions using AI models
|
||||
- scrape_webpage: Extract content from webpages
|
||||
- update_memory: Update the user's / team's memory document
|
||||
The agent tools now live in the shared kernel at ``app.agents.shared.tools``.
|
||||
This package re-exports the public surface (and keeps ``invalid_tool`` /
|
||||
``registry`` submodule shims) so the frozen single-agent stack
|
||||
(``new_chat.__init__`` and ``chat_deepagent``) keeps working until that stack is
|
||||
retired. All live code imports from ``app.agents.shared.tools`` directly.
|
||||
"""
|
||||
|
||||
# Registry exports
|
||||
# Tool factory exports (for direct use)
|
||||
from .generate_image import create_generate_image_tool
|
||||
from .knowledge_base import (
|
||||
CONNECTOR_DESCRIPTIONS,
|
||||
format_documents_for_context,
|
||||
search_knowledge_base_async,
|
||||
)
|
||||
from .podcast import create_generate_podcast_tool
|
||||
from .registry import (
|
||||
from app.agents.shared.tools import (
|
||||
BUILTIN_TOOLS,
|
||||
CONNECTOR_DESCRIPTIONS,
|
||||
ToolDefinition,
|
||||
build_tools,
|
||||
create_generate_image_tool,
|
||||
create_generate_podcast_tool,
|
||||
create_generate_video_presentation_tool,
|
||||
create_scrape_webpage_tool,
|
||||
create_update_memory_tool,
|
||||
create_update_team_memory_tool,
|
||||
format_documents_for_context,
|
||||
get_all_tool_names,
|
||||
get_default_enabled_tools,
|
||||
get_tool_by_name,
|
||||
search_knowledge_base_async,
|
||||
)
|
||||
from .scrape_webpage import create_scrape_webpage_tool
|
||||
from .update_memory import create_update_memory_tool, create_update_team_memory_tool
|
||||
from .video_presentation import create_generate_video_presentation_tool
|
||||
|
||||
__all__ = [
|
||||
# Registry
|
||||
"BUILTIN_TOOLS",
|
||||
# Knowledge base utilities
|
||||
"CONNECTOR_DESCRIPTIONS",
|
||||
"ToolDefinition",
|
||||
"build_tools",
|
||||
# Tool factories
|
||||
"create_generate_image_tool",
|
||||
"create_generate_podcast_tool",
|
||||
"create_generate_video_presentation_tool",
|
||||
|
|
|
|||
|
|
@ -1,50 +1,14 @@
|
|||
"""
|
||||
The ``invalid`` fallback tool.
|
||||
"""Backward-compatible shim.
|
||||
|
||||
When the model emits a tool call whose name doesn't match any registered
|
||||
tool, :class:`ToolCallNameRepairMiddleware` rewrites the call to ``invalid``
|
||||
with the original name and a parser/validation error string. This tool's
|
||||
execution then returns that error to the model so it can self-correct.
|
||||
|
||||
Ported from OpenCode's ``packages/opencode/src/tool/invalid.ts`` —
|
||||
LangChain has no equivalent fallback path; the default behavior on an
|
||||
unknown tool name is a hard ``ToolNotFoundError`` which kills the turn.
|
||||
|
||||
Critically, the :class:`ToolDefinition` for this tool is **excluded** from
|
||||
the system-prompt tool list and from ``LLMToolSelectorMiddleware`` selection
|
||||
(see ``ToolDefinition.always_include`` filtering in the registry) — the
|
||||
model never advertises ``invalid`` as a callable. It only ever shows up
|
||||
in the tool registry so LangGraph can dispatch the rewritten call.
|
||||
Moved to ``app.agents.shared.tools.invalid_tool``. Re-exported here for the
|
||||
frozen single-agent stack (``chat_deepagent``) until that stack is retired.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from langchain_core.tools import tool
|
||||
|
||||
INVALID_TOOL_NAME = "invalid"
|
||||
INVALID_TOOL_DESCRIPTION = "Do not use"
|
||||
|
||||
|
||||
def _format_invalid_message(tool: str | None, error: str | None) -> str:
|
||||
"""Return the user-visible error string. Mirrors ``invalid.ts``."""
|
||||
name = tool or "<unknown>"
|
||||
detail = error or "(no error message provided)"
|
||||
return (
|
||||
f"The arguments provided to the tool `{name}` are invalid: {detail}\n"
|
||||
f"Read the tool's docstring carefully and try again with valid arguments."
|
||||
)
|
||||
|
||||
|
||||
@tool(name_or_callable=INVALID_TOOL_NAME, description=INVALID_TOOL_DESCRIPTION)
|
||||
def invalid_tool(tool: str | None = None, error: str | None = None) -> str:
|
||||
"""Return a human-readable explanation of a tool-call validation failure.
|
||||
|
||||
Activated only when :class:`ToolCallNameRepairMiddleware` rewrites a
|
||||
failed tool call to ``invalid`` with the original tool name and the
|
||||
error message produced during validation.
|
||||
"""
|
||||
return _format_invalid_message(tool, error)
|
||||
|
||||
from app.agents.shared.tools.invalid_tool import (
|
||||
INVALID_TOOL_DESCRIPTION,
|
||||
INVALID_TOOL_NAME,
|
||||
invalid_tool,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"INVALID_TOOL_DESCRIPTION",
|
||||
|
|
|
|||
|
|
@ -1,962 +1,19 @@
|
|||
"""Tools registry for SurfSense deep agent.
|
||||
"""Backward-compatible shim.
|
||||
|
||||
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"],
|
||||
),
|
||||
Moved to ``app.agents.shared.tools.registry``. Re-exported here for the frozen
|
||||
single-agent stack (``chat_deepagent``) until that stack is retired.
|
||||
"""
|
||||
|
||||
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.agents.shared.tools.registry import (
|
||||
BUILTIN_TOOLS,
|
||||
ToolDefinition,
|
||||
build_tools_async,
|
||||
get_connector_gated_tools,
|
||||
)
|
||||
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 .mcp_tool import load_mcp_tools
|
||||
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 .scrape_webpage import create_scrape_webpage_tool
|
||||
from .teams import (
|
||||
create_list_teams_channels_tool,
|
||||
create_read_teams_messages_tool,
|
||||
create_send_teams_message_tool,
|
||||
)
|
||||
from .update_memory import create_update_memory_tool, create_update_team_memory_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"],
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 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=lambda deps: create_scrape_webpage_tool(
|
||||
firecrawl_api_key=deps.get("firecrawl_api_key"),
|
||||
),
|
||||
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=lambda deps: (
|
||||
create_update_team_memory_tool(
|
||||
search_space_id=deps["search_space_id"],
|
||||
db_session=deps["db_session"],
|
||||
llm=deps.get("llm"),
|
||||
)
|
||||
if deps["thread_visibility"] == ChatVisibility.SEARCH_SPACE
|
||||
else create_update_memory_tool(
|
||||
user_id=deps["user_id"],
|
||||
db_session=deps["db_session"],
|
||||
llm=deps.get("llm"),
|
||||
)
|
||||
),
|
||||
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 (see chat_deepagent.py)
|
||||
# =========================================================================
|
||||
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 (see chat_deepagent.py)
|
||||
# =========================================================================
|
||||
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 (see chat_deepagent.py)
|
||||
# =========================================================================
|
||||
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 (see chat_deepagent.py)
|
||||
# =========================================================================
|
||||
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 (see chat_deepagent.py)
|
||||
# =========================================================================
|
||||
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",
|
||||
),
|
||||
__all__ = [
|
||||
"BUILTIN_TOOLS",
|
||||
"ToolDefinition",
|
||||
"build_tools_async",
|
||||
"get_connector_gated_tools",
|
||||
]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Registry Functions
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def get_tool_by_name(name: str) -> ToolDefinition | None:
|
||||
"""Get a tool definition by its name."""
|
||||
for tool_def in BUILTIN_TOOLS:
|
||||
if tool_def.name == name:
|
||||
return tool_def
|
||||
return None
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def get_all_tool_names() -> list[str]:
|
||||
"""Get names of all registered tools."""
|
||||
return [tool_def.name for tool_def in BUILTIN_TOOLS]
|
||||
|
||||
|
||||
def get_default_enabled_tools() -> list[str]:
|
||||
"""Get names of tools that are enabled by default (excludes hidden tools)."""
|
||||
return [tool_def.name for tool_def in BUILTIN_TOOLS if tool_def.enabled_by_default]
|
||||
|
||||
|
||||
def build_tools(
|
||||
dependencies: dict[str, Any],
|
||||
enabled_tools: list[str] | None = None,
|
||||
disabled_tools: list[str] | None = None,
|
||||
additional_tools: list[BaseTool] | None = None,
|
||||
) -> list[BaseTool]:
|
||||
"""Build the list of tools for the agent.
|
||||
|
||||
Args:
|
||||
dependencies: Dict containing all possible dependencies:
|
||||
- search_space_id: The search space ID
|
||||
- db_session: Database session
|
||||
- connector_service: Connector service instance
|
||||
- firecrawl_api_key: Optional Firecrawl API key
|
||||
enabled_tools: Explicit list of tool names to enable. If None, uses defaults.
|
||||
disabled_tools: List of tool names to disable (applied after enabled_tools).
|
||||
additional_tools: Extra tools to add (e.g., custom tools not in registry).
|
||||
|
||||
Returns:
|
||||
List of configured tool instances ready for the agent.
|
||||
|
||||
Example:
|
||||
# Use all default tools
|
||||
tools = build_tools(deps)
|
||||
|
||||
# Use only specific tools
|
||||
tools = build_tools(deps, enabled_tools=["generate_report"])
|
||||
|
||||
# Use defaults but disable podcast
|
||||
tools = build_tools(deps, disabled_tools=["generate_podcast"])
|
||||
|
||||
# Add custom tools
|
||||
tools = build_tools(deps, additional_tools=[my_custom_tool])
|
||||
|
||||
"""
|
||||
# Determine which tools to enable
|
||||
if enabled_tools is not None:
|
||||
tool_names_to_use = set(enabled_tools)
|
||||
else:
|
||||
tool_names_to_use = set(get_default_enabled_tools())
|
||||
|
||||
# Apply disabled list
|
||||
if disabled_tools:
|
||||
tool_names_to_use -= set(disabled_tools)
|
||||
|
||||
# Build the tools (skip hidden/WIP tools unconditionally)
|
||||
tools: list[BaseTool] = []
|
||||
for tool_def in BUILTIN_TOOLS:
|
||||
if tool_def.hidden or tool_def.name not in tool_names_to_use:
|
||||
continue
|
||||
|
||||
# Check that all required dependencies are provided
|
||||
missing_deps = [dep for dep in tool_def.requires if dep not in dependencies]
|
||||
if missing_deps:
|
||||
msg = f"Tool '{tool_def.name}' requires dependencies: {missing_deps}"
|
||||
raise ValueError(
|
||||
msg,
|
||||
)
|
||||
|
||||
# Create the tool
|
||||
tool = tool_def.factory(dependencies)
|
||||
# Propagate the registry-level metadata so middleware (e.g.
|
||||
# ``DedupHITLToolCallsMiddleware``) and the action-log/revert
|
||||
# pipeline can pick the resolvers up via ``tool.metadata`` without
|
||||
# re-importing :data:`BUILTIN_TOOLS`.
|
||||
if tool_def.dedup_key is not None or tool_def.reverse is not None:
|
||||
existing_meta = getattr(tool, "metadata", None) or {}
|
||||
merged_meta = dict(existing_meta)
|
||||
if tool_def.dedup_key is not None:
|
||||
merged_meta.setdefault("dedup_key", tool_def.dedup_key)
|
||||
if tool_def.reverse is not None:
|
||||
merged_meta.setdefault("reverse", tool_def.reverse)
|
||||
try:
|
||||
tool.metadata = merged_meta
|
||||
except Exception:
|
||||
logger.debug(
|
||||
"Tool %s rejected metadata mutation; relying on registry lookup",
|
||||
tool_def.name,
|
||||
)
|
||||
tools.append(tool)
|
||||
|
||||
# Add any additional custom tools
|
||||
if additional_tools:
|
||||
tools.extend(additional_tools)
|
||||
|
||||
return tools
|
||||
|
||||
|
||||
async def build_tools_async(
|
||||
dependencies: dict[str, Any],
|
||||
enabled_tools: list[str] | None = None,
|
||||
disabled_tools: list[str] | None = None,
|
||||
additional_tools: list[BaseTool] | None = None,
|
||||
include_mcp_tools: bool = True,
|
||||
) -> list[BaseTool]:
|
||||
"""Async version of build_tools that also loads MCP tools from database.
|
||||
|
||||
Design Note:
|
||||
This function exists because MCP tools require database queries to load
|
||||
user configs, while built-in tools are created synchronously from static
|
||||
code.
|
||||
|
||||
Alternative: We could make build_tools() itself async and always query
|
||||
the database, but that would force async everywhere even when only using
|
||||
built-in tools. The current design keeps the simple case (static tools
|
||||
only) synchronous while supporting dynamic database-loaded tools through
|
||||
this async wrapper.
|
||||
|
||||
Phase 1.3: built-in tool construction (CPU; runs in a thread pool to
|
||||
avoid event-loop stalls) and MCP tool loading (HTTP/DB I/O; runs on
|
||||
the event loop) are kicked off concurrently. Cold-path savings are
|
||||
bounded by the slower of the two — typically MCP at ~200ms-1.7s —
|
||||
so the parallelization recovers the ~50-200ms previously spent
|
||||
serially on built-in construction.
|
||||
|
||||
Args:
|
||||
dependencies: Dict containing all possible dependencies
|
||||
enabled_tools: Explicit list of tool names to enable. If None, uses defaults.
|
||||
disabled_tools: List of tool names to disable (applied after enabled_tools).
|
||||
additional_tools: Extra tools to add (e.g., custom tools not in registry).
|
||||
include_mcp_tools: Whether to load user's MCP tools from database.
|
||||
|
||||
Returns:
|
||||
List of configured tool instances ready for the agent, including MCP tools.
|
||||
|
||||
"""
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
_perf_log = logging.getLogger("surfsense.perf")
|
||||
_perf_log.setLevel(logging.DEBUG)
|
||||
|
||||
can_load_mcp = (
|
||||
include_mcp_tools
|
||||
and "db_session" in dependencies
|
||||
and "search_space_id" in dependencies
|
||||
)
|
||||
|
||||
# Built-in tool construction is synchronous + CPU-only. Off-loop it so
|
||||
# MCP's HTTP/DB I/O can fire concurrently. ``build_tools`` is pure
|
||||
# function over its inputs — safe to thread-shift.
|
||||
_t0 = time.perf_counter()
|
||||
builtin_task = asyncio.create_task(
|
||||
asyncio.to_thread(
|
||||
build_tools, dependencies, enabled_tools, disabled_tools, additional_tools
|
||||
)
|
||||
)
|
||||
|
||||
mcp_task: asyncio.Task | None = None
|
||||
if can_load_mcp:
|
||||
mcp_task = asyncio.create_task(
|
||||
load_mcp_tools(
|
||||
dependencies["db_session"],
|
||||
dependencies["search_space_id"],
|
||||
)
|
||||
)
|
||||
|
||||
# Surface failures from each task independently so a flaky MCP
|
||||
# endpoint never poisons built-in tool registration. ``return_exceptions``
|
||||
# gives us per-task exceptions instead of dropping the second result
|
||||
# when the first raises.
|
||||
if mcp_task is not None:
|
||||
builtin_result, mcp_result = await asyncio.gather(
|
||||
builtin_task, mcp_task, return_exceptions=True
|
||||
)
|
||||
else:
|
||||
builtin_result = await builtin_task
|
||||
mcp_result = None
|
||||
|
||||
if isinstance(builtin_result, BaseException):
|
||||
raise builtin_result # built-in registration failure is non-recoverable
|
||||
tools: list[BaseTool] = builtin_result
|
||||
_perf_log.info(
|
||||
"[build_tools_async] Built-in tools in %.3fs (%d tools, parallel)",
|
||||
time.perf_counter() - _t0,
|
||||
len(tools),
|
||||
)
|
||||
|
||||
if mcp_task is not None:
|
||||
if isinstance(mcp_result, BaseException):
|
||||
# ``return_exceptions=True`` captures the exception out-of-band,
|
||||
# so ``sys.exc_info()`` is empty here. Pass the captured
|
||||
# exception via ``exc_info=`` to get a real traceback.
|
||||
logging.error(
|
||||
"Failed to load MCP tools: %s", mcp_result, exc_info=mcp_result
|
||||
)
|
||||
else:
|
||||
mcp_tools = mcp_result or []
|
||||
_perf_log.info(
|
||||
"[build_tools_async] MCP tools loaded in %.3fs (%d tools, parallel)",
|
||||
time.perf_counter() - _t0,
|
||||
len(mcp_tools),
|
||||
)
|
||||
tools.extend(mcp_tools)
|
||||
logging.info(
|
||||
"Registered %d MCP tools: %s",
|
||||
len(mcp_tools),
|
||||
[t.name for t in mcp_tools],
|
||||
)
|
||||
|
||||
logging.info(
|
||||
"Total tools for agent: %d — %s",
|
||||
len(tools),
|
||||
[t.name for t in tools],
|
||||
)
|
||||
|
||||
return tools
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
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.new_chat.tools.registry.ToolDefinition`; the rendered
|
||||
:class:`~app.agents.shared.tools.registry.ToolDefinition`; the rendered
|
||||
descriptor is persisted in ``reverse_descriptor`` for use by
|
||||
``/api/threads/{thread_id}/revert/{action_id}``.
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ if TYPE_CHECKING: # pragma: no cover - type-only
|
|||
# Type-only import: keeping it lazy avoids a module-load cycle through the
|
||||
# frozen single-agent package (new_chat.__init__ -> chat_deepagent ->
|
||||
# middleware shim). Resolves to app.agents.shared.tools once tools migrate.
|
||||
from app.agents.new_chat.tools.registry import ToolDefinition
|
||||
from app.agents.shared.tools.registry import ToolDefinition
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ class ToolCallNameRepairMiddleware(
|
|||
# Local import avoids a module-load cycle through the frozen single-agent
|
||||
# package (new_chat.__init__ -> chat_deepagent -> middleware shim).
|
||||
# Resolves to app.agents.shared.tools once tools migrate.
|
||||
from app.agents.new_chat.tools.invalid_tool import INVALID_TOOL_NAME
|
||||
from app.agents.shared.tools.invalid_tool import INVALID_TOOL_NAME
|
||||
|
||||
if INVALID_TOOL_NAME in registered:
|
||||
original_args = call.get("args") or {}
|
||||
|
|
|
|||
55
surfsense_backend/app/agents/shared/tools/__init__.py
Normal file
55
surfsense_backend/app/agents/shared/tools/__init__.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
"""
|
||||
Tools module for SurfSense deep agent.
|
||||
|
||||
This module contains all the tools available to the SurfSense agent.
|
||||
To add a new tool, see the documentation in registry.py.
|
||||
|
||||
Available tools:
|
||||
- generate_podcast: Generate audio podcasts from content
|
||||
- generate_video_presentation: Generate video presentations with slides and narration
|
||||
- generate_image: Generate images from text descriptions using AI models
|
||||
- scrape_webpage: Extract content from webpages
|
||||
- update_memory: Update the user's / team's memory document
|
||||
"""
|
||||
|
||||
# Registry exports
|
||||
# Tool factory exports (for direct use)
|
||||
from .generate_image import create_generate_image_tool
|
||||
from .knowledge_base import (
|
||||
CONNECTOR_DESCRIPTIONS,
|
||||
format_documents_for_context,
|
||||
search_knowledge_base_async,
|
||||
)
|
||||
from .podcast import create_generate_podcast_tool
|
||||
from .registry import (
|
||||
BUILTIN_TOOLS,
|
||||
ToolDefinition,
|
||||
build_tools,
|
||||
get_all_tool_names,
|
||||
get_default_enabled_tools,
|
||||
get_tool_by_name,
|
||||
)
|
||||
from .scrape_webpage import create_scrape_webpage_tool
|
||||
from .update_memory import create_update_memory_tool, create_update_team_memory_tool
|
||||
from .video_presentation import create_generate_video_presentation_tool
|
||||
|
||||
__all__ = [
|
||||
# Registry
|
||||
"BUILTIN_TOOLS",
|
||||
# Knowledge base utilities
|
||||
"CONNECTOR_DESCRIPTIONS",
|
||||
"ToolDefinition",
|
||||
"build_tools",
|
||||
# Tool factories
|
||||
"create_generate_image_tool",
|
||||
"create_generate_podcast_tool",
|
||||
"create_generate_video_presentation_tool",
|
||||
"create_scrape_webpage_tool",
|
||||
"create_update_memory_tool",
|
||||
"create_update_team_memory_tool",
|
||||
"format_documents_for_context",
|
||||
"get_all_tool_names",
|
||||
"get_default_enabled_tools",
|
||||
"get_tool_by_name",
|
||||
"search_knowledge_base_async",
|
||||
]
|
||||
|
|
@ -5,7 +5,7 @@ from langchain_core.tools import tool
|
|||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
|
||||
from app.agents.new_chat.tools.hitl import request_approval
|
||||
from app.agents.shared.tools.hitl import request_approval
|
||||
from app.connectors.confluence_history import ConfluenceHistoryConnector
|
||||
from app.db import async_session_maker
|
||||
from app.services.confluence import ConfluenceToolMetadataService
|
||||
|
|
@ -5,7 +5,7 @@ from langchain_core.tools import tool
|
|||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
|
||||
from app.agents.new_chat.tools.hitl import request_approval
|
||||
from app.agents.shared.tools.hitl import request_approval
|
||||
from app.connectors.confluence_history import ConfluenceHistoryConnector
|
||||
from app.db import async_session_maker
|
||||
from app.services.confluence import ConfluenceToolMetadataService
|
||||
|
|
@ -5,7 +5,7 @@ from langchain_core.tools import tool
|
|||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
|
||||
from app.agents.new_chat.tools.hitl import request_approval
|
||||
from app.agents.shared.tools.hitl import request_approval
|
||||
from app.connectors.confluence_history import ConfluenceHistoryConnector
|
||||
from app.db import async_session_maker
|
||||
from app.services.confluence import ConfluenceToolMetadataService
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
from app.agents.new_chat.tools.discord.list_channels import (
|
||||
from app.agents.shared.tools.discord.list_channels import (
|
||||
create_list_discord_channels_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.discord.read_messages import (
|
||||
from app.agents.shared.tools.discord.read_messages import (
|
||||
create_read_discord_messages_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.discord.send_message import (
|
||||
from app.agents.shared.tools.discord.send_message import (
|
||||
create_send_discord_message_tool,
|
||||
)
|
||||
|
||||
|
|
@ -5,7 +5,7 @@ import httpx
|
|||
from langchain_core.tools import tool
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.agents.new_chat.tools.hitl import request_approval
|
||||
from app.agents.shared.tools.hitl import request_approval
|
||||
from app.db import async_session_maker
|
||||
|
||||
from ._auth import DISCORD_API, get_bot_token, get_discord_connector
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
from app.agents.new_chat.tools.dropbox.create_file import (
|
||||
from app.agents.shared.tools.dropbox.create_file import (
|
||||
create_create_dropbox_file_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.dropbox.trash_file import (
|
||||
from app.agents.shared.tools.dropbox.trash_file import (
|
||||
create_delete_dropbox_file_tool,
|
||||
)
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ from langchain_core.tools import tool
|
|||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
|
||||
from app.agents.new_chat.tools.hitl import request_approval
|
||||
from app.agents.shared.tools.hitl import request_approval
|
||||
from app.connectors.dropbox.client import DropboxClient
|
||||
from app.db import SearchSourceConnector, SearchSourceConnectorType, async_session_maker
|
||||
|
||||
|
|
@ -6,7 +6,7 @@ from sqlalchemy import String, and_, cast, func
|
|||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
|
||||
from app.agents.new_chat.tools.hitl import request_approval
|
||||
from app.agents.shared.tools.hitl import request_approval
|
||||
from app.connectors.dropbox.client import DropboxClient
|
||||
from app.db import (
|
||||
Document,
|
||||
|
|
@ -1,19 +1,19 @@
|
|||
from app.agents.new_chat.tools.gmail.create_draft import (
|
||||
from app.agents.shared.tools.gmail.create_draft import (
|
||||
create_create_gmail_draft_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.gmail.read_email import (
|
||||
from app.agents.shared.tools.gmail.read_email import (
|
||||
create_read_gmail_email_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.gmail.search_emails import (
|
||||
from app.agents.shared.tools.gmail.search_emails import (
|
||||
create_search_gmail_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.gmail.send_email import (
|
||||
from app.agents.shared.tools.gmail.send_email import (
|
||||
create_send_gmail_email_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.gmail.trash_email import (
|
||||
from app.agents.shared.tools.gmail.trash_email import (
|
||||
create_trash_gmail_email_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.gmail.update_draft import (
|
||||
from app.agents.shared.tools.gmail.update_draft import (
|
||||
create_update_gmail_draft_tool,
|
||||
)
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ from typing import Any
|
|||
from langchain_core.tools import tool
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.agents.new_chat.tools.hitl import request_approval
|
||||
from app.agents.shared.tools.hitl import request_approval
|
||||
from app.db import async_session_maker
|
||||
from app.services.gmail import GmailToolMetadataService
|
||||
|
||||
|
|
@ -241,7 +241,7 @@ def create_create_gmail_draft_tool(
|
|||
|
||||
try:
|
||||
if is_composio_gmail:
|
||||
from app.agents.new_chat.tools.gmail.composio_helpers import (
|
||||
from app.agents.shared.tools.gmail.composio_helpers import (
|
||||
execute_composio_gmail_tool,
|
||||
split_recipients,
|
||||
)
|
||||
|
|
@ -79,7 +79,7 @@ def create_read_gmail_email_tool(
|
|||
"message": "Composio connected account ID not found.",
|
||||
}
|
||||
|
||||
from app.agents.new_chat.tools.gmail.search_emails import (
|
||||
from app.agents.shared.tools.gmail.search_emails import (
|
||||
_format_gmail_summary,
|
||||
)
|
||||
from app.services.composio_service import ComposioService
|
||||
|
|
@ -116,7 +116,7 @@ def create_read_gmail_email_tool(
|
|||
"content": content,
|
||||
}
|
||||
|
||||
from app.agents.new_chat.tools.gmail.search_emails import (
|
||||
from app.agents.shared.tools.gmail.search_emails import (
|
||||
_build_credentials,
|
||||
)
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ from typing import Any
|
|||
from langchain_core.tools import tool
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.agents.new_chat.tools.hitl import request_approval
|
||||
from app.agents.shared.tools.hitl import request_approval
|
||||
from app.db import async_session_maker
|
||||
from app.services.gmail import GmailToolMetadataService
|
||||
|
||||
|
|
@ -242,7 +242,7 @@ def create_send_gmail_email_tool(
|
|||
|
||||
try:
|
||||
if is_composio_gmail:
|
||||
from app.agents.new_chat.tools.gmail.composio_helpers import (
|
||||
from app.agents.shared.tools.gmail.composio_helpers import (
|
||||
execute_composio_gmail_tool,
|
||||
split_recipients,
|
||||
)
|
||||
|
|
@ -6,7 +6,7 @@ from typing import Any
|
|||
from langchain_core.tools import tool
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.agents.new_chat.tools.hitl import request_approval
|
||||
from app.agents.shared.tools.hitl import request_approval
|
||||
from app.db import async_session_maker
|
||||
from app.services.gmail import GmailToolMetadataService
|
||||
|
||||
|
|
@ -233,7 +233,7 @@ def create_trash_gmail_email_tool(
|
|||
|
||||
try:
|
||||
if is_composio_gmail:
|
||||
from app.agents.new_chat.tools.gmail.composio_helpers import (
|
||||
from app.agents.shared.tools.gmail.composio_helpers import (
|
||||
execute_composio_gmail_tool,
|
||||
)
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ from typing import Any
|
|||
from langchain_core.tools import tool
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.agents.new_chat.tools.hitl import request_approval
|
||||
from app.agents.shared.tools.hitl import request_approval
|
||||
from app.db import async_session_maker
|
||||
from app.services.gmail import GmailToolMetadataService
|
||||
|
||||
|
|
@ -297,7 +297,7 @@ def create_update_gmail_draft_tool(
|
|||
|
||||
try:
|
||||
if is_composio_gmail:
|
||||
from app.agents.new_chat.tools.gmail.composio_helpers import (
|
||||
from app.agents.shared.tools.gmail.composio_helpers import (
|
||||
execute_composio_gmail_tool,
|
||||
split_recipients,
|
||||
)
|
||||
|
|
@ -466,7 +466,7 @@ async def _find_draft_id_by_message(gmail_service: Any, message_id: str) -> str
|
|||
async def _find_composio_draft_id_by_message(
|
||||
connector: Any, user_id: str, message_id: str
|
||||
) -> str | None:
|
||||
from app.agents.new_chat.tools.gmail.composio_helpers import (
|
||||
from app.agents.shared.tools.gmail.composio_helpers import (
|
||||
execute_composio_gmail_tool,
|
||||
)
|
||||
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
from app.agents.new_chat.tools.google_calendar.create_event import (
|
||||
from app.agents.shared.tools.google_calendar.create_event import (
|
||||
create_create_calendar_event_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.google_calendar.delete_event import (
|
||||
from app.agents.shared.tools.google_calendar.delete_event import (
|
||||
create_delete_calendar_event_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.google_calendar.search_events import (
|
||||
from app.agents.shared.tools.google_calendar.search_events import (
|
||||
create_search_calendar_events_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.google_calendar.update_event import (
|
||||
from app.agents.shared.tools.google_calendar.update_event import (
|
||||
create_update_calendar_event_tool,
|
||||
)
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ from googleapiclient.discovery import build
|
|||
from langchain_core.tools import tool
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.agents.new_chat.tools.hitl import request_approval
|
||||
from app.agents.shared.tools.hitl import request_approval
|
||||
from app.db import async_session_maker
|
||||
from app.services.google_calendar import GoogleCalendarToolMetadataService
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ from googleapiclient.discovery import build
|
|||
from langchain_core.tools import tool
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.agents.new_chat.tools.hitl import request_approval
|
||||
from app.agents.shared.tools.hitl import request_approval
|
||||
from app.db import async_session_maker
|
||||
from app.services.google_calendar import GoogleCalendarToolMetadataService
|
||||
|
||||
|
|
@ -5,7 +5,7 @@ from langchain_core.tools import tool
|
|||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
|
||||
from app.agents.new_chat.tools.gmail.search_emails import _build_credentials
|
||||
from app.agents.shared.tools.gmail.search_emails import _build_credentials
|
||||
from app.db import SearchSourceConnector, SearchSourceConnectorType, async_session_maker
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -8,7 +8,7 @@ from googleapiclient.discovery import build
|
|||
from langchain_core.tools import tool
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.agents.new_chat.tools.hitl import request_approval
|
||||
from app.agents.shared.tools.hitl import request_approval
|
||||
from app.db import async_session_maker
|
||||
from app.services.google_calendar import GoogleCalendarToolMetadataService
|
||||
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
from app.agents.new_chat.tools.google_drive.create_file import (
|
||||
from app.agents.shared.tools.google_drive.create_file import (
|
||||
create_create_google_drive_file_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.google_drive.trash_file import (
|
||||
from app.agents.shared.tools.google_drive.trash_file import (
|
||||
create_delete_google_drive_file_tool,
|
||||
)
|
||||
|
||||
|
|
@ -5,7 +5,7 @@ from googleapiclient.errors import HttpError
|
|||
from langchain_core.tools import tool
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.agents.new_chat.tools.hitl import request_approval
|
||||
from app.agents.shared.tools.hitl import request_approval
|
||||
from app.connectors.google_drive.client import GoogleDriveClient
|
||||
from app.connectors.google_drive.file_types import GOOGLE_DOC, GOOGLE_SHEET
|
||||
from app.db import async_session_maker
|
||||
|
|
@ -5,7 +5,7 @@ from googleapiclient.errors import HttpError
|
|||
from langchain_core.tools import tool
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.agents.new_chat.tools.hitl import request_approval
|
||||
from app.agents.shared.tools.hitl import request_approval
|
||||
from app.connectors.google_drive.client import GoogleDriveClient
|
||||
from app.db import async_session_maker
|
||||
from app.services.google_drive import GoogleDriveToolMetadataService
|
||||
|
|
@ -6,7 +6,7 @@ shared by every sensitive tool (native connectors and MCP tools alike).
|
|||
|
||||
Usage inside a tool::
|
||||
|
||||
from app.agents.new_chat.tools.hitl import request_approval
|
||||
from app.agents.shared.tools.hitl import request_approval
|
||||
|
||||
result = request_approval(
|
||||
action_type="gmail_email_send",
|
||||
53
surfsense_backend/app/agents/shared/tools/invalid_tool.py
Normal file
53
surfsense_backend/app/agents/shared/tools/invalid_tool.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
"""
|
||||
The ``invalid`` fallback tool.
|
||||
|
||||
When the model emits a tool call whose name doesn't match any registered
|
||||
tool, :class:`ToolCallNameRepairMiddleware` rewrites the call to ``invalid``
|
||||
with the original name and a parser/validation error string. This tool's
|
||||
execution then returns that error to the model so it can self-correct.
|
||||
|
||||
Ported from OpenCode's ``packages/opencode/src/tool/invalid.ts`` —
|
||||
LangChain has no equivalent fallback path; the default behavior on an
|
||||
unknown tool name is a hard ``ToolNotFoundError`` which kills the turn.
|
||||
|
||||
Critically, the :class:`ToolDefinition` for this tool is **excluded** from
|
||||
the system-prompt tool list and from ``LLMToolSelectorMiddleware`` selection
|
||||
(see ``ToolDefinition.always_include`` filtering in the registry) — the
|
||||
model never advertises ``invalid`` as a callable. It only ever shows up
|
||||
in the tool registry so LangGraph can dispatch the rewritten call.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from langchain_core.tools import tool
|
||||
|
||||
INVALID_TOOL_NAME = "invalid"
|
||||
INVALID_TOOL_DESCRIPTION = "Do not use"
|
||||
|
||||
|
||||
def _format_invalid_message(tool: str | None, error: str | None) -> str:
|
||||
"""Return the user-visible error string. Mirrors ``invalid.ts``."""
|
||||
name = tool or "<unknown>"
|
||||
detail = error or "(no error message provided)"
|
||||
return (
|
||||
f"The arguments provided to the tool `{name}` are invalid: {detail}\n"
|
||||
f"Read the tool's docstring carefully and try again with valid arguments."
|
||||
)
|
||||
|
||||
|
||||
@tool(name_or_callable=INVALID_TOOL_NAME, description=INVALID_TOOL_DESCRIPTION)
|
||||
def invalid_tool(tool: str | None = None, error: str | None = None) -> str:
|
||||
"""Return a human-readable explanation of a tool-call validation failure.
|
||||
|
||||
Activated only when :class:`ToolCallNameRepairMiddleware` rewrites a
|
||||
failed tool call to ``invalid`` with the original tool name and the
|
||||
error message produced during validation.
|
||||
"""
|
||||
return _format_invalid_message(tool, error)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"INVALID_TOOL_DESCRIPTION",
|
||||
"INVALID_TOOL_NAME",
|
||||
"invalid_tool",
|
||||
]
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
from app.agents.new_chat.tools.luma.create_event import (
|
||||
from app.agents.shared.tools.luma.create_event import (
|
||||
create_create_luma_event_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.luma.list_events import (
|
||||
from app.agents.shared.tools.luma.list_events import (
|
||||
create_list_luma_events_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.luma.read_event import (
|
||||
from app.agents.shared.tools.luma.read_event import (
|
||||
create_read_luma_event_tool,
|
||||
)
|
||||
|
||||
|
|
@ -5,7 +5,7 @@ import httpx
|
|||
from langchain_core.tools import tool
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.agents.new_chat.tools.hitl import request_approval
|
||||
from app.agents.shared.tools.hitl import request_approval
|
||||
from app.db import async_session_maker
|
||||
|
||||
from ._auth import LUMA_API, get_api_key, get_luma_connector, luma_headers
|
||||
|
|
@ -34,9 +34,9 @@ from sqlalchemy.dialects.postgresql import JSONB
|
|||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.agents.shared.middleware.dedup_tool_calls import dedup_key_full_args
|
||||
from app.agents.new_chat.tools.hitl import request_approval
|
||||
from app.agents.new_chat.tools.mcp_client import MCPClient
|
||||
from app.agents.new_chat.tools.mcp_tools_cache import (
|
||||
from app.agents.shared.tools.hitl import request_approval
|
||||
from app.agents.shared.tools.mcp_client import MCPClient
|
||||
from app.agents.shared.tools.mcp_tools_cache import (
|
||||
CachedMCPTools,
|
||||
read_cached_tools,
|
||||
write_cached_tools,
|
||||
|
|
@ -112,7 +112,7 @@ def refresh_mcp_tools_cache_for_connector(
|
|||
when an event loop is available. Neither path raises.
|
||||
"""
|
||||
try:
|
||||
from app.agents.new_chat.tools.mcp_tool import invalidate_mcp_tools_cache
|
||||
from app.agents.shared.tools.mcp_tool import invalidate_mcp_tools_cache
|
||||
|
||||
invalidate_mcp_tools_cache(search_space_id)
|
||||
except Exception:
|
||||
|
|
@ -133,7 +133,7 @@ def refresh_mcp_tools_cache_for_connector(
|
|||
|
||||
|
||||
async def _run_connector_prefetch(connector_id: int) -> None:
|
||||
from app.agents.new_chat.tools.mcp_tool import discover_single_mcp_connector
|
||||
from app.agents.shared.tools.mcp_tool import discover_single_mcp_connector
|
||||
|
||||
try:
|
||||
await discover_single_mcp_connector(connector_id)
|
||||
|
|
@ -4,7 +4,7 @@ from typing import Any
|
|||
from langchain_core.tools import tool
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.agents.new_chat.tools.hitl import request_approval
|
||||
from app.agents.shared.tools.hitl import request_approval
|
||||
from app.connectors.notion_history import NotionAPIError, NotionHistoryConnector
|
||||
from app.db import async_session_maker
|
||||
from app.services.notion import NotionToolMetadataService
|
||||
|
|
@ -4,7 +4,7 @@ from typing import Any
|
|||
from langchain_core.tools import tool
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.agents.new_chat.tools.hitl import request_approval
|
||||
from app.agents.shared.tools.hitl import request_approval
|
||||
from app.connectors.notion_history import NotionAPIError, NotionHistoryConnector
|
||||
from app.db import async_session_maker
|
||||
from app.services.notion.tool_metadata_service import NotionToolMetadataService
|
||||
|
|
@ -4,7 +4,7 @@ from typing import Any
|
|||
from langchain_core.tools import tool
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.agents.new_chat.tools.hitl import request_approval
|
||||
from app.agents.shared.tools.hitl import request_approval
|
||||
from app.connectors.notion_history import NotionAPIError, NotionHistoryConnector
|
||||
from app.db import async_session_maker
|
||||
from app.services.notion import NotionToolMetadataService
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
from app.agents.new_chat.tools.onedrive.create_file import (
|
||||
from app.agents.shared.tools.onedrive.create_file import (
|
||||
create_create_onedrive_file_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.onedrive.trash_file import (
|
||||
from app.agents.shared.tools.onedrive.trash_file import (
|
||||
create_delete_onedrive_file_tool,
|
||||
)
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ from langchain_core.tools import tool
|
|||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
|
||||
from app.agents.new_chat.tools.hitl import request_approval
|
||||
from app.agents.shared.tools.hitl import request_approval
|
||||
from app.connectors.onedrive.client import OneDriveClient
|
||||
from app.db import SearchSourceConnector, SearchSourceConnectorType, async_session_maker
|
||||
|
||||
|
|
@ -6,7 +6,7 @@ from sqlalchemy import String, and_, cast, func
|
|||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
|
||||
from app.agents.new_chat.tools.hitl import request_approval
|
||||
from app.agents.shared.tools.hitl import request_approval
|
||||
from app.connectors.onedrive.client import OneDriveClient
|
||||
from app.db import (
|
||||
Document,
|
||||
962
surfsense_backend/app/agents/shared/tools/registry.py
Normal file
962
surfsense_backend/app/agents/shared/tools/registry.py
Normal file
|
|
@ -0,0 +1,962 @@
|
|||
"""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 .mcp_tool import load_mcp_tools
|
||||
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 .scrape_webpage import create_scrape_webpage_tool
|
||||
from .teams import (
|
||||
create_list_teams_channels_tool,
|
||||
create_read_teams_messages_tool,
|
||||
create_send_teams_message_tool,
|
||||
)
|
||||
from .update_memory import create_update_memory_tool, create_update_team_memory_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"],
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 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=lambda deps: create_scrape_webpage_tool(
|
||||
firecrawl_api_key=deps.get("firecrawl_api_key"),
|
||||
),
|
||||
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=lambda deps: (
|
||||
create_update_team_memory_tool(
|
||||
search_space_id=deps["search_space_id"],
|
||||
db_session=deps["db_session"],
|
||||
llm=deps.get("llm"),
|
||||
)
|
||||
if deps["thread_visibility"] == ChatVisibility.SEARCH_SPACE
|
||||
else create_update_memory_tool(
|
||||
user_id=deps["user_id"],
|
||||
db_session=deps["db_session"],
|
||||
llm=deps.get("llm"),
|
||||
)
|
||||
),
|
||||
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 (see chat_deepagent.py)
|
||||
# =========================================================================
|
||||
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 (see chat_deepagent.py)
|
||||
# =========================================================================
|
||||
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 (see chat_deepagent.py)
|
||||
# =========================================================================
|
||||
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 (see chat_deepagent.py)
|
||||
# =========================================================================
|
||||
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 (see chat_deepagent.py)
|
||||
# =========================================================================
|
||||
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_tool_by_name(name: str) -> ToolDefinition | None:
|
||||
"""Get a tool definition by its name."""
|
||||
for tool_def in BUILTIN_TOOLS:
|
||||
if tool_def.name == name:
|
||||
return tool_def
|
||||
return None
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def get_all_tool_names() -> list[str]:
|
||||
"""Get names of all registered tools."""
|
||||
return [tool_def.name for tool_def in BUILTIN_TOOLS]
|
||||
|
||||
|
||||
def get_default_enabled_tools() -> list[str]:
|
||||
"""Get names of tools that are enabled by default (excludes hidden tools)."""
|
||||
return [tool_def.name for tool_def in BUILTIN_TOOLS if tool_def.enabled_by_default]
|
||||
|
||||
|
||||
def build_tools(
|
||||
dependencies: dict[str, Any],
|
||||
enabled_tools: list[str] | None = None,
|
||||
disabled_tools: list[str] | None = None,
|
||||
additional_tools: list[BaseTool] | None = None,
|
||||
) -> list[BaseTool]:
|
||||
"""Build the list of tools for the agent.
|
||||
|
||||
Args:
|
||||
dependencies: Dict containing all possible dependencies:
|
||||
- search_space_id: The search space ID
|
||||
- db_session: Database session
|
||||
- connector_service: Connector service instance
|
||||
- firecrawl_api_key: Optional Firecrawl API key
|
||||
enabled_tools: Explicit list of tool names to enable. If None, uses defaults.
|
||||
disabled_tools: List of tool names to disable (applied after enabled_tools).
|
||||
additional_tools: Extra tools to add (e.g., custom tools not in registry).
|
||||
|
||||
Returns:
|
||||
List of configured tool instances ready for the agent.
|
||||
|
||||
Example:
|
||||
# Use all default tools
|
||||
tools = build_tools(deps)
|
||||
|
||||
# Use only specific tools
|
||||
tools = build_tools(deps, enabled_tools=["generate_report"])
|
||||
|
||||
# Use defaults but disable podcast
|
||||
tools = build_tools(deps, disabled_tools=["generate_podcast"])
|
||||
|
||||
# Add custom tools
|
||||
tools = build_tools(deps, additional_tools=[my_custom_tool])
|
||||
|
||||
"""
|
||||
# Determine which tools to enable
|
||||
if enabled_tools is not None:
|
||||
tool_names_to_use = set(enabled_tools)
|
||||
else:
|
||||
tool_names_to_use = set(get_default_enabled_tools())
|
||||
|
||||
# Apply disabled list
|
||||
if disabled_tools:
|
||||
tool_names_to_use -= set(disabled_tools)
|
||||
|
||||
# Build the tools (skip hidden/WIP tools unconditionally)
|
||||
tools: list[BaseTool] = []
|
||||
for tool_def in BUILTIN_TOOLS:
|
||||
if tool_def.hidden or tool_def.name not in tool_names_to_use:
|
||||
continue
|
||||
|
||||
# Check that all required dependencies are provided
|
||||
missing_deps = [dep for dep in tool_def.requires if dep not in dependencies]
|
||||
if missing_deps:
|
||||
msg = f"Tool '{tool_def.name}' requires dependencies: {missing_deps}"
|
||||
raise ValueError(
|
||||
msg,
|
||||
)
|
||||
|
||||
# Create the tool
|
||||
tool = tool_def.factory(dependencies)
|
||||
# Propagate the registry-level metadata so middleware (e.g.
|
||||
# ``DedupHITLToolCallsMiddleware``) and the action-log/revert
|
||||
# pipeline can pick the resolvers up via ``tool.metadata`` without
|
||||
# re-importing :data:`BUILTIN_TOOLS`.
|
||||
if tool_def.dedup_key is not None or tool_def.reverse is not None:
|
||||
existing_meta = getattr(tool, "metadata", None) or {}
|
||||
merged_meta = dict(existing_meta)
|
||||
if tool_def.dedup_key is not None:
|
||||
merged_meta.setdefault("dedup_key", tool_def.dedup_key)
|
||||
if tool_def.reverse is not None:
|
||||
merged_meta.setdefault("reverse", tool_def.reverse)
|
||||
try:
|
||||
tool.metadata = merged_meta
|
||||
except Exception:
|
||||
logger.debug(
|
||||
"Tool %s rejected metadata mutation; relying on registry lookup",
|
||||
tool_def.name,
|
||||
)
|
||||
tools.append(tool)
|
||||
|
||||
# Add any additional custom tools
|
||||
if additional_tools:
|
||||
tools.extend(additional_tools)
|
||||
|
||||
return tools
|
||||
|
||||
|
||||
async def build_tools_async(
|
||||
dependencies: dict[str, Any],
|
||||
enabled_tools: list[str] | None = None,
|
||||
disabled_tools: list[str] | None = None,
|
||||
additional_tools: list[BaseTool] | None = None,
|
||||
include_mcp_tools: bool = True,
|
||||
) -> list[BaseTool]:
|
||||
"""Async version of build_tools that also loads MCP tools from database.
|
||||
|
||||
Design Note:
|
||||
This function exists because MCP tools require database queries to load
|
||||
user configs, while built-in tools are created synchronously from static
|
||||
code.
|
||||
|
||||
Alternative: We could make build_tools() itself async and always query
|
||||
the database, but that would force async everywhere even when only using
|
||||
built-in tools. The current design keeps the simple case (static tools
|
||||
only) synchronous while supporting dynamic database-loaded tools through
|
||||
this async wrapper.
|
||||
|
||||
Phase 1.3: built-in tool construction (CPU; runs in a thread pool to
|
||||
avoid event-loop stalls) and MCP tool loading (HTTP/DB I/O; runs on
|
||||
the event loop) are kicked off concurrently. Cold-path savings are
|
||||
bounded by the slower of the two — typically MCP at ~200ms-1.7s —
|
||||
so the parallelization recovers the ~50-200ms previously spent
|
||||
serially on built-in construction.
|
||||
|
||||
Args:
|
||||
dependencies: Dict containing all possible dependencies
|
||||
enabled_tools: Explicit list of tool names to enable. If None, uses defaults.
|
||||
disabled_tools: List of tool names to disable (applied after enabled_tools).
|
||||
additional_tools: Extra tools to add (e.g., custom tools not in registry).
|
||||
include_mcp_tools: Whether to load user's MCP tools from database.
|
||||
|
||||
Returns:
|
||||
List of configured tool instances ready for the agent, including MCP tools.
|
||||
|
||||
"""
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
_perf_log = logging.getLogger("surfsense.perf")
|
||||
_perf_log.setLevel(logging.DEBUG)
|
||||
|
||||
can_load_mcp = (
|
||||
include_mcp_tools
|
||||
and "db_session" in dependencies
|
||||
and "search_space_id" in dependencies
|
||||
)
|
||||
|
||||
# Built-in tool construction is synchronous + CPU-only. Off-loop it so
|
||||
# MCP's HTTP/DB I/O can fire concurrently. ``build_tools`` is pure
|
||||
# function over its inputs — safe to thread-shift.
|
||||
_t0 = time.perf_counter()
|
||||
builtin_task = asyncio.create_task(
|
||||
asyncio.to_thread(
|
||||
build_tools, dependencies, enabled_tools, disabled_tools, additional_tools
|
||||
)
|
||||
)
|
||||
|
||||
mcp_task: asyncio.Task | None = None
|
||||
if can_load_mcp:
|
||||
mcp_task = asyncio.create_task(
|
||||
load_mcp_tools(
|
||||
dependencies["db_session"],
|
||||
dependencies["search_space_id"],
|
||||
)
|
||||
)
|
||||
|
||||
# Surface failures from each task independently so a flaky MCP
|
||||
# endpoint never poisons built-in tool registration. ``return_exceptions``
|
||||
# gives us per-task exceptions instead of dropping the second result
|
||||
# when the first raises.
|
||||
if mcp_task is not None:
|
||||
builtin_result, mcp_result = await asyncio.gather(
|
||||
builtin_task, mcp_task, return_exceptions=True
|
||||
)
|
||||
else:
|
||||
builtin_result = await builtin_task
|
||||
mcp_result = None
|
||||
|
||||
if isinstance(builtin_result, BaseException):
|
||||
raise builtin_result # built-in registration failure is non-recoverable
|
||||
tools: list[BaseTool] = builtin_result
|
||||
_perf_log.info(
|
||||
"[build_tools_async] Built-in tools in %.3fs (%d tools, parallel)",
|
||||
time.perf_counter() - _t0,
|
||||
len(tools),
|
||||
)
|
||||
|
||||
if mcp_task is not None:
|
||||
if isinstance(mcp_result, BaseException):
|
||||
# ``return_exceptions=True`` captures the exception out-of-band,
|
||||
# so ``sys.exc_info()`` is empty here. Pass the captured
|
||||
# exception via ``exc_info=`` to get a real traceback.
|
||||
logging.error(
|
||||
"Failed to load MCP tools: %s", mcp_result, exc_info=mcp_result
|
||||
)
|
||||
else:
|
||||
mcp_tools = mcp_result or []
|
||||
_perf_log.info(
|
||||
"[build_tools_async] MCP tools loaded in %.3fs (%d tools, parallel)",
|
||||
time.perf_counter() - _t0,
|
||||
len(mcp_tools),
|
||||
)
|
||||
tools.extend(mcp_tools)
|
||||
logging.info(
|
||||
"Registered %d MCP tools: %s",
|
||||
len(mcp_tools),
|
||||
[t.name for t in mcp_tools],
|
||||
)
|
||||
|
||||
logging.info(
|
||||
"Total tools for agent: %d — %s",
|
||||
len(tools),
|
||||
[t.name for t in tools],
|
||||
)
|
||||
|
||||
return tools
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
from app.agents.new_chat.tools.teams.list_channels import (
|
||||
from app.agents.shared.tools.teams.list_channels import (
|
||||
create_list_teams_channels_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.teams.read_messages import (
|
||||
from app.agents.shared.tools.teams.read_messages import (
|
||||
create_read_teams_messages_tool,
|
||||
)
|
||||
from app.agents.new_chat.tools.teams.send_message import (
|
||||
from app.agents.shared.tools.teams.send_message import (
|
||||
create_send_teams_message_tool,
|
||||
)
|
||||
|
||||
|
|
@ -5,7 +5,7 @@ import httpx
|
|||
from langchain_core.tools import tool
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.agents.new_chat.tools.hitl import request_approval
|
||||
from app.agents.shared.tools.hitl import request_approval
|
||||
from app.db import async_session_maker
|
||||
|
||||
from ._auth import GRAPH_API, get_access_token, get_teams_connector
|
||||
|
|
@ -665,7 +665,7 @@ def _refresh_mcp_cache(connector_id: int, space_id: int) -> None:
|
|||
isolated from the OAuth response flow.
|
||||
"""
|
||||
try:
|
||||
from app.agents.new_chat.tools.mcp_tools_cache import (
|
||||
from app.agents.shared.tools.mcp_tools_cache import (
|
||||
refresh_mcp_tools_cache_for_connector,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1668,7 +1668,7 @@ async def list_agent_tools(
|
|||
|
||||
Hidden (WIP) tools are excluded from the response.
|
||||
"""
|
||||
from app.agents.new_chat.tools.registry import BUILTIN_TOOLS
|
||||
from app.agents.shared.tools.registry import BUILTIN_TOOLS
|
||||
|
||||
return [
|
||||
AgentToolInfo(
|
||||
|
|
|
|||
|
|
@ -675,7 +675,7 @@ async def delete_search_source_connector(
|
|||
await session.commit()
|
||||
|
||||
if is_mcp:
|
||||
from app.agents.new_chat.tools.mcp_tool import invalidate_mcp_tools_cache
|
||||
from app.agents.shared.tools.mcp_tool import invalidate_mcp_tools_cache
|
||||
|
||||
invalidate_mcp_tools_cache(search_space_id)
|
||||
|
||||
|
|
@ -2687,7 +2687,7 @@ async def create_mcp_connector(
|
|||
f"for user {user.id} in search space {search_space_id}"
|
||||
)
|
||||
|
||||
from app.agents.new_chat.tools.mcp_tools_cache import (
|
||||
from app.agents.shared.tools.mcp_tools_cache import (
|
||||
refresh_mcp_tools_cache_for_connector,
|
||||
)
|
||||
|
||||
|
|
@ -2867,7 +2867,7 @@ async def update_mcp_connector(
|
|||
|
||||
logger.info(f"Updated MCP connector {connector_id}")
|
||||
|
||||
from app.agents.new_chat.tools.mcp_tools_cache import (
|
||||
from app.agents.shared.tools.mcp_tools_cache import (
|
||||
refresh_mcp_tools_cache_for_connector,
|
||||
)
|
||||
|
||||
|
|
@ -2927,7 +2927,7 @@ async def delete_mcp_connector(
|
|||
await session.delete(connector)
|
||||
await session.commit()
|
||||
|
||||
from app.agents.new_chat.tools.mcp_tool import invalidate_mcp_tools_cache
|
||||
from app.agents.shared.tools.mcp_tool import invalidate_mcp_tools_cache
|
||||
|
||||
invalidate_mcp_tools_cache(search_space_id)
|
||||
|
||||
|
|
@ -2966,7 +2966,7 @@ async def test_mcp_server_connection(
|
|||
Connection status and list of available tools
|
||||
"""
|
||||
try:
|
||||
from app.agents.new_chat.tools.mcp_client import (
|
||||
from app.agents.shared.tools.mcp_client import (
|
||||
test_mcp_connection,
|
||||
test_mcp_http_connection,
|
||||
)
|
||||
|
|
@ -3157,7 +3157,7 @@ async def trust_mcp_tool(
|
|||
connectors (``LINEAR_CONNECTOR``, ``JIRA_CONNECTOR``, ...) — the
|
||||
storage primitive is the same JSON list under ``config.trusted_tools``.
|
||||
"""
|
||||
from app.agents.new_chat.tools.mcp_tool import invalidate_mcp_tools_cache
|
||||
from app.agents.shared.tools.mcp_tool import invalidate_mcp_tools_cache
|
||||
from app.services.user_tool_allowlist import add_user_trust
|
||||
|
||||
try:
|
||||
|
|
@ -3197,7 +3197,7 @@ async def untrust_mcp_tool(
|
|||
|
||||
The tool will require HITL approval again on subsequent calls.
|
||||
"""
|
||||
from app.agents.new_chat.tools.mcp_tool import invalidate_mcp_tools_cache
|
||||
from app.agents.shared.tools.mcp_tool import invalidate_mcp_tools_cache
|
||||
from app.services.user_tool_allowlist import remove_user_trust
|
||||
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ logger = logging.getLogger(__name__)
|
|||
# class-body init time. ``app.agents.shared.llm_config`` re-exports
|
||||
# this constant under the historical ``PROVIDER_MAP`` name; placing the
|
||||
# map there directly would re-introduce the
|
||||
# ``app.config -> ... -> app.agents.new_chat.tools.generate_image ->
|
||||
# ``app.config -> ... -> app.agents.shared.tools.generate_image ->
|
||||
# app.config`` cycle that prompted the move.
|
||||
_PROVIDER_PREFIX_MAP: dict[str, str] = {
|
||||
"OPENAI": "openai",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue