From aab95b913048971b4cf8cfddb64e9b03f577479f Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Thu, 4 Jun 2026 13:11:56 +0200 Subject: [PATCH] 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. --- .../main_agent/runtime/factory.py | 4 +- .../middleware/main_agent/action_log.py | 2 +- .../connectors/calendar/tools/__init__.py | 8 +- .../calendar/tools/search_events.py | 2 +- .../connectors/discord/tools/__init__.py | 6 +- .../connectors/dropbox/tools/__init__.py | 4 +- .../connectors/gmail/tools/__init__.py | 12 +- .../connectors/gmail/tools/read_email.py | 4 +- .../connectors/gmail/tools/search_emails.py | 4 +- .../connectors/google_drive/tools/__init__.py | 4 +- .../connectors/luma/tools/__init__.py | 6 +- .../connectors/onedrive/tools/__init__.py | 4 +- .../connectors/teams/tools/__init__.py | 6 +- .../subagents/mcp_tools/index.py | 2 +- .../app/agents/new_chat/anonymous_agent.py | 2 +- .../app/agents/new_chat/tools/__init__.py | 43 +- .../app/agents/new_chat/tools/invalid_tool.py | 52 +- .../app/agents/new_chat/tools/registry.py | 969 +----------------- .../agents/shared/middleware/action_log.py | 4 +- .../shared/middleware/tool_call_repair.py | 2 +- .../app/agents/shared/tools/__init__.py | 55 + .../tools/confluence/__init__.py | 0 .../tools/confluence/create_page.py | 2 +- .../tools/confluence/delete_page.py | 2 +- .../tools/confluence/update_page.py | 2 +- .../tools/connected_accounts.py | 0 .../tools/discord/__init__.py | 6 +- .../tools/discord/_auth.py | 0 .../tools/discord/list_channels.py | 0 .../tools/discord/read_messages.py | 0 .../tools/discord/send_message.py | 2 +- .../tools/dropbox/__init__.py | 4 +- .../tools/dropbox/create_file.py | 2 +- .../tools/dropbox/trash_file.py | 2 +- .../tools/generate_image.py | 0 .../tools/gmail/__init__.py | 12 +- .../tools/gmail/composio_helpers.py | 0 .../tools/gmail/create_draft.py | 4 +- .../tools/gmail/read_email.py | 4 +- .../tools/gmail/search_emails.py | 0 .../tools/gmail/send_email.py | 4 +- .../tools/gmail/trash_email.py | 4 +- .../tools/gmail/update_draft.py | 6 +- .../tools/google_calendar/__init__.py | 8 +- .../tools/google_calendar/create_event.py | 2 +- .../tools/google_calendar/delete_event.py | 2 +- .../tools/google_calendar/search_events.py | 2 +- .../tools/google_calendar/update_event.py | 2 +- .../tools/google_drive/__init__.py | 4 +- .../tools/google_drive/create_file.py | 2 +- .../tools/google_drive/trash_file.py | 2 +- .../agents/{new_chat => shared}/tools/hitl.py | 2 +- .../app/agents/shared/tools/invalid_tool.py | 53 + .../tools/knowledge_base.py | 0 .../tools/luma/__init__.py | 6 +- .../{new_chat => shared}/tools/luma/_auth.py | 0 .../tools/luma/create_event.py | 2 +- .../tools/luma/list_events.py | 0 .../tools/luma/read_event.py | 0 .../{new_chat => shared}/tools/mcp_client.py | 0 .../{new_chat => shared}/tools/mcp_tool.py | 6 +- .../tools/mcp_tools_cache.py | 4 +- .../tools/notion/__init__.py | 0 .../tools/notion/create_page.py | 2 +- .../tools/notion/delete_page.py | 2 +- .../tools/notion/update_page.py | 2 +- .../tools/onedrive/__init__.py | 4 +- .../tools/onedrive/create_file.py | 2 +- .../tools/onedrive/trash_file.py | 2 +- .../{new_chat => shared}/tools/podcast.py | 0 .../app/agents/shared/tools/registry.py | 962 +++++++++++++++++ .../{new_chat => shared}/tools/report.py | 0 .../{new_chat => shared}/tools/resume.py | 0 .../tools/scrape_webpage.py | 0 .../tools/teams/__init__.py | 6 +- .../{new_chat => shared}/tools/teams/_auth.py | 0 .../tools/teams/list_channels.py | 0 .../tools/teams/read_messages.py | 0 .../tools/teams/send_message.py | 2 +- .../tools/update_memory.py | 0 .../tools/video_presentation.py | 0 .../{new_chat => shared}/tools/web_search.py | 0 .../app/routes/mcp_oauth_route.py | 2 +- .../app/routes/new_chat_routes.py | 2 +- .../routes/search_source_connectors_routes.py | 14 +- .../app/services/provider_capabilities.py | 2 +- .../tests/e2e/fakes/mcp_runtime.py | 4 +- .../tests/e2e/fakes/native_google.py | 6 +- .../google_unification/conftest.py | 2 +- .../test_browse_includes_legacy_docs.py | 2 +- .../unit/agents/new_chat/test_action_log.py | 2 +- .../agents/new_chat/test_dedup_tool_calls.py | 4 +- .../test_default_permissions_layering.py | 2 +- .../agents/new_chat/test_hitl_auto_approve.py | 2 +- .../agents/new_chat/test_tool_call_repair.py | 2 +- .../new_chat/tools/test_mcp_tools_cache.py | 2 +- .../new_chat/tools/test_resume_page_limits.py | 2 +- .../test_image_gen_api_base_defense.py | 2 +- 98 files changed, 1232 insertions(+), 1152 deletions(-) create mode 100644 surfsense_backend/app/agents/shared/tools/__init__.py rename surfsense_backend/app/agents/{new_chat => shared}/tools/confluence/__init__.py (100%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/confluence/create_page.py (99%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/confluence/delete_page.py (99%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/confluence/update_page.py (99%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/connected_accounts.py (100%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/discord/__init__.py (58%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/discord/_auth.py (100%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/discord/list_channels.py (100%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/discord/read_messages.py (100%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/discord/send_message.py (98%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/dropbox/__init__.py (58%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/dropbox/create_file.py (99%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/dropbox/trash_file.py (99%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/generate_image.py (100%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/gmail/__init__.py (56%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/gmail/composio_helpers.py (100%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/gmail/create_draft.py (99%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/gmail/read_email.py (97%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/gmail/search_emails.py (100%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/gmail/send_email.py (99%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/gmail/trash_email.py (99%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/gmail/update_draft.py (99%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/google_calendar/__init__.py (55%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/google_calendar/create_event.py (99%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/google_calendar/delete_event.py (99%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/google_calendar/search_events.py (98%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/google_calendar/update_event.py (99%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/google_drive/__init__.py (59%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/google_drive/create_file.py (99%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/google_drive/trash_file.py (99%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/hitl.py (99%) create mode 100644 surfsense_backend/app/agents/shared/tools/invalid_tool.py rename surfsense_backend/app/agents/{new_chat => shared}/tools/knowledge_base.py (100%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/luma/__init__.py (57%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/luma/_auth.py (100%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/luma/create_event.py (98%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/luma/list_events.py (100%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/luma/read_event.py (100%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/mcp_client.py (100%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/mcp_tool.py (99%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/mcp_tools_cache.py (96%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/notion/__init__.py (100%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/notion/create_page.py (99%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/notion/delete_page.py (99%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/notion/update_page.py (99%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/onedrive/__init__.py (59%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/onedrive/create_file.py (99%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/onedrive/trash_file.py (99%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/podcast.py (100%) create mode 100644 surfsense_backend/app/agents/shared/tools/registry.py rename surfsense_backend/app/agents/{new_chat => shared}/tools/report.py (100%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/resume.py (100%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/scrape_webpage.py (100%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/teams/__init__.py (57%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/teams/_auth.py (100%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/teams/list_channels.py (100%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/teams/read_messages.py (100%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/teams/send_message.py (98%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/update_memory.py (100%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/video_presentation.py (100%) rename surfsense_backend/app/agents/{new_chat => shared}/tools/web_search.py (100%) diff --git a/surfsense_backend/app/agents/multi_agent_chat/main_agent/runtime/factory.py b/surfsense_backend/app/agents/multi_agent_chat/main_agent/runtime/factory.py index 0f442b026..27f17b0db 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/main_agent/runtime/factory.py +++ b/surfsense_backend/app/agents/multi_agent_chat/main_agent/runtime/factory.py @@ -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 ( diff --git a/surfsense_backend/app/agents/multi_agent_chat/middleware/main_agent/action_log.py b/surfsense_backend/app/agents/multi_agent_chat/middleware/main_agent/action_log.py index 66758de8f..dac97b790 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/middleware/main_agent/action_log.py +++ b/surfsense_backend/app/agents/multi_agent_chat/middleware/main_agent/action_log.py @@ -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 diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/calendar/tools/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/calendar/tools/__init__.py index 13d4c06cb..362cf4127 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/calendar/tools/__init__.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/calendar/tools/__init__.py @@ -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, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/calendar/tools/search_events.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/calendar/tools/search_events.py index 6772d5a1e..2768563f4 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/calendar/tools/search_events.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/calendar/tools/search_events.py @@ -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__) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/discord/tools/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/discord/tools/__init__.py index b4eaec1f0..930f2bea1 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/discord/tools/__init__.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/discord/tools/__init__.py @@ -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, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/dropbox/tools/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/dropbox/tools/__init__.py index 836b9ee41..2db97cc60 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/dropbox/tools/__init__.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/dropbox/tools/__init__.py @@ -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, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/gmail/tools/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/gmail/tools/__init__.py index 294840122..f32312fe6 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/gmail/tools/__init__.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/gmail/tools/__init__.py @@ -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, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/gmail/tools/read_email.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/gmail/tools/read_email.py index 39526f25e..0636bf3d9 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/gmail/tools/read_email.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/gmail/tools/read_email.py @@ -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, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/gmail/tools/search_emails.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/gmail/tools/search_emails.py index a9d7cdedf..a3466cfa5 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/gmail/tools/search_emails.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/gmail/tools/search_emails.py @@ -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, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/google_drive/tools/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/google_drive/tools/__init__.py index 9c63bceb1..1f5feca60 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/google_drive/tools/__init__.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/google_drive/tools/__init__.py @@ -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, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/luma/tools/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/luma/tools/__init__.py index 255119bee..83af8c8c5 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/luma/tools/__init__.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/luma/tools/__init__.py @@ -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, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/onedrive/tools/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/onedrive/tools/__init__.py index 8edb4857e..04e6fc341 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/onedrive/tools/__init__.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/onedrive/tools/__init__.py @@ -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, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/teams/tools/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/teams/tools/__init__.py index 60e2add49..d9129fa82 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/teams/tools/__init__.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/teams/tools/__init__.py @@ -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, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/mcp_tools/index.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/mcp_tools/index.py index 16dc09ac5..76363937d 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/mcp_tools/index.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/mcp_tools/index.py @@ -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__) diff --git a/surfsense_backend/app/agents/new_chat/anonymous_agent.py b/surfsense_backend/app/agents/new_chat/anonymous_agent.py index 0e9f70d78..b3eab37ca 100644 --- a/surfsense_backend/app/agents/new_chat/anonymous_agent.py +++ b/surfsense_backend/app/agents/new_chat/anonymous_agent.py @@ -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 diff --git a/surfsense_backend/app/agents/new_chat/tools/__init__.py b/surfsense_backend/app/agents/new_chat/tools/__init__.py index 4b5ae3706..852fc813f 100644 --- a/surfsense_backend/app/agents/new_chat/tools/__init__.py +++ b/surfsense_backend/app/agents/new_chat/tools/__init__.py @@ -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", diff --git a/surfsense_backend/app/agents/new_chat/tools/invalid_tool.py b/surfsense_backend/app/agents/new_chat/tools/invalid_tool.py index ea4bc0bc1..cc7fe4c11 100644 --- a/surfsense_backend/app/agents/new_chat/tools/invalid_tool.py +++ b/surfsense_backend/app/agents/new_chat/tools/invalid_tool.py @@ -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 "" - 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", diff --git a/surfsense_backend/app/agents/new_chat/tools/registry.py b/surfsense_backend/app/agents/new_chat/tools/registry.py index 9b1944aa5..9b5d92559 100644 --- a/surfsense_backend/app/agents/new_chat/tools/registry.py +++ b/surfsense_backend/app/agents/new_chat/tools/registry.py @@ -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 diff --git a/surfsense_backend/app/agents/shared/middleware/action_log.py b/surfsense_backend/app/agents/shared/middleware/action_log.py index bba790c06..f26d78a4e 100644 --- a/surfsense_backend/app/agents/shared/middleware/action_log.py +++ b/surfsense_backend/app/agents/shared/middleware/action_log.py @@ -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__) diff --git a/surfsense_backend/app/agents/shared/middleware/tool_call_repair.py b/surfsense_backend/app/agents/shared/middleware/tool_call_repair.py index 96154e7ab..966a1c75e 100644 --- a/surfsense_backend/app/agents/shared/middleware/tool_call_repair.py +++ b/surfsense_backend/app/agents/shared/middleware/tool_call_repair.py @@ -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 {} diff --git a/surfsense_backend/app/agents/shared/tools/__init__.py b/surfsense_backend/app/agents/shared/tools/__init__.py new file mode 100644 index 000000000..4b5ae3706 --- /dev/null +++ b/surfsense_backend/app/agents/shared/tools/__init__.py @@ -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", +] diff --git a/surfsense_backend/app/agents/new_chat/tools/confluence/__init__.py b/surfsense_backend/app/agents/shared/tools/confluence/__init__.py similarity index 100% rename from surfsense_backend/app/agents/new_chat/tools/confluence/__init__.py rename to surfsense_backend/app/agents/shared/tools/confluence/__init__.py diff --git a/surfsense_backend/app/agents/new_chat/tools/confluence/create_page.py b/surfsense_backend/app/agents/shared/tools/confluence/create_page.py similarity index 99% rename from surfsense_backend/app/agents/new_chat/tools/confluence/create_page.py rename to surfsense_backend/app/agents/shared/tools/confluence/create_page.py index c56db1528..95e2308e3 100644 --- a/surfsense_backend/app/agents/new_chat/tools/confluence/create_page.py +++ b/surfsense_backend/app/agents/shared/tools/confluence/create_page.py @@ -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 diff --git a/surfsense_backend/app/agents/new_chat/tools/confluence/delete_page.py b/surfsense_backend/app/agents/shared/tools/confluence/delete_page.py similarity index 99% rename from surfsense_backend/app/agents/new_chat/tools/confluence/delete_page.py rename to surfsense_backend/app/agents/shared/tools/confluence/delete_page.py index d4cd5032f..dd1ee326e 100644 --- a/surfsense_backend/app/agents/new_chat/tools/confluence/delete_page.py +++ b/surfsense_backend/app/agents/shared/tools/confluence/delete_page.py @@ -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 diff --git a/surfsense_backend/app/agents/new_chat/tools/confluence/update_page.py b/surfsense_backend/app/agents/shared/tools/confluence/update_page.py similarity index 99% rename from surfsense_backend/app/agents/new_chat/tools/confluence/update_page.py rename to surfsense_backend/app/agents/shared/tools/confluence/update_page.py index 51c205e00..1368f41b8 100644 --- a/surfsense_backend/app/agents/new_chat/tools/confluence/update_page.py +++ b/surfsense_backend/app/agents/shared/tools/confluence/update_page.py @@ -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 diff --git a/surfsense_backend/app/agents/new_chat/tools/connected_accounts.py b/surfsense_backend/app/agents/shared/tools/connected_accounts.py similarity index 100% rename from surfsense_backend/app/agents/new_chat/tools/connected_accounts.py rename to surfsense_backend/app/agents/shared/tools/connected_accounts.py diff --git a/surfsense_backend/app/agents/new_chat/tools/discord/__init__.py b/surfsense_backend/app/agents/shared/tools/discord/__init__.py similarity index 58% rename from surfsense_backend/app/agents/new_chat/tools/discord/__init__.py rename to surfsense_backend/app/agents/shared/tools/discord/__init__.py index b4eaec1f0..930f2bea1 100644 --- a/surfsense_backend/app/agents/new_chat/tools/discord/__init__.py +++ b/surfsense_backend/app/agents/shared/tools/discord/__init__.py @@ -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, ) diff --git a/surfsense_backend/app/agents/new_chat/tools/discord/_auth.py b/surfsense_backend/app/agents/shared/tools/discord/_auth.py similarity index 100% rename from surfsense_backend/app/agents/new_chat/tools/discord/_auth.py rename to surfsense_backend/app/agents/shared/tools/discord/_auth.py diff --git a/surfsense_backend/app/agents/new_chat/tools/discord/list_channels.py b/surfsense_backend/app/agents/shared/tools/discord/list_channels.py similarity index 100% rename from surfsense_backend/app/agents/new_chat/tools/discord/list_channels.py rename to surfsense_backend/app/agents/shared/tools/discord/list_channels.py diff --git a/surfsense_backend/app/agents/new_chat/tools/discord/read_messages.py b/surfsense_backend/app/agents/shared/tools/discord/read_messages.py similarity index 100% rename from surfsense_backend/app/agents/new_chat/tools/discord/read_messages.py rename to surfsense_backend/app/agents/shared/tools/discord/read_messages.py diff --git a/surfsense_backend/app/agents/new_chat/tools/discord/send_message.py b/surfsense_backend/app/agents/shared/tools/discord/send_message.py similarity index 98% rename from surfsense_backend/app/agents/new_chat/tools/discord/send_message.py rename to surfsense_backend/app/agents/shared/tools/discord/send_message.py index 5fe6fde35..3b4339e80 100644 --- a/surfsense_backend/app/agents/new_chat/tools/discord/send_message.py +++ b/surfsense_backend/app/agents/shared/tools/discord/send_message.py @@ -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 diff --git a/surfsense_backend/app/agents/new_chat/tools/dropbox/__init__.py b/surfsense_backend/app/agents/shared/tools/dropbox/__init__.py similarity index 58% rename from surfsense_backend/app/agents/new_chat/tools/dropbox/__init__.py rename to surfsense_backend/app/agents/shared/tools/dropbox/__init__.py index 836b9ee41..2db97cc60 100644 --- a/surfsense_backend/app/agents/new_chat/tools/dropbox/__init__.py +++ b/surfsense_backend/app/agents/shared/tools/dropbox/__init__.py @@ -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, ) diff --git a/surfsense_backend/app/agents/new_chat/tools/dropbox/create_file.py b/surfsense_backend/app/agents/shared/tools/dropbox/create_file.py similarity index 99% rename from surfsense_backend/app/agents/new_chat/tools/dropbox/create_file.py rename to surfsense_backend/app/agents/shared/tools/dropbox/create_file.py index 7aae034cc..e5af16b34 100644 --- a/surfsense_backend/app/agents/new_chat/tools/dropbox/create_file.py +++ b/surfsense_backend/app/agents/shared/tools/dropbox/create_file.py @@ -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 diff --git a/surfsense_backend/app/agents/new_chat/tools/dropbox/trash_file.py b/surfsense_backend/app/agents/shared/tools/dropbox/trash_file.py similarity index 99% rename from surfsense_backend/app/agents/new_chat/tools/dropbox/trash_file.py rename to surfsense_backend/app/agents/shared/tools/dropbox/trash_file.py index 0e59e49db..e878c5294 100644 --- a/surfsense_backend/app/agents/new_chat/tools/dropbox/trash_file.py +++ b/surfsense_backend/app/agents/shared/tools/dropbox/trash_file.py @@ -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, diff --git a/surfsense_backend/app/agents/new_chat/tools/generate_image.py b/surfsense_backend/app/agents/shared/tools/generate_image.py similarity index 100% rename from surfsense_backend/app/agents/new_chat/tools/generate_image.py rename to surfsense_backend/app/agents/shared/tools/generate_image.py diff --git a/surfsense_backend/app/agents/new_chat/tools/gmail/__init__.py b/surfsense_backend/app/agents/shared/tools/gmail/__init__.py similarity index 56% rename from surfsense_backend/app/agents/new_chat/tools/gmail/__init__.py rename to surfsense_backend/app/agents/shared/tools/gmail/__init__.py index 294840122..f32312fe6 100644 --- a/surfsense_backend/app/agents/new_chat/tools/gmail/__init__.py +++ b/surfsense_backend/app/agents/shared/tools/gmail/__init__.py @@ -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, ) diff --git a/surfsense_backend/app/agents/new_chat/tools/gmail/composio_helpers.py b/surfsense_backend/app/agents/shared/tools/gmail/composio_helpers.py similarity index 100% rename from surfsense_backend/app/agents/new_chat/tools/gmail/composio_helpers.py rename to surfsense_backend/app/agents/shared/tools/gmail/composio_helpers.py diff --git a/surfsense_backend/app/agents/new_chat/tools/gmail/create_draft.py b/surfsense_backend/app/agents/shared/tools/gmail/create_draft.py similarity index 99% rename from surfsense_backend/app/agents/new_chat/tools/gmail/create_draft.py rename to surfsense_backend/app/agents/shared/tools/gmail/create_draft.py index c88b48d2d..e44fa33a2 100644 --- a/surfsense_backend/app/agents/new_chat/tools/gmail/create_draft.py +++ b/surfsense_backend/app/agents/shared/tools/gmail/create_draft.py @@ -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, ) diff --git a/surfsense_backend/app/agents/new_chat/tools/gmail/read_email.py b/surfsense_backend/app/agents/shared/tools/gmail/read_email.py similarity index 97% rename from surfsense_backend/app/agents/new_chat/tools/gmail/read_email.py rename to surfsense_backend/app/agents/shared/tools/gmail/read_email.py index 464713591..684379a09 100644 --- a/surfsense_backend/app/agents/new_chat/tools/gmail/read_email.py +++ b/surfsense_backend/app/agents/shared/tools/gmail/read_email.py @@ -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, ) diff --git a/surfsense_backend/app/agents/new_chat/tools/gmail/search_emails.py b/surfsense_backend/app/agents/shared/tools/gmail/search_emails.py similarity index 100% rename from surfsense_backend/app/agents/new_chat/tools/gmail/search_emails.py rename to surfsense_backend/app/agents/shared/tools/gmail/search_emails.py diff --git a/surfsense_backend/app/agents/new_chat/tools/gmail/send_email.py b/surfsense_backend/app/agents/shared/tools/gmail/send_email.py similarity index 99% rename from surfsense_backend/app/agents/new_chat/tools/gmail/send_email.py rename to surfsense_backend/app/agents/shared/tools/gmail/send_email.py index 4d5aa3bcc..0f10e8082 100644 --- a/surfsense_backend/app/agents/new_chat/tools/gmail/send_email.py +++ b/surfsense_backend/app/agents/shared/tools/gmail/send_email.py @@ -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, ) diff --git a/surfsense_backend/app/agents/new_chat/tools/gmail/trash_email.py b/surfsense_backend/app/agents/shared/tools/gmail/trash_email.py similarity index 99% rename from surfsense_backend/app/agents/new_chat/tools/gmail/trash_email.py rename to surfsense_backend/app/agents/shared/tools/gmail/trash_email.py index 95f5b4e6c..fa6e015d1 100644 --- a/surfsense_backend/app/agents/new_chat/tools/gmail/trash_email.py +++ b/surfsense_backend/app/agents/shared/tools/gmail/trash_email.py @@ -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, ) diff --git a/surfsense_backend/app/agents/new_chat/tools/gmail/update_draft.py b/surfsense_backend/app/agents/shared/tools/gmail/update_draft.py similarity index 99% rename from surfsense_backend/app/agents/new_chat/tools/gmail/update_draft.py rename to surfsense_backend/app/agents/shared/tools/gmail/update_draft.py index 129b7defb..965b42675 100644 --- a/surfsense_backend/app/agents/new_chat/tools/gmail/update_draft.py +++ b/surfsense_backend/app/agents/shared/tools/gmail/update_draft.py @@ -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, ) diff --git a/surfsense_backend/app/agents/new_chat/tools/google_calendar/__init__.py b/surfsense_backend/app/agents/shared/tools/google_calendar/__init__.py similarity index 55% rename from surfsense_backend/app/agents/new_chat/tools/google_calendar/__init__.py rename to surfsense_backend/app/agents/shared/tools/google_calendar/__init__.py index 13d4c06cb..362cf4127 100644 --- a/surfsense_backend/app/agents/new_chat/tools/google_calendar/__init__.py +++ b/surfsense_backend/app/agents/shared/tools/google_calendar/__init__.py @@ -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, ) diff --git a/surfsense_backend/app/agents/new_chat/tools/google_calendar/create_event.py b/surfsense_backend/app/agents/shared/tools/google_calendar/create_event.py similarity index 99% rename from surfsense_backend/app/agents/new_chat/tools/google_calendar/create_event.py rename to surfsense_backend/app/agents/shared/tools/google_calendar/create_event.py index dec92cc8b..7e5367049 100644 --- a/surfsense_backend/app/agents/new_chat/tools/google_calendar/create_event.py +++ b/surfsense_backend/app/agents/shared/tools/google_calendar/create_event.py @@ -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 diff --git a/surfsense_backend/app/agents/new_chat/tools/google_calendar/delete_event.py b/surfsense_backend/app/agents/shared/tools/google_calendar/delete_event.py similarity index 99% rename from surfsense_backend/app/agents/new_chat/tools/google_calendar/delete_event.py rename to surfsense_backend/app/agents/shared/tools/google_calendar/delete_event.py index e7e891b08..21a67a947 100644 --- a/surfsense_backend/app/agents/new_chat/tools/google_calendar/delete_event.py +++ b/surfsense_backend/app/agents/shared/tools/google_calendar/delete_event.py @@ -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 diff --git a/surfsense_backend/app/agents/new_chat/tools/google_calendar/search_events.py b/surfsense_backend/app/agents/shared/tools/google_calendar/search_events.py similarity index 98% rename from surfsense_backend/app/agents/new_chat/tools/google_calendar/search_events.py rename to surfsense_backend/app/agents/shared/tools/google_calendar/search_events.py index e5f18f675..6a79b63fb 100644 --- a/surfsense_backend/app/agents/new_chat/tools/google_calendar/search_events.py +++ b/surfsense_backend/app/agents/shared/tools/google_calendar/search_events.py @@ -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__) diff --git a/surfsense_backend/app/agents/new_chat/tools/google_calendar/update_event.py b/surfsense_backend/app/agents/shared/tools/google_calendar/update_event.py similarity index 99% rename from surfsense_backend/app/agents/new_chat/tools/google_calendar/update_event.py rename to surfsense_backend/app/agents/shared/tools/google_calendar/update_event.py index b8561fee6..586695056 100644 --- a/surfsense_backend/app/agents/new_chat/tools/google_calendar/update_event.py +++ b/surfsense_backend/app/agents/shared/tools/google_calendar/update_event.py @@ -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 diff --git a/surfsense_backend/app/agents/new_chat/tools/google_drive/__init__.py b/surfsense_backend/app/agents/shared/tools/google_drive/__init__.py similarity index 59% rename from surfsense_backend/app/agents/new_chat/tools/google_drive/__init__.py rename to surfsense_backend/app/agents/shared/tools/google_drive/__init__.py index 9c63bceb1..1f5feca60 100644 --- a/surfsense_backend/app/agents/new_chat/tools/google_drive/__init__.py +++ b/surfsense_backend/app/agents/shared/tools/google_drive/__init__.py @@ -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, ) diff --git a/surfsense_backend/app/agents/new_chat/tools/google_drive/create_file.py b/surfsense_backend/app/agents/shared/tools/google_drive/create_file.py similarity index 99% rename from surfsense_backend/app/agents/new_chat/tools/google_drive/create_file.py rename to surfsense_backend/app/agents/shared/tools/google_drive/create_file.py index 66199ca67..dc64d8c92 100644 --- a/surfsense_backend/app/agents/new_chat/tools/google_drive/create_file.py +++ b/surfsense_backend/app/agents/shared/tools/google_drive/create_file.py @@ -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 diff --git a/surfsense_backend/app/agents/new_chat/tools/google_drive/trash_file.py b/surfsense_backend/app/agents/shared/tools/google_drive/trash_file.py similarity index 99% rename from surfsense_backend/app/agents/new_chat/tools/google_drive/trash_file.py rename to surfsense_backend/app/agents/shared/tools/google_drive/trash_file.py index b3c9240d8..69e8ba6d0 100644 --- a/surfsense_backend/app/agents/new_chat/tools/google_drive/trash_file.py +++ b/surfsense_backend/app/agents/shared/tools/google_drive/trash_file.py @@ -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 diff --git a/surfsense_backend/app/agents/new_chat/tools/hitl.py b/surfsense_backend/app/agents/shared/tools/hitl.py similarity index 99% rename from surfsense_backend/app/agents/new_chat/tools/hitl.py rename to surfsense_backend/app/agents/shared/tools/hitl.py index 5b64929de..287a19014 100644 --- a/surfsense_backend/app/agents/new_chat/tools/hitl.py +++ b/surfsense_backend/app/agents/shared/tools/hitl.py @@ -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", diff --git a/surfsense_backend/app/agents/shared/tools/invalid_tool.py b/surfsense_backend/app/agents/shared/tools/invalid_tool.py new file mode 100644 index 000000000..ea4bc0bc1 --- /dev/null +++ b/surfsense_backend/app/agents/shared/tools/invalid_tool.py @@ -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 "" + 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", +] diff --git a/surfsense_backend/app/agents/new_chat/tools/knowledge_base.py b/surfsense_backend/app/agents/shared/tools/knowledge_base.py similarity index 100% rename from surfsense_backend/app/agents/new_chat/tools/knowledge_base.py rename to surfsense_backend/app/agents/shared/tools/knowledge_base.py diff --git a/surfsense_backend/app/agents/new_chat/tools/luma/__init__.py b/surfsense_backend/app/agents/shared/tools/luma/__init__.py similarity index 57% rename from surfsense_backend/app/agents/new_chat/tools/luma/__init__.py rename to surfsense_backend/app/agents/shared/tools/luma/__init__.py index 255119bee..83af8c8c5 100644 --- a/surfsense_backend/app/agents/new_chat/tools/luma/__init__.py +++ b/surfsense_backend/app/agents/shared/tools/luma/__init__.py @@ -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, ) diff --git a/surfsense_backend/app/agents/new_chat/tools/luma/_auth.py b/surfsense_backend/app/agents/shared/tools/luma/_auth.py similarity index 100% rename from surfsense_backend/app/agents/new_chat/tools/luma/_auth.py rename to surfsense_backend/app/agents/shared/tools/luma/_auth.py diff --git a/surfsense_backend/app/agents/new_chat/tools/luma/create_event.py b/surfsense_backend/app/agents/shared/tools/luma/create_event.py similarity index 98% rename from surfsense_backend/app/agents/new_chat/tools/luma/create_event.py rename to surfsense_backend/app/agents/shared/tools/luma/create_event.py index 65c177d7a..d4c47535e 100644 --- a/surfsense_backend/app/agents/new_chat/tools/luma/create_event.py +++ b/surfsense_backend/app/agents/shared/tools/luma/create_event.py @@ -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 diff --git a/surfsense_backend/app/agents/new_chat/tools/luma/list_events.py b/surfsense_backend/app/agents/shared/tools/luma/list_events.py similarity index 100% rename from surfsense_backend/app/agents/new_chat/tools/luma/list_events.py rename to surfsense_backend/app/agents/shared/tools/luma/list_events.py diff --git a/surfsense_backend/app/agents/new_chat/tools/luma/read_event.py b/surfsense_backend/app/agents/shared/tools/luma/read_event.py similarity index 100% rename from surfsense_backend/app/agents/new_chat/tools/luma/read_event.py rename to surfsense_backend/app/agents/shared/tools/luma/read_event.py diff --git a/surfsense_backend/app/agents/new_chat/tools/mcp_client.py b/surfsense_backend/app/agents/shared/tools/mcp_client.py similarity index 100% rename from surfsense_backend/app/agents/new_chat/tools/mcp_client.py rename to surfsense_backend/app/agents/shared/tools/mcp_client.py diff --git a/surfsense_backend/app/agents/new_chat/tools/mcp_tool.py b/surfsense_backend/app/agents/shared/tools/mcp_tool.py similarity index 99% rename from surfsense_backend/app/agents/new_chat/tools/mcp_tool.py rename to surfsense_backend/app/agents/shared/tools/mcp_tool.py index 8bef19050..8e688a71b 100644 --- a/surfsense_backend/app/agents/new_chat/tools/mcp_tool.py +++ b/surfsense_backend/app/agents/shared/tools/mcp_tool.py @@ -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, diff --git a/surfsense_backend/app/agents/new_chat/tools/mcp_tools_cache.py b/surfsense_backend/app/agents/shared/tools/mcp_tools_cache.py similarity index 96% rename from surfsense_backend/app/agents/new_chat/tools/mcp_tools_cache.py rename to surfsense_backend/app/agents/shared/tools/mcp_tools_cache.py index 81027e1c4..bd89856ae 100644 --- a/surfsense_backend/app/agents/new_chat/tools/mcp_tools_cache.py +++ b/surfsense_backend/app/agents/shared/tools/mcp_tools_cache.py @@ -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) diff --git a/surfsense_backend/app/agents/new_chat/tools/notion/__init__.py b/surfsense_backend/app/agents/shared/tools/notion/__init__.py similarity index 100% rename from surfsense_backend/app/agents/new_chat/tools/notion/__init__.py rename to surfsense_backend/app/agents/shared/tools/notion/__init__.py diff --git a/surfsense_backend/app/agents/new_chat/tools/notion/create_page.py b/surfsense_backend/app/agents/shared/tools/notion/create_page.py similarity index 99% rename from surfsense_backend/app/agents/new_chat/tools/notion/create_page.py rename to surfsense_backend/app/agents/shared/tools/notion/create_page.py index 6ec95e9f0..b9e4d46d3 100644 --- a/surfsense_backend/app/agents/new_chat/tools/notion/create_page.py +++ b/surfsense_backend/app/agents/shared/tools/notion/create_page.py @@ -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 diff --git a/surfsense_backend/app/agents/new_chat/tools/notion/delete_page.py b/surfsense_backend/app/agents/shared/tools/notion/delete_page.py similarity index 99% rename from surfsense_backend/app/agents/new_chat/tools/notion/delete_page.py rename to surfsense_backend/app/agents/shared/tools/notion/delete_page.py index 7b85da4c2..3fa4af9dc 100644 --- a/surfsense_backend/app/agents/new_chat/tools/notion/delete_page.py +++ b/surfsense_backend/app/agents/shared/tools/notion/delete_page.py @@ -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 diff --git a/surfsense_backend/app/agents/new_chat/tools/notion/update_page.py b/surfsense_backend/app/agents/shared/tools/notion/update_page.py similarity index 99% rename from surfsense_backend/app/agents/new_chat/tools/notion/update_page.py rename to surfsense_backend/app/agents/shared/tools/notion/update_page.py index df757476a..ed4991052 100644 --- a/surfsense_backend/app/agents/new_chat/tools/notion/update_page.py +++ b/surfsense_backend/app/agents/shared/tools/notion/update_page.py @@ -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 diff --git a/surfsense_backend/app/agents/new_chat/tools/onedrive/__init__.py b/surfsense_backend/app/agents/shared/tools/onedrive/__init__.py similarity index 59% rename from surfsense_backend/app/agents/new_chat/tools/onedrive/__init__.py rename to surfsense_backend/app/agents/shared/tools/onedrive/__init__.py index 8edb4857e..04e6fc341 100644 --- a/surfsense_backend/app/agents/new_chat/tools/onedrive/__init__.py +++ b/surfsense_backend/app/agents/shared/tools/onedrive/__init__.py @@ -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, ) diff --git a/surfsense_backend/app/agents/new_chat/tools/onedrive/create_file.py b/surfsense_backend/app/agents/shared/tools/onedrive/create_file.py similarity index 99% rename from surfsense_backend/app/agents/new_chat/tools/onedrive/create_file.py rename to surfsense_backend/app/agents/shared/tools/onedrive/create_file.py index 5f199a41b..97efb896d 100644 --- a/surfsense_backend/app/agents/new_chat/tools/onedrive/create_file.py +++ b/surfsense_backend/app/agents/shared/tools/onedrive/create_file.py @@ -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 diff --git a/surfsense_backend/app/agents/new_chat/tools/onedrive/trash_file.py b/surfsense_backend/app/agents/shared/tools/onedrive/trash_file.py similarity index 99% rename from surfsense_backend/app/agents/new_chat/tools/onedrive/trash_file.py rename to surfsense_backend/app/agents/shared/tools/onedrive/trash_file.py index 4857ea988..ef8c74662 100644 --- a/surfsense_backend/app/agents/new_chat/tools/onedrive/trash_file.py +++ b/surfsense_backend/app/agents/shared/tools/onedrive/trash_file.py @@ -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, diff --git a/surfsense_backend/app/agents/new_chat/tools/podcast.py b/surfsense_backend/app/agents/shared/tools/podcast.py similarity index 100% rename from surfsense_backend/app/agents/new_chat/tools/podcast.py rename to surfsense_backend/app/agents/shared/tools/podcast.py diff --git a/surfsense_backend/app/agents/shared/tools/registry.py b/surfsense_backend/app/agents/shared/tools/registry.py new file mode 100644 index 000000000..9b1944aa5 --- /dev/null +++ b/surfsense_backend/app/agents/shared/tools/registry.py @@ -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 diff --git a/surfsense_backend/app/agents/new_chat/tools/report.py b/surfsense_backend/app/agents/shared/tools/report.py similarity index 100% rename from surfsense_backend/app/agents/new_chat/tools/report.py rename to surfsense_backend/app/agents/shared/tools/report.py diff --git a/surfsense_backend/app/agents/new_chat/tools/resume.py b/surfsense_backend/app/agents/shared/tools/resume.py similarity index 100% rename from surfsense_backend/app/agents/new_chat/tools/resume.py rename to surfsense_backend/app/agents/shared/tools/resume.py diff --git a/surfsense_backend/app/agents/new_chat/tools/scrape_webpage.py b/surfsense_backend/app/agents/shared/tools/scrape_webpage.py similarity index 100% rename from surfsense_backend/app/agents/new_chat/tools/scrape_webpage.py rename to surfsense_backend/app/agents/shared/tools/scrape_webpage.py diff --git a/surfsense_backend/app/agents/new_chat/tools/teams/__init__.py b/surfsense_backend/app/agents/shared/tools/teams/__init__.py similarity index 57% rename from surfsense_backend/app/agents/new_chat/tools/teams/__init__.py rename to surfsense_backend/app/agents/shared/tools/teams/__init__.py index 60e2add49..d9129fa82 100644 --- a/surfsense_backend/app/agents/new_chat/tools/teams/__init__.py +++ b/surfsense_backend/app/agents/shared/tools/teams/__init__.py @@ -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, ) diff --git a/surfsense_backend/app/agents/new_chat/tools/teams/_auth.py b/surfsense_backend/app/agents/shared/tools/teams/_auth.py similarity index 100% rename from surfsense_backend/app/agents/new_chat/tools/teams/_auth.py rename to surfsense_backend/app/agents/shared/tools/teams/_auth.py diff --git a/surfsense_backend/app/agents/new_chat/tools/teams/list_channels.py b/surfsense_backend/app/agents/shared/tools/teams/list_channels.py similarity index 100% rename from surfsense_backend/app/agents/new_chat/tools/teams/list_channels.py rename to surfsense_backend/app/agents/shared/tools/teams/list_channels.py diff --git a/surfsense_backend/app/agents/new_chat/tools/teams/read_messages.py b/surfsense_backend/app/agents/shared/tools/teams/read_messages.py similarity index 100% rename from surfsense_backend/app/agents/new_chat/tools/teams/read_messages.py rename to surfsense_backend/app/agents/shared/tools/teams/read_messages.py diff --git a/surfsense_backend/app/agents/new_chat/tools/teams/send_message.py b/surfsense_backend/app/agents/shared/tools/teams/send_message.py similarity index 98% rename from surfsense_backend/app/agents/new_chat/tools/teams/send_message.py rename to surfsense_backend/app/agents/shared/tools/teams/send_message.py index 6f40d27e1..600481872 100644 --- a/surfsense_backend/app/agents/new_chat/tools/teams/send_message.py +++ b/surfsense_backend/app/agents/shared/tools/teams/send_message.py @@ -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 diff --git a/surfsense_backend/app/agents/new_chat/tools/update_memory.py b/surfsense_backend/app/agents/shared/tools/update_memory.py similarity index 100% rename from surfsense_backend/app/agents/new_chat/tools/update_memory.py rename to surfsense_backend/app/agents/shared/tools/update_memory.py diff --git a/surfsense_backend/app/agents/new_chat/tools/video_presentation.py b/surfsense_backend/app/agents/shared/tools/video_presentation.py similarity index 100% rename from surfsense_backend/app/agents/new_chat/tools/video_presentation.py rename to surfsense_backend/app/agents/shared/tools/video_presentation.py diff --git a/surfsense_backend/app/agents/new_chat/tools/web_search.py b/surfsense_backend/app/agents/shared/tools/web_search.py similarity index 100% rename from surfsense_backend/app/agents/new_chat/tools/web_search.py rename to surfsense_backend/app/agents/shared/tools/web_search.py diff --git a/surfsense_backend/app/routes/mcp_oauth_route.py b/surfsense_backend/app/routes/mcp_oauth_route.py index 57248d631..89049c1ca 100644 --- a/surfsense_backend/app/routes/mcp_oauth_route.py +++ b/surfsense_backend/app/routes/mcp_oauth_route.py @@ -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, ) diff --git a/surfsense_backend/app/routes/new_chat_routes.py b/surfsense_backend/app/routes/new_chat_routes.py index 967b88e7a..814c44af1 100644 --- a/surfsense_backend/app/routes/new_chat_routes.py +++ b/surfsense_backend/app/routes/new_chat_routes.py @@ -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( diff --git a/surfsense_backend/app/routes/search_source_connectors_routes.py b/surfsense_backend/app/routes/search_source_connectors_routes.py index 362b4d232..32ecac6fa 100644 --- a/surfsense_backend/app/routes/search_source_connectors_routes.py +++ b/surfsense_backend/app/routes/search_source_connectors_routes.py @@ -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: diff --git a/surfsense_backend/app/services/provider_capabilities.py b/surfsense_backend/app/services/provider_capabilities.py index 74fae0e19..e68fd53f3 100644 --- a/surfsense_backend/app/services/provider_capabilities.py +++ b/surfsense_backend/app/services/provider_capabilities.py @@ -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", diff --git a/surfsense_backend/tests/e2e/fakes/mcp_runtime.py b/surfsense_backend/tests/e2e/fakes/mcp_runtime.py index e772bb63a..ffd070816 100644 --- a/surfsense_backend/tests/e2e/fakes/mcp_runtime.py +++ b/surfsense_backend/tests/e2e/fakes/mcp_runtime.py @@ -137,10 +137,10 @@ def install(active_patches: list[Any]) -> None: """Patch production MCP streamable-HTTP boundaries exactly once.""" targets = [ ( - "app.agents.new_chat.tools.mcp_tool.streamablehttp_client", + "app.agents.shared.tools.mcp_tool.streamablehttp_client", _fake_streamablehttp_client, ), - ("app.agents.new_chat.tools.mcp_tool.ClientSession", _FakeClientSession), + ("app.agents.shared.tools.mcp_tool.ClientSession", _FakeClientSession), ] for target, replacement in targets: p = patch(target, replacement) diff --git a/surfsense_backend/tests/e2e/fakes/native_google.py b/surfsense_backend/tests/e2e/fakes/native_google.py index 73c8cc738..84c98d69a 100644 --- a/surfsense_backend/tests/e2e/fakes/native_google.py +++ b/surfsense_backend/tests/e2e/fakes/native_google.py @@ -429,9 +429,9 @@ def install(active_patches: list[Any]) -> None: ("app.connectors.google_drive.client.build", _fake_build), ("app.connectors.google_gmail_connector.build", _fake_build), ("app.connectors.google_calendar_connector.build", _fake_build), - ("app.agents.new_chat.tools.google_calendar.create_event.build", _fake_build), - ("app.agents.new_chat.tools.google_calendar.update_event.build", _fake_build), - ("app.agents.new_chat.tools.google_calendar.delete_event.build", _fake_build), + ("app.agents.shared.tools.google_calendar.create_event.build", _fake_build), + ("app.agents.shared.tools.google_calendar.update_event.build", _fake_build), + ("app.agents.shared.tools.google_calendar.delete_event.build", _fake_build), ("googleapiclient.http.MediaIoBaseDownload", _FakeMediaIoBaseDownload), ( "app.connectors.google_drive.client._build_thread_http", diff --git a/surfsense_backend/tests/integration/google_unification/conftest.py b/surfsense_backend/tests/integration/google_unification/conftest.py index de68c7acb..d189afad2 100644 --- a/surfsense_backend/tests/integration/google_unification/conftest.py +++ b/surfsense_backend/tests/integration/google_unification/conftest.py @@ -239,7 +239,7 @@ def patched_shielded_session(async_engine, monkeypatch): yield session monkeypatch.setattr( - "app.agents.new_chat.tools.knowledge_base.shielded_async_session", + "app.agents.shared.tools.knowledge_base.shielded_async_session", _test_shielded, ) diff --git a/surfsense_backend/tests/integration/google_unification/test_browse_includes_legacy_docs.py b/surfsense_backend/tests/integration/google_unification/test_browse_includes_legacy_docs.py index fc2fec5a8..96bf371d6 100644 --- a/surfsense_backend/tests/integration/google_unification/test_browse_includes_legacy_docs.py +++ b/surfsense_backend/tests/integration/google_unification/test_browse_includes_legacy_docs.py @@ -17,7 +17,7 @@ async def test_browse_recent_documents_with_list_type_returns_both( committed_google_data, patched_shielded_session ): """_browse_recent_documents returns docs of all types when given a list.""" - from app.agents.new_chat.tools.knowledge_base import _browse_recent_documents + from app.agents.shared.tools.knowledge_base import _browse_recent_documents space_id = committed_google_data["search_space_id"] diff --git a/surfsense_backend/tests/unit/agents/new_chat/test_action_log.py b/surfsense_backend/tests/unit/agents/new_chat/test_action_log.py index 5e3955bf1..387d67e61 100644 --- a/surfsense_backend/tests/unit/agents/new_chat/test_action_log.py +++ b/surfsense_backend/tests/unit/agents/new_chat/test_action_log.py @@ -12,7 +12,7 @@ from langchain_core.tools import tool from app.agents.shared.feature_flags import AgentFeatureFlags from app.agents.shared.middleware.action_log import ActionLogMiddleware -from app.agents.new_chat.tools.registry import ToolDefinition +from app.agents.shared.tools.registry import ToolDefinition @dataclass diff --git a/surfsense_backend/tests/unit/agents/new_chat/test_dedup_tool_calls.py b/surfsense_backend/tests/unit/agents/new_chat/test_dedup_tool_calls.py index b0a3b2e00..65c2c578a 100644 --- a/surfsense_backend/tests/unit/agents/new_chat/test_dedup_tool_calls.py +++ b/surfsense_backend/tests/unit/agents/new_chat/test_dedup_tool_calls.py @@ -93,7 +93,7 @@ def test_no_agent_tools_means_no_dedup() -> None: Coverage for the previously hardcoded native HITL tools now lives on each :class:`ToolDefinition.dedup_key` in - :mod:`app.agents.new_chat.tools.registry`, which is wired through to + :mod:`app.agents.shared.tools.registry`, which is wired through to ``tool.metadata`` by :func:`build_tools`. """ mw = DedupHITLToolCallsMiddleware(agent_tools=None) @@ -116,7 +116,7 @@ def test_registry_propagates_dedup_key_to_tool_metadata() -> None: the constructed tool's ``metadata`` so :class:`DedupHITLToolCallsMiddleware` can pick it up at agent build time. """ - from app.agents.new_chat.tools.registry import ( + from app.agents.shared.tools.registry import ( BUILTIN_TOOLS, wrap_dedup_key_by_arg_name, ) diff --git a/surfsense_backend/tests/unit/agents/new_chat/test_default_permissions_layering.py b/surfsense_backend/tests/unit/agents/new_chat/test_default_permissions_layering.py index 796df8128..e2ae513da 100644 --- a/surfsense_backend/tests/unit/agents/new_chat/test_default_permissions_layering.py +++ b/surfsense_backend/tests/unit/agents/new_chat/test_default_permissions_layering.py @@ -17,7 +17,7 @@ caused two production-painful behaviors: read-only tool calls, raising ``RejectedError("ls")``. * Mutating connector tools got *double* prompted — once via the middleware ``ask`` and again via the per-tool ``interrupt()`` in - ``app.agents.new_chat.tools.hitl``. + ``app.agents.shared.tools.hitl``. These tests pin the layering so a refactor that drops the default ruleset fails loud. diff --git a/surfsense_backend/tests/unit/agents/new_chat/test_hitl_auto_approve.py b/surfsense_backend/tests/unit/agents/new_chat/test_hitl_auto_approve.py index d0ea73376..6552d6bc6 100644 --- a/surfsense_backend/tests/unit/agents/new_chat/test_hitl_auto_approve.py +++ b/surfsense_backend/tests/unit/agents/new_chat/test_hitl_auto_approve.py @@ -10,7 +10,7 @@ from __future__ import annotations import pytest -from app.agents.new_chat.tools.hitl import ( +from app.agents.shared.tools.hitl import ( DEFAULT_AUTO_APPROVED_TOOLS, HITLResult, request_approval, diff --git a/surfsense_backend/tests/unit/agents/new_chat/test_tool_call_repair.py b/surfsense_backend/tests/unit/agents/new_chat/test_tool_call_repair.py index 0cd338ce3..068d8415b 100644 --- a/surfsense_backend/tests/unit/agents/new_chat/test_tool_call_repair.py +++ b/surfsense_backend/tests/unit/agents/new_chat/test_tool_call_repair.py @@ -8,7 +8,7 @@ from langchain_core.messages import AIMessage from app.agents.shared.middleware.tool_call_repair import ( ToolCallNameRepairMiddleware, ) -from app.agents.new_chat.tools.invalid_tool import INVALID_TOOL_NAME +from app.agents.shared.tools.invalid_tool import INVALID_TOOL_NAME pytestmark = pytest.mark.unit diff --git a/surfsense_backend/tests/unit/agents/new_chat/tools/test_mcp_tools_cache.py b/surfsense_backend/tests/unit/agents/new_chat/tools/test_mcp_tools_cache.py index bae97ba9f..90337dd7b 100644 --- a/surfsense_backend/tests/unit/agents/new_chat/tools/test_mcp_tools_cache.py +++ b/surfsense_backend/tests/unit/agents/new_chat/tools/test_mcp_tools_cache.py @@ -7,7 +7,7 @@ from types import SimpleNamespace import pytest -from app.agents.new_chat.tools.mcp_tools_cache import ( +from app.agents.shared.tools.mcp_tools_cache import ( CachedMCPToolDef, CachedMCPTools, read_cached_tools, diff --git a/surfsense_backend/tests/unit/agents/new_chat/tools/test_resume_page_limits.py b/surfsense_backend/tests/unit/agents/new_chat/tools/test_resume_page_limits.py index 4f93ad732..8bfcb8947 100644 --- a/surfsense_backend/tests/unit/agents/new_chat/tools/test_resume_page_limits.py +++ b/surfsense_backend/tests/unit/agents/new_chat/tools/test_resume_page_limits.py @@ -7,7 +7,7 @@ from unittest.mock import AsyncMock import pypdf import pytest -from app.agents.new_chat.tools import resume as resume_tool +from app.agents.shared.tools import resume as resume_tool pytestmark = pytest.mark.unit diff --git a/surfsense_backend/tests/unit/services/test_image_gen_api_base_defense.py b/surfsense_backend/tests/unit/services/test_image_gen_api_base_defense.py index 9d5fdb190..575d245c2 100644 --- a/surfsense_backend/tests/unit/services/test_image_gen_api_base_defense.py +++ b/surfsense_backend/tests/unit/services/test_image_gen_api_base_defense.py @@ -90,7 +90,7 @@ async def test_global_openrouter_image_gen_sets_api_base_when_config_empty(): async def test_generate_image_tool_global_sets_api_base_when_config_empty(): """Same defense at the agent tool entry point — both surfaces share the same OpenRouter config payloads.""" - from app.agents.new_chat.tools import generate_image as gi_module + from app.agents.shared.tools import generate_image as gi_module cfg = { "id": -20_001,