diff --git a/surfsense_backend/app/agents/multi_agent_chat/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/__init__.py deleted file mode 100644 index bdd54b4e0..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/__init__.py +++ /dev/null @@ -1,132 +0,0 @@ -""" -Multi-agent chat (LangChain Subagents pattern). - -**Layout (SRP)** - -- :mod:`expert_agent.builtins` — general categories from the tool registry (research, memory, deliverables — not tied to one vendor). -- :mod:`expert_agent.connectors` — external integrations (one subgraph per product where split). -- :mod:`core` — prompts, compiled subgraph helper, delegation, registry subsets, tool-factory kwargs (:mod:`core.bindings`). -- :mod:`routing` — supervisor-facing ``@tool`` routers → domain invoke. -- :mod:`supervisor` — orchestrator graph + ``supervisor_prompt.md``. -- :mod:`integration` — async ``create_multi_agent_chat`` composer (partitions MCP tools into experts). - -Documentation: -https://docs.langchain.com/oss/python/langchain/multi-agent -https://docs.langchain.com/oss/python/langchain/multi-agent/subagents - -Display name: ``multi-agent-chat`` — Python package: ``multi_agent_chat``. -""" - -from app.agents.multi_agent_chat.expert_agent.builtins.deliverables import ( - build_deliverables_tools, - build_deliverables_domain_agent, -) -from app.agents.multi_agent_chat.expert_agent.builtins.memory import ( - build_memory_tools, - build_memory_domain_agent, -) -from app.agents.multi_agent_chat.expert_agent.builtins.research import ( - build_research_tools, - build_research_domain_agent, -) -from app.agents.multi_agent_chat.expert_agent.connectors.calendar import ( - build_calendar_domain_agent, - build_calendar_tools, -) -from app.agents.multi_agent_chat.expert_agent.connectors.confluence import ( - build_confluence_tools, - build_confluence_domain_agent, -) -from app.agents.multi_agent_chat.expert_agent.connectors.discord import ( - build_discord_tools, - build_discord_domain_agent, -) -from app.agents.multi_agent_chat.expert_agent.connectors.dropbox import ( - build_dropbox_tools, - build_dropbox_domain_agent, -) -from app.agents.multi_agent_chat.expert_agent.connectors.gmail import ( - build_gmail_tools, - build_gmail_domain_agent, -) -from app.agents.multi_agent_chat.expert_agent.connectors.google_drive import ( - build_google_drive_tools, - build_google_drive_domain_agent, -) -from app.agents.multi_agent_chat.expert_agent.connectors.luma import ( - build_luma_tools, - build_luma_domain_agent, -) -from app.agents.multi_agent_chat.expert_agent.connectors.notion import ( - build_notion_tools, - build_notion_domain_agent, -) -from app.agents.multi_agent_chat.expert_agent.connectors.onedrive import ( - build_onedrive_tools, - build_onedrive_domain_agent, -) -from app.agents.multi_agent_chat.expert_agent.connectors.teams import ( - build_teams_tools, - build_teams_domain_agent, -) -from app.agents.multi_agent_chat.core import ( - REGISTRY_ROUTING_CATEGORY_KEYS, - TOOL_NAMES_BY_CATEGORY, - build_domain_agent, - build_registry_dependencies, - build_registry_tools_for_category, - compose_child_task, - connector_binding, - extract_last_assistant_text, - read_prompt_md, -) -from app.agents.multi_agent_chat.integration import create_multi_agent_chat -from app.agents.multi_agent_chat.routing import ( - DomainRoutingSpec, - build_supervisor_routing_tools, - routing_tools_from_specs, -) -from app.agents.multi_agent_chat.supervisor import build_supervisor_agent - -__all__ = [ - "REGISTRY_ROUTING_CATEGORY_KEYS", - "TOOL_NAMES_BY_CATEGORY", - "DomainRoutingSpec", - "build_calendar_domain_agent", - "build_confluence_tools", - "build_confluence_domain_agent", - "build_deliverables_tools", - "build_deliverables_domain_agent", - "build_discord_tools", - "build_discord_domain_agent", - "build_domain_agent", - "build_dropbox_tools", - "build_dropbox_domain_agent", - "build_gmail_tools", - "build_gmail_domain_agent", - "build_calendar_tools", - "build_google_drive_tools", - "build_google_drive_domain_agent", - "build_luma_tools", - "build_luma_domain_agent", - "build_memory_tools", - "build_memory_domain_agent", - "build_notion_tools", - "build_notion_domain_agent", - "build_onedrive_tools", - "build_onedrive_domain_agent", - "build_registry_dependencies", - "build_registry_tools_for_category", - "build_research_tools", - "build_research_domain_agent", - "build_supervisor_agent", - "build_supervisor_routing_tools", - "build_teams_tools", - "build_teams_domain_agent", - "connector_binding", - "compose_child_task", - "create_multi_agent_chat", - "extract_last_assistant_text", - "read_prompt_md", - "routing_tools_from_specs", -] diff --git a/surfsense_backend/app/agents/multi_agent_chat/core/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/core/__init__.py deleted file mode 100644 index 0299138fe..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/core/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Cross-cutting building blocks (prompts, agents, delegation, registry) — not domain logic.""" - -from app.agents.multi_agent_chat.core.agents import build_domain_agent -from app.agents.multi_agent_chat.core.bindings import connector_binding -from app.agents.multi_agent_chat.core.delegation import compose_child_task -from app.agents.multi_agent_chat.core.invocation import extract_last_assistant_text -from app.agents.multi_agent_chat.core.prompts import read_prompt_md -from app.agents.multi_agent_chat.core.registry import ( - REGISTRY_ROUTING_CATEGORY_KEYS, - TOOL_NAMES_BY_CATEGORY, - build_registry_dependencies, - build_registry_tools_for_category, -) - -__all__ = [ - "REGISTRY_ROUTING_CATEGORY_KEYS", - "TOOL_NAMES_BY_CATEGORY", - "build_domain_agent", - "build_registry_dependencies", - "build_registry_tools_for_category", - "compose_child_task", - "connector_binding", - "extract_last_assistant_text", - "read_prompt_md", -] diff --git a/surfsense_backend/app/agents/multi_agent_chat/core/agents/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/core/agents/__init__.py deleted file mode 100644 index 7586c72b0..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/core/agents/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Compiled subgraph factories shared by domain slices.""" - -from app.agents.multi_agent_chat.core.agents.domain_graph import build_domain_agent - -__all__ = ["build_domain_agent"] diff --git a/surfsense_backend/app/agents/multi_agent_chat/core/agents/domain_graph.py b/surfsense_backend/app/agents/multi_agent_chat/core/agents/domain_graph.py deleted file mode 100644 index 51b745553..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/core/agents/domain_graph.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Compile a domain LangGraph agent from a co-located prompt + tool list.""" - -from __future__ import annotations - -from collections.abc import Sequence - -from langchain.agents import create_agent -from langchain_core.language_models import BaseChatModel -from langchain_core.tools import BaseTool - -from app.agents.multi_agent_chat.core.prompts import read_prompt_md - - -def build_domain_agent( - llm: BaseChatModel, - tools: Sequence[BaseTool], - *, - prompt_package: str, - prompt_stem: str = "domain_prompt", -): - """``create_agent`` + ``{prompt_stem}.md`` loaded from ``prompt_package``.""" - system_prompt = read_prompt_md(prompt_package, prompt_stem) - return create_agent( - llm, - system_prompt=system_prompt, - tools=list(tools), - ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/core/bindings/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/core/bindings/__init__.py deleted file mode 100644 index c15375e47..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/core/bindings/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Search-space / DB kwargs shared by main-chat tool factories (distinct from ``expert_agent.connectors`` integrations).""" - -from app.agents.multi_agent_chat.core.bindings.binding import connector_binding - -__all__ = ["connector_binding"] diff --git a/surfsense_backend/app/agents/multi_agent_chat/core/bindings/binding.py b/surfsense_backend/app/agents/multi_agent_chat/core/bindings/binding.py deleted file mode 100644 index da82e3b3c..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/core/bindings/binding.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Shared kwargs dict for main-chat tool factories (DB session + search space + user).""" - -from __future__ import annotations - -from sqlalchemy.ext.asyncio import AsyncSession - - -def connector_binding( - *, - db_session: AsyncSession, - search_space_id: int, - user_id: str, -) -> dict[str, AsyncSession | int | str]: - return { - "db_session": db_session, - "search_space_id": search_space_id, - "user_id": user_id, - } diff --git a/surfsense_backend/app/agents/multi_agent_chat/core/delegation/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/core/delegation/__init__.py deleted file mode 100644 index cc27ec6f5..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/core/delegation/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Supervisor → domain message shaping.""" - -from app.agents.multi_agent_chat.core.delegation.child_task import compose_child_task - -__all__ = ["compose_child_task"] diff --git a/surfsense_backend/app/agents/multi_agent_chat/core/delegation/child_task.py b/surfsense_backend/app/agents/multi_agent_chat/core/delegation/child_task.py deleted file mode 100644 index ac8a5b25a..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/core/delegation/child_task.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Fold orchestrator-selected context into the single user message sent to a domain agent.""" - -from __future__ import annotations - - -def compose_child_task(task: str, *, curated_context: str | None = None) -> str: - """Build the domain-agent user message: optional curated KB/context + task. - - When ``curated_context`` is set (from supervisor/KB wiring), it is prepended so the - child sees only what orchestration chose — not the full parent transcript. - """ - task = task.strip() - if not curated_context or not curated_context.strip(): - return f"\n{task}\n" - return ( - "\n" - f"{curated_context.strip()}\n" - "\n\n" - "\n" - f"{task}\n" - "" - ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/core/invocation/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/core/invocation/__init__.py deleted file mode 100644 index 60d0ff9fa..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/core/invocation/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Parsing LangGraph invoke results.""" - -from app.agents.multi_agent_chat.core.invocation.output import extract_last_assistant_text - -__all__ = ["extract_last_assistant_text"] diff --git a/surfsense_backend/app/agents/multi_agent_chat/core/invocation/output.py b/surfsense_backend/app/agents/multi_agent_chat/core/invocation/output.py deleted file mode 100644 index 2bbab6e57..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/core/invocation/output.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Extract displayable text from a LangGraph agent ``invoke`` / ``ainvoke`` result.""" - -from __future__ import annotations - -from typing import Any - - -def extract_last_assistant_text(result: dict[str, Any]) -> str: - """Return the last message's string content, or ``\"\"`` if missing.""" - messages = result.get("messages") or [] - if not messages: - return "" - last = messages[-1] - content = getattr(last, "content", None) - if isinstance(content, str): - return content - return str(last) diff --git a/surfsense_backend/app/agents/multi_agent_chat/core/mcp_partition.py b/surfsense_backend/app/agents/multi_agent_chat/core/mcp_partition.py deleted file mode 100644 index a1ee6fdb6..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/core/mcp_partition.py +++ /dev/null @@ -1,135 +0,0 @@ -"""Partition MCP tools onto multi-agent expert routes (read-only; does not change the MCP loader). - -Uses the same connector discovery shape as ``load_mcp_tools`` (copied query below). Tools come from -``app.agents.new_chat.tools.mcp_tool.load_mcp_tools``; routing uses metadata already set there: - -- HTTP tools: ``metadata["mcp_connector_id"]`` → DB connector row → expert route. -- stdio tools: no connector id on the tool; ``metadata["mcp_connector_name"]`` → connector name map - (duplicate names: last row wins — rare). -""" - -from __future__ import annotations - -import logging -from collections import defaultdict -from collections.abc import Sequence -from typing import Any - -from langchain_core.tools import BaseTool -from sqlalchemy import cast, select -from sqlalchemy.dialects.postgresql import JSONB -from sqlalchemy.ext.asyncio import AsyncSession - -from app.db import SearchSourceConnector - -logger = logging.getLogger(__name__) - -# SurfSense ``SearchSourceConnectorType`` string → supervisor routing key (must match -# ``DomainRoutingSpec.tool_name`` values used in ``supervisor_routing``). -_CONNECTOR_TYPE_TO_EXPERT_ROUTE: dict[str, str] = { - "GOOGLE_GMAIL_CONNECTOR": "gmail", - "COMPOSIO_GMAIL_CONNECTOR": "gmail", - "GOOGLE_CALENDAR_CONNECTOR": "calendar", - "COMPOSIO_GOOGLE_CALENDAR_CONNECTOR": "calendar", - "DISCORD_CONNECTOR": "discord", - "TEAMS_CONNECTOR": "teams", - "LUMA_CONNECTOR": "luma", - "LINEAR_CONNECTOR": "linear", - "JIRA_CONNECTOR": "jira", - "CLICKUP_CONNECTOR": "clickup", - "SLACK_CONNECTOR": "slack", - "AIRTABLE_CONNECTOR": "airtable", - # generic_mcp route intentionally disabled for now. - # "MCP_CONNECTOR": "generic_mcp", -} - -# Ordering when appending MCP-only routes (no native registry slice for these types). -MCP_ONLY_ROUTE_KEYS_IN_ORDER: tuple[str, ...] = ( - "linear", - "slack", - "jira", - "clickup", - "airtable", - # generic_mcp intentionally disabled for now. - # "generic_mcp", -) - - -async def fetch_mcp_connector_metadata_maps( - session: AsyncSession, - search_space_id: int, -) -> tuple[dict[int, str], dict[str, str]]: - """Read-only copy of connector discovery used alongside ``load_mcp_tools``. - - Same filter as :func:`app.agents.new_chat.tools.mcp_tool.load_mcp_tools` (connectors with ``server_config``). - """ - result = await session.execute( - select(SearchSourceConnector).filter( - SearchSourceConnector.search_space_id == search_space_id, - cast(SearchSourceConnector.config, JSONB).has_key("server_config"), - ), - ) - id_to_type: dict[int, str] = {} - name_to_type: dict[str, str] = {} - for connector in result.scalars(): - ct = ( - connector.connector_type.value - if hasattr(connector.connector_type, "value") - else str(connector.connector_type) - ) - id_to_type[connector.id] = ct - if connector.name: - name_to_type[connector.name] = ct - return id_to_type, name_to_type - - -def partition_mcp_tools_by_expert_route( - tools: Sequence[BaseTool], - connector_id_to_type: dict[int, str], - connector_name_to_type: dict[str, str], -) -> dict[str, list[BaseTool]]: - """Bucket MCP tools by expert route key. Supervisor never receives raw MCP tools. - - Same inclusion rule as :func:`app.agents.new_chat.tools.registry.build_tools_async`: all tools returned by - ``load_mcp_tools`` are partitioned — connector availability for **registry** builtins is handled via - ``get_connector_gated_tools`` / routing gates; MCP tools are not pre-filtered by inventory here. - """ - buckets: dict[str, list[BaseTool]] = defaultdict(list) - - for tool in tools: - meta: dict[str, Any] = getattr(tool, "metadata", None) or {} - connector_type: str | None = None - - cid = meta.get("mcp_connector_id") - if cid is not None: - try: - cid_int = int(cid) - except (TypeError, ValueError): - cid_int = None - if cid_int is not None: - connector_type = connector_id_to_type.get(cid_int) - - if connector_type is None and meta.get("mcp_transport") == "stdio": - cname = meta.get("mcp_connector_name") - if cname: - connector_type = connector_name_to_type.get(str(cname)) - - if connector_type is None: - logger.debug( - "Skipping MCP tool %r — could not resolve connector type from metadata", - getattr(tool, "name", None), - ) - continue - - route = _CONNECTOR_TYPE_TO_EXPERT_ROUTE.get(connector_type) - if route is None: - logger.warning( - "MCP tool %r has unmapped connector type %s — skipped", - getattr(tool, "name", None), - connector_type, - ) - continue - - buckets[route].append(tool) - - return dict(buckets) diff --git a/surfsense_backend/app/agents/multi_agent_chat/core/prompts/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/core/prompts/__init__.py deleted file mode 100644 index 92dd9b854..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/core/prompts/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Markdown prompt loading for domain and supervisor packages.""" - -from app.agents.multi_agent_chat.core.prompts.load import read_prompt_md - -__all__ = ["read_prompt_md"] diff --git a/surfsense_backend/app/agents/multi_agent_chat/core/prompts/load.py b/surfsense_backend/app/agents/multi_agent_chat/core/prompts/load.py deleted file mode 100644 index 355a26a4f..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/core/prompts/load.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Load ``*.md`` prompt files from co-located packages (domain slices ship ``domain_prompt.md``).""" - -from __future__ import annotations - -from importlib import resources - - -def read_prompt_md(package: str, stem: str) -> str: - """Read ``{stem}.md`` from the given import package (e.g. ``…expert_agent.connectors.notion``).""" - try: - ref = resources.files(package).joinpath(f"{stem}.md") - if not ref.is_file(): - return "" - text = ref.read_text(encoding="utf-8") - except (FileNotFoundError, ModuleNotFoundError, OSError, TypeError): - return "" - if text.endswith("\n"): - text = text[:-1] - return text diff --git a/surfsense_backend/app/agents/multi_agent_chat/core/registry/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/core/registry/__init__.py deleted file mode 100644 index cfd8a5d62..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/core/registry/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Main chat tool registry grouping + dependency bundles for domain slices.""" - -from app.agents.multi_agent_chat.core.registry.categories import ( - REGISTRY_ROUTING_CATEGORY_KEYS, - TOOL_NAMES_BY_CATEGORY, -) -from app.agents.multi_agent_chat.core.registry.dependencies import build_registry_dependencies -from app.agents.multi_agent_chat.core.registry.subset import build_registry_tools_for_category - -__all__ = [ - "REGISTRY_ROUTING_CATEGORY_KEYS", - "TOOL_NAMES_BY_CATEGORY", - "build_registry_dependencies", - "build_registry_tools_for_category", -] diff --git a/surfsense_backend/app/agents/multi_agent_chat/core/registry/categories.py b/surfsense_backend/app/agents/multi_agent_chat/core/registry/categories.py deleted file mode 100644 index 13d8cd12f..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/core/registry/categories.py +++ /dev/null @@ -1,84 +0,0 @@ -"""Registry tool names grouped by multi-agent routing category. - -Each string must match ``ToolDefinition.name`` in -``app.agents.new_chat.tools.registry.BUILTIN_TOOLS`` — these are **not** guessed or MCP-only: -:class:`~app.agents.multi_agent_chat.core.registry.subset.build_registry_tools_for_category` -uses synchronous :func:`~app.agents.new_chat.tools.registry.build_tools`, which only instantiates -``BUILTIN_TOOLS``. MCP tools are loaded separately and merged in ``supervisor_routing``. - -Connectors that exist for search/indexing but have **no** entry in ``BUILTIN_TOOLS`` correctly have -no row here (no chat tools to delegate).""" - -from __future__ import annotations - -# Keys match supervisor routing tool names; values match ``BUILTIN_TOOLS`` names exactly. -TOOL_NAMES_BY_CATEGORY: dict[str, list[str]] = { - "gmail": [ - "search_gmail", - "read_gmail_email", - "create_gmail_draft", - "send_gmail_email", - "trash_gmail_email", - "update_gmail_draft", - ], - "calendar": [ - "search_calendar_events", - "create_calendar_event", - "update_calendar_event", - "delete_calendar_event", - ], - "research": [ - "web_search", - "scrape_webpage", - "search_surfsense_docs", - ], - "deliverables": [ - "generate_podcast", - "generate_video_presentation", - "generate_report", - "generate_resume", - "generate_image", - ], - "memory": [ - "update_memory", - ], - "discord": [ - "list_discord_channels", - "read_discord_messages", - "send_discord_message", - ], - "teams": [ - "list_teams_channels", - "read_teams_messages", - "send_teams_message", - ], - "notion": [ - "create_notion_page", - "update_notion_page", - "delete_notion_page", - ], - "confluence": [ - "create_confluence_page", - "update_confluence_page", - "delete_confluence_page", - ], - "google_drive": [ - "create_google_drive_file", - "delete_google_drive_file", - ], - "dropbox": [ - "create_dropbox_file", - "delete_dropbox_file", - ], - "onedrive": [ - "create_onedrive_file", - "delete_onedrive_file", - ], - "luma": [ - "list_luma_events", - "read_luma_event", - "create_luma_event", - ], -} - -REGISTRY_ROUTING_CATEGORY_KEYS: tuple[str, ...] = tuple(TOOL_NAMES_BY_CATEGORY.keys()) diff --git a/surfsense_backend/app/agents/multi_agent_chat/core/registry/dependencies.py b/surfsense_backend/app/agents/multi_agent_chat/core/registry/dependencies.py deleted file mode 100644 index 24fa6b19c..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/core/registry/dependencies.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Dependency dict for :func:`app.agents.new_chat.tools.registry.build_tools` on expert subgraphs.""" - -from __future__ import annotations - -from typing import Any - -from langchain_core.language_models import BaseChatModel -from sqlalchemy.ext.asyncio import AsyncSession - -from app.db import ChatVisibility - - -def coerce_thread_id_for_registry(thread_id: str | int | None) -> int | None: - """Normalize chat thread id for registry tools that FK to ``new_chat_threads.id``. - - ``create_surfsense_deep_agent`` passes an ``int``; multi-agent wiring may pass - ``str(chat_id)`` for LangGraph/checkpointer consistency. AsyncPG requires ``int`` - for integer columns. - """ - if thread_id is None: - return None - if isinstance(thread_id, int): - return thread_id - s = str(thread_id).strip() - if not s: - return None - if s.isdigit(): - return int(s) - return None - - -def build_registry_dependencies( - *, - db_session: AsyncSession, - search_space_id: int, - user_id: str, - thread_id: str | int | None, - llm: BaseChatModel | None = None, - firecrawl_api_key: str | None = None, - connector_service: Any | None = None, - available_connectors: list[str] | None = None, - available_document_types: list[str] | None = None, - thread_visibility: ChatVisibility = ChatVisibility.PRIVATE, -) -> dict[str, Any]: - """Union of kwargs commonly required by registry factories across category slices. - - Individual categories enable a subset of tools; each tool still validates its own - ``ToolDefinition.requires`` against this dict. - """ - return { - "db_session": db_session, - "search_space_id": search_space_id, - "user_id": user_id, - "thread_id": coerce_thread_id_for_registry(thread_id), - "llm": llm, - "firecrawl_api_key": firecrawl_api_key, - "connector_service": connector_service, - "available_connectors": available_connectors, - "available_document_types": available_document_types, - "thread_visibility": thread_visibility, - } diff --git a/surfsense_backend/app/agents/multi_agent_chat/core/registry/subset.py b/surfsense_backend/app/agents/multi_agent_chat/core/registry/subset.py deleted file mode 100644 index 95db1b64c..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/core/registry/subset.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Build registry tool subsets (``app.agents.new_chat.tools.registry``) for multi-agent domain slices.""" - -from __future__ import annotations - -from typing import Any - -from langchain_core.tools import BaseTool - -from app.agents.new_chat.tools.registry import build_tools -from app.agents.multi_agent_chat.core.registry.categories import TOOL_NAMES_BY_CATEGORY - - -def build_registry_tools_for_category( - dependencies: dict[str, Any], - category: str, -) -> list[BaseTool]: - """Instantiate only the tools registered for ``category`` (see ``TOOL_NAMES_BY_CATEGORY``).""" - names = TOOL_NAMES_BY_CATEGORY.get(category) - if not names: - msg = f"Unknown registry category: {category!r}" - raise ValueError(msg) - return build_tools(dependencies, enabled_tools=names) diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/__init__.py deleted file mode 100644 index 4ca5c00de..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Expert subgraphs (specialists the supervisor delegates to). - -- :mod:`expert_agent.builtins` — cross-cutting registry categories (e.g. research, memory, deliverables). -- :mod:`expert_agent.connectors` — vendor/product integrations (email, chat, documents, … — one slice per route). -""" diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/__init__.py deleted file mode 100644 index 84bd2948d..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Built-ins: broad capability categories from the registry (not single-vendor integrations).""" diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/deliverables/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/deliverables/__init__.py deleted file mode 100644 index 8a225b50b..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/deliverables/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Deliverables vertical slice: registry tools, domain agent, ``domain_prompt.md``.""" - -from app.agents.multi_agent_chat.expert_agent.builtins.deliverables.agent import build_deliverables_domain_agent -from app.agents.multi_agent_chat.expert_agent.builtins.deliverables.slice_tools import ( - build_deliverables_tools, -) - -__all__ = [ - "build_deliverables_tools", - "build_deliverables_domain_agent", -] diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/deliverables/agent.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/deliverables/agent.py deleted file mode 100644 index 729dc9410..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/deliverables/agent.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Deliverables domain agent graph.""" - -from __future__ import annotations - -from collections.abc import Sequence - -import app.agents.multi_agent_chat.expert_agent.builtins.deliverables as deliverables_pkg -from langchain_core.language_models import BaseChatModel -from langchain_core.tools import BaseTool - -from app.agents.multi_agent_chat.core.agents import build_domain_agent - - -def build_deliverables_domain_agent(llm: BaseChatModel, tools: Sequence[BaseTool]): - """Compiled deliverables domain-agent graph.""" - return build_domain_agent( - llm, - tools, - prompt_package=deliverables_pkg.__name__, - prompt_stem="domain_prompt", - ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/deliverables/domain_prompt.md b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/deliverables/domain_prompt.md deleted file mode 100644 index c44f131bb..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/deliverables/domain_prompt.md +++ /dev/null @@ -1,55 +0,0 @@ -You are the SurfSense deliverables operations sub-agent. -You receive delegated instructions from a supervisor agent and return structured results for supervisor synthesis. - - -Produce **deliverables**: shareable **artifacts** the user keeps (reports, slide-style video presentations, podcasts, resumes, images). Use explicit constraints and reliable proof of what was generated. - - - -- `generate_report` -- `generate_podcast` -- `generate_video_presentation` -- `generate_resume` -- `generate_image` - - - -- Use only tools in ``. -- Require essential generation constraints (audience, format, tone, core content). -- If critical constraints are missing, return `status=blocked` with `missing_fields`. -- Never claim artifact generation success without tool confirmation. - - - -- Do not perform connector data mutations unrelated to artifact generation. - - - -- Avoid generating artifacts with missing critical constraints. -- Prefer one complete artifact over partial multi-artifact output. - - - -- On generation failure, return `status=error` with best retry guidance. -- On missing constraints, return `status=blocked` with required fields. - - - -Return **only** one JSON object (no markdown/prose): -{ - "status": "success" | "partial" | "blocked" | "error", - "action_summary": string, - "evidence": { - "artifact_type": "report" | "podcast" | "video_presentation" | "resume" | "image" | null, - "artifact_id": string | null, - "artifact_location": string | null - }, - "next_step": string | null, - "missing_fields": string[] | null, - "assumptions": string[] | null -} -Rules: -- `status=success` -> `next_step=null`, `missing_fields=null`. -- `status=partial|blocked|error` -> `next_step` must be non-null. -- `status=blocked` due to missing required inputs -> `missing_fields` must be non-null. - diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/deliverables/slice_tools.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/deliverables/slice_tools.py deleted file mode 100644 index 42241bda5..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/deliverables/slice_tools.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Registry-backed deliverables tools (reports, media exports, resume, images).""" - -from __future__ import annotations - -from typing import Any - -from langchain_core.tools import BaseTool - -from app.agents.multi_agent_chat.core.registry import build_registry_tools_for_category - - -def build_deliverables_tools(dependencies: dict[str, Any]) -> list[BaseTool]: - """Registry-backed tools for the ``deliverables`` category.""" - return build_registry_tools_for_category(dependencies, "deliverables") diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/memory/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/memory/__init__.py deleted file mode 100644 index 0499bfdf4..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/memory/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Memory vertical slice: registry tools, domain agent, ``domain_prompt.md``.""" - -from app.agents.multi_agent_chat.expert_agent.builtins.memory.agent import build_memory_domain_agent -from app.agents.multi_agent_chat.expert_agent.builtins.memory.slice_tools import build_memory_tools - -__all__ = [ - "build_memory_tools", - "build_memory_domain_agent", -] diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/memory/agent.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/memory/agent.py deleted file mode 100644 index 6a0c115c2..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/memory/agent.py +++ /dev/null @@ -1,45 +0,0 @@ -"""Memory domain agent graph.""" - -from __future__ import annotations - -from collections.abc import Sequence - -import app.agents.multi_agent_chat.expert_agent.builtins.memory as memory_pkg -from langchain.agents import create_agent -from langchain_core.language_models import BaseChatModel -from langchain_core.tools import BaseTool - -from app.agents.multi_agent_chat.core.prompts import read_prompt_md -from app.db import ChatVisibility - -_PRIVATE_VISIBILITY_POLICY = ( - "This thread is private. Store user-specific long-lived preferences, facts, and instructions." -) -_TEAM_VISIBILITY_POLICY = ( - "This thread is shared with the search space. Store only team-appropriate shared preferences," - " facts, and instructions that are safe for all members to inherit." -) - - -def _render_memory_prompt(thread_visibility: ChatVisibility | None) -> str: - template = read_prompt_md(memory_pkg.__name__, "domain_prompt") - policy = ( - _TEAM_VISIBILITY_POLICY - if thread_visibility == ChatVisibility.SEARCH_SPACE - else _PRIVATE_VISIBILITY_POLICY - ) - return template.replace("{{MEMORY_VISIBILITY_POLICY}}", policy) - - -def build_memory_domain_agent( - llm: BaseChatModel, - tools: Sequence[BaseTool], - *, - thread_visibility: ChatVisibility | None = None, -): - """Compiled memory domain-agent graph.""" - return create_agent( - llm, - system_prompt=_render_memory_prompt(thread_visibility), - tools=list(tools), - ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/memory/domain_prompt.md b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/memory/domain_prompt.md deleted file mode 100644 index 32becf233..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/memory/domain_prompt.md +++ /dev/null @@ -1,56 +0,0 @@ -You are the SurfSense memory operations sub-agent. -You receive delegated instructions from a supervisor agent and return structured results for supervisor synthesis. - - -Persist durable preferences/facts/instructions with `update_memory` while avoiding transient or unsafe storage. - - - -{{MEMORY_VISIBILITY_POLICY}} - - - -- `update_memory` - - - -- Save only durable information with future value. -- Do not store transient chatter. -- Do not store secrets unless explicitly instructed. -- If memory intent is unclear, return `status=blocked` with the missing intent signal. - - - -- Do not execute non-memory tool actions. -- Do not store irrelevant, transient, or speculative information. - - - -- Prefer minimal-memory writes over over-collection. -- Never claim memory was updated unless `update_memory` succeeded. - - - -- On tool failure, return `status=error` with concise recovery steps. -- When intent is ambiguous, return `status=blocked` with required disambiguation fields. - - - -Return **only** one JSON object (no markdown/prose): -{ - "status": "success" | "partial" | "blocked" | "error", - "action_summary": string, - "evidence": { - "memory_updated": boolean, - "memory_category": "preference" | "fact" | "instruction" | null, - "stored_summary": string | null - }, - "next_step": string | null, - "missing_fields": string[] | null, - "assumptions": string[] | null -} -Rules: -- `status=success` -> `next_step=null`, `missing_fields=null`. -- `status=partial|blocked|error` -> `next_step` must be non-null. -- `status=blocked` due to missing required inputs -> `missing_fields` must be non-null. - diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/memory/slice_tools.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/memory/slice_tools.py deleted file mode 100644 index 7f4d2d29a..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/memory/slice_tools.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Registry-backed memory tools (long-term user or team memory).""" - -from __future__ import annotations - -from typing import Any - -from langchain_core.tools import BaseTool - -from app.agents.multi_agent_chat.core.registry import build_registry_tools_for_category - - -def build_memory_tools(dependencies: dict[str, Any]) -> list[BaseTool]: - """Registry-backed tools for the ``memory`` category.""" - return build_registry_tools_for_category(dependencies, "memory") diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/research/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/research/__init__.py deleted file mode 100644 index ada6c9853..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/research/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Research vertical slice: registry tools, domain agent, ``domain_prompt.md``.""" - -from app.agents.multi_agent_chat.expert_agent.builtins.research.agent import build_research_domain_agent -from app.agents.multi_agent_chat.expert_agent.builtins.research.slice_tools import build_research_tools - -__all__ = [ - "build_research_tools", - "build_research_domain_agent", -] diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/research/agent.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/research/agent.py deleted file mode 100644 index a7dc635c9..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/research/agent.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Research domain agent graph.""" - -from __future__ import annotations - -from collections.abc import Sequence - -import app.agents.multi_agent_chat.expert_agent.builtins.research as research_pkg -from langchain_core.language_models import BaseChatModel -from langchain_core.tools import BaseTool - -from app.agents.multi_agent_chat.core.agents import build_domain_agent - - -def build_research_domain_agent(llm: BaseChatModel, tools: Sequence[BaseTool]): - """Compiled research domain-agent graph.""" - return build_domain_agent( - llm, - tools, - prompt_package=research_pkg.__name__, - prompt_stem="domain_prompt", - ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/research/domain_prompt.md b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/research/domain_prompt.md deleted file mode 100644 index cf558db62..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/research/domain_prompt.md +++ /dev/null @@ -1,53 +0,0 @@ -You are the SurfSense research operations sub-agent. -You receive delegated instructions from a supervisor agent and return structured results for supervisor synthesis. - - -Gather and synthesize evidence using SurfSense research tools with clear citations and uncertainty reporting. - - - -- `web_search` -- `scrape_webpage` -- `search_surfsense_docs` - - - -- Use only tools in ``. -- Prefer primary and recent sources when recency matters. -- If the delegated request is underspecified, return `status=blocked` with the missing research constraints. -- Never fabricate facts, citations, URLs, or quote text. - - - -- Do not execute connector mutations (email/calendar/docs/chat writes) or deliverable generation. - - - -- Report uncertainty explicitly when evidence is incomplete or conflicting. -- Never present unverified claims as facts. - - - -- On tool failure, return `status=error` with a concise recovery `next_step`. -- On no useful evidence, return `status=blocked` with recommended narrower filters. - - - -Return **only** one JSON object (no markdown/prose): -{ - "status": "success" | "partial" | "blocked" | "error", - "action_summary": string, - "evidence": { - "findings": string[], - "sources": string[], - "confidence": "high" | "medium" | "low" - }, - "next_step": string | null, - "missing_fields": string[] | null, - "assumptions": string[] | null -} -Rules: -- `status=success` -> `next_step=null`, `missing_fields=null`. -- `status=partial|blocked|error` -> `next_step` must be non-null. -- `status=blocked` due to missing required inputs -> `missing_fields` must be non-null. - diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/research/slice_tools.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/research/slice_tools.py deleted file mode 100644 index 85a2a9dd9..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/builtins/research/slice_tools.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Registry-backed research tools (web, scrape, SurfSense docs help).""" - -from __future__ import annotations - -from typing import Any - -from langchain_core.tools import BaseTool - -from app.agents.multi_agent_chat.core.registry import build_registry_tools_for_category - - -def build_research_tools(dependencies: dict[str, Any]) -> list[BaseTool]: - """Registry-backed tools for the ``research`` category.""" - return build_registry_tools_for_category(dependencies, "research") diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/__init__.py deleted file mode 100644 index f752e4dd9..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""External integrations: third-party products (explicit factories or registry-backed connector tools).""" diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/calendar/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/calendar/__init__.py deleted file mode 100644 index 65b880dd0..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/calendar/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Google Calendar vertical slice: registry tools, domain agent, ``domain_prompt.md``.""" - -from app.agents.multi_agent_chat.expert_agent.connectors.calendar.agent import build_calendar_domain_agent -from app.agents.multi_agent_chat.expert_agent.connectors.calendar.slice_tools import ( - build_calendar_tools, -) - -__all__ = [ - "build_calendar_domain_agent", - "build_calendar_tools", -] diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/calendar/agent.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/calendar/agent.py deleted file mode 100644 index 64a82c6ba..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/calendar/agent.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Google Calendar domain agent graph.""" - -from __future__ import annotations - -from collections.abc import Sequence - -import app.agents.multi_agent_chat.expert_agent.connectors.calendar as calendar_pkg -from langchain_core.language_models import BaseChatModel -from langchain_core.tools import BaseTool - -from app.agents.multi_agent_chat.core.agents import build_domain_agent - - -def build_calendar_domain_agent(llm: BaseChatModel, tools: Sequence[BaseTool]): - """Compiled Google Calendar domain-agent graph.""" - return build_domain_agent( - llm, - tools, - prompt_package=calendar_pkg.__name__, - prompt_stem="domain_prompt", - ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/calendar/domain_prompt.md b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/calendar/domain_prompt.md deleted file mode 100644 index a7ef846d5..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/calendar/domain_prompt.md +++ /dev/null @@ -1,62 +0,0 @@ -You are the Google Calendar operations sub-agent. -You receive delegated instructions from a supervisor agent and return structured results for supervisor synthesis. - - -Execute calendar event operations (search, create, update, delete) accurately with timezone-safe scheduling. - - - -- `search_calendar_events` -- `create_calendar_event` -- `update_calendar_event` -- `delete_calendar_event` - - - -- Use only tools in ``. -- Resolve relative dates against current runtime timestamp. -- If required fields (date/time/timezone/target event) are missing or ambiguous, return `status=blocked` with `missing_fields` and supervisor `next_step`. -- Never invent event IDs or mutation results. - - - -- Do not perform non-calendar tasks. - - - -- Before update/delete, ensure event target is explicit. -- Never claim event mutation success without tool confirmation. - - - -- On tool failure, return `status=error` with concise recovery `next_step`. -- On ambiguity, return `status=blocked` with top event candidates. - - - -Return **only** one JSON object (no markdown/prose): -{ - "status": "success" | "partial" | "blocked" | "error", - "action_summary": string, - "evidence": { - "event_id": string | null, - "title": string | null, - "start_at": string (ISO 8601 with timezone) | null, - "end_at": string (ISO 8601 with timezone) | null, - "matched_candidates": [ - { - "event_id": string, - "title": string | null, - "start_at": string (ISO 8601 with timezone) | null - } - ] | null - }, - "next_step": string | null, - "missing_fields": string[] | null, - "assumptions": string[] | null -} -Rules: -- `status=success` -> `next_step=null`, `missing_fields=null`. -- `status=partial|blocked|error` -> `next_step` must be non-null. -- `status=blocked` due to missing required inputs -> `missing_fields` must be non-null. - diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/calendar/slice_tools.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/calendar/slice_tools.py deleted file mode 100644 index e2f2b404a..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/calendar/slice_tools.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Registry-backed Google Calendar tools.""" - -from __future__ import annotations - -from typing import Any - -from langchain_core.tools import BaseTool - -from app.agents.multi_agent_chat.core.registry import build_registry_tools_for_category - - -def build_calendar_tools(dependencies: dict[str, Any]) -> list[BaseTool]: - """Registry-backed tools for the ``calendar`` category.""" - return build_registry_tools_for_category(dependencies, "calendar") diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/confluence/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/confluence/__init__.py deleted file mode 100644 index a3aa01959..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/confluence/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Confluence connector slice.""" - -from app.agents.multi_agent_chat.expert_agent.connectors.confluence.agent import build_confluence_domain_agent -from app.agents.multi_agent_chat.expert_agent.connectors.confluence.slice_tools import ( - build_confluence_tools, -) - -__all__ = [ - "build_confluence_tools", - "build_confluence_domain_agent", -] diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/confluence/agent.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/confluence/agent.py deleted file mode 100644 index 2746d31f0..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/confluence/agent.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Confluence domain agent graph.""" - -from __future__ import annotations - -from collections.abc import Sequence - -import app.agents.multi_agent_chat.expert_agent.connectors.confluence as confluence_pkg -from langchain_core.language_models import BaseChatModel -from langchain_core.tools import BaseTool - -from app.agents.multi_agent_chat.core.agents import build_domain_agent - - -def build_confluence_domain_agent(llm: BaseChatModel, tools: Sequence[BaseTool]): - """Compiled Confluence domain-agent graph.""" - return build_domain_agent( - llm, - tools, - prompt_package=confluence_pkg.__name__, - prompt_stem="domain_prompt", - ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/confluence/domain_prompt.md b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/confluence/domain_prompt.md deleted file mode 100644 index 4d3b7462c..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/confluence/domain_prompt.md +++ /dev/null @@ -1,55 +0,0 @@ -You are the Confluence operations sub-agent. -You receive delegated instructions from a supervisor agent and return structured results for supervisor synthesis. - - -Execute Confluence page operations accurately in the connected space. - - - -- `create_confluence_page` -- `update_confluence_page` -- `delete_confluence_page` - - - -- Use only tools in ``. -- Verify target page and intended mutation before update/delete. -- If target page is ambiguous, return `status=blocked` with candidate options for supervisor disambiguation. -- Never invent page IDs, titles, or mutation outcomes. - - - -- Do not perform non-Confluence tasks. - - - -- Never claim page mutation success without tool confirmation. -- If destructive action appears already completed in this session, do not repeat; return prior evidence with an `assumptions` note. - - - -- On tool failure, return `status=error` with concise retry/recovery `next_step`. -- On unresolved page ambiguity, return `status=blocked` with candidates. - - - -Return **only** one JSON object (no markdown/prose): -{ - "status": "success" | "partial" | "blocked" | "error", - "action_summary": string, - "evidence": { - "page_id": string | null, - "page_title": string | null, - "matched_candidates": [ - { "page_id": string, "page_title": string | null } - ] | null - }, - "next_step": string | null, - "missing_fields": string[] | null, - "assumptions": string[] | null -} -Rules: -- `status=success` -> `next_step=null`, `missing_fields=null`. -- `status=partial|blocked|error` -> `next_step` must be non-null. -- `status=blocked` due to missing required inputs -> `missing_fields` must be non-null. - diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/confluence/slice_tools.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/confluence/slice_tools.py deleted file mode 100644 index 3f4f2d45c..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/confluence/slice_tools.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Registry-backed Confluence tools.""" - -from __future__ import annotations - -from typing import Any - -from langchain_core.tools import BaseTool - -from app.agents.multi_agent_chat.core.registry import build_registry_tools_for_category - - -def build_confluence_tools(dependencies: dict[str, Any]) -> list[BaseTool]: - """Registry-backed tools for the ``confluence`` category.""" - return build_registry_tools_for_category(dependencies, "confluence") diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/discord/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/discord/__init__.py deleted file mode 100644 index a7b864f16..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/discord/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Discord vertical slice: registry tools, domain agent, ``domain_prompt.md``.""" - -from app.agents.multi_agent_chat.expert_agent.connectors.discord.agent import build_discord_domain_agent -from app.agents.multi_agent_chat.expert_agent.connectors.discord.slice_tools import build_discord_tools - -__all__ = [ - "build_discord_tools", - "build_discord_domain_agent", -] diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/discord/agent.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/discord/agent.py deleted file mode 100644 index dfcd4ec45..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/discord/agent.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Discord domain agent graph.""" - -from __future__ import annotations - -from collections.abc import Sequence - -import app.agents.multi_agent_chat.expert_agent.connectors.discord as discord_pkg -from langchain_core.language_models import BaseChatModel -from langchain_core.tools import BaseTool - -from app.agents.multi_agent_chat.core.agents import build_domain_agent - - -def build_discord_domain_agent(llm: BaseChatModel, tools: Sequence[BaseTool]): - """Compiled Discord domain-agent graph.""" - return build_domain_agent( - llm, - tools, - prompt_package=discord_pkg.__name__, - prompt_stem="domain_prompt", - ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/discord/domain_prompt.md b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/discord/domain_prompt.md deleted file mode 100644 index 40e9eb314..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/discord/domain_prompt.md +++ /dev/null @@ -1,56 +0,0 @@ -You are the Discord operations sub-agent. -You receive delegated instructions from a supervisor agent and return structured results for supervisor synthesis. - - -Execute Discord reads and sends accurately in the connected server/workspace. - - - -- `list_discord_channels` -- `read_discord_messages` -- `send_discord_message` - - - -- Use only tools in ``. -- Resolve channel/thread targets before reads/sends. -- If target is ambiguous, return `status=blocked` with candidate channels/threads. -- Never invent message content, sender identity, timestamps, or delivery results. - - - -- Do not perform non-Discord tasks. - - - -- Before send, verify destination and message intent match delegated instructions. -- Never claim send success without tool confirmation. - - - -- On tool failure, return `status=error` with concise recovery `next_step`. -- On unresolved destination ambiguity, return `status=blocked` with candidate options. - - - -Return **only** one JSON object (no markdown/prose): -{ - "status": "success" | "partial" | "blocked" | "error", - "action_summary": string, - "evidence": { - "channel_id": string | null, - "thread_id": string | null, - "message_id": string | null, - "matched_candidates": [ - { "channel_id": string, "thread_id": string | null, "label": string | null } - ] | null - }, - "next_step": string | null, - "missing_fields": string[] | null, - "assumptions": string[] | null -} -Rules: -- `status=success` -> `next_step=null`, `missing_fields=null`. -- `status=partial|blocked|error` -> `next_step` must be non-null. -- `status=blocked` due to missing required inputs -> `missing_fields` must be non-null. - diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/discord/slice_tools.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/discord/slice_tools.py deleted file mode 100644 index 79eea4f3f..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/discord/slice_tools.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Registry-backed Discord tools.""" - -from __future__ import annotations - -from typing import Any - -from langchain_core.tools import BaseTool - -from app.agents.multi_agent_chat.core.registry import build_registry_tools_for_category - - -def build_discord_tools(dependencies: dict[str, Any]) -> list[BaseTool]: - """Registry-backed tools for the ``discord`` category.""" - return build_registry_tools_for_category(dependencies, "discord") diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/dropbox/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/dropbox/__init__.py deleted file mode 100644 index 61c58aaa6..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/dropbox/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Dropbox connector slice.""" - -from app.agents.multi_agent_chat.expert_agent.connectors.dropbox.agent import build_dropbox_domain_agent -from app.agents.multi_agent_chat.expert_agent.connectors.dropbox.slice_tools import ( - build_dropbox_tools, -) - -__all__ = [ - "build_dropbox_tools", - "build_dropbox_domain_agent", -] diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/dropbox/agent.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/dropbox/agent.py deleted file mode 100644 index 6913f4e6f..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/dropbox/agent.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Dropbox domain agent graph.""" - -from __future__ import annotations - -from collections.abc import Sequence - -import app.agents.multi_agent_chat.expert_agent.connectors.dropbox as dropbox_pkg -from langchain_core.language_models import BaseChatModel -from langchain_core.tools import BaseTool - -from app.agents.multi_agent_chat.core.agents import build_domain_agent - - -def build_dropbox_domain_agent(llm: BaseChatModel, tools: Sequence[BaseTool]): - """Compiled Dropbox domain-agent graph.""" - return build_domain_agent( - llm, - tools, - prompt_package=dropbox_pkg.__name__, - prompt_stem="domain_prompt", - ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/dropbox/domain_prompt.md b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/dropbox/domain_prompt.md deleted file mode 100644 index 4b19be794..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/dropbox/domain_prompt.md +++ /dev/null @@ -1,52 +0,0 @@ -You are the Dropbox operations sub-agent. -You receive delegated instructions from a supervisor agent and return structured results for supervisor synthesis. - - -Execute Dropbox file create/delete actions accurately in the connected account. - - - -- `create_dropbox_file` -- `delete_dropbox_file` - - - -- Use only tools in ``. -- Ensure target path/file identity is explicit before mutate actions. -- If target is ambiguous, return `status=blocked` with candidate paths. -- Never invent file IDs/paths or mutation outcomes. - - - -- Do not perform non-Dropbox tasks. - - - -- Never claim file mutation success without tool confirmation. - - - -- On tool failure, return `status=error` with concise recovery `next_step`. -- On target ambiguity, return `status=blocked` with candidate paths. - - - -Return **only** one JSON object (no markdown/prose): -{ - "status": "success" | "partial" | "blocked" | "error", - "action_summary": string, - "evidence": { - "file_path": string | null, - "file_id": string | null, - "operation": "create" | "delete" | null, - "matched_candidates": string[] | null - }, - "next_step": string | null, - "missing_fields": string[] | null, - "assumptions": string[] | null -} -Rules: -- `status=success` -> `next_step=null`, `missing_fields=null`. -- `status=partial|blocked|error` -> `next_step` must be non-null. -- `status=blocked` due to missing required inputs -> `missing_fields` must be non-null. - diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/dropbox/slice_tools.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/dropbox/slice_tools.py deleted file mode 100644 index ff28a5b71..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/dropbox/slice_tools.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Registry-backed Dropbox tools.""" - -from __future__ import annotations - -from typing import Any - -from langchain_core.tools import BaseTool - -from app.agents.multi_agent_chat.core.registry import build_registry_tools_for_category - - -def build_dropbox_tools(dependencies: dict[str, Any]) -> list[BaseTool]: - """Registry-backed tools for the ``dropbox`` category.""" - return build_registry_tools_for_category(dependencies, "dropbox") diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/gmail/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/gmail/__init__.py deleted file mode 100644 index f7f899b4b..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/gmail/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Gmail vertical slice: registry tools, domain agent, ``domain_prompt.md``.""" - -from app.agents.multi_agent_chat.expert_agent.connectors.gmail.agent import build_gmail_domain_agent -from app.agents.multi_agent_chat.expert_agent.connectors.gmail.slice_tools import build_gmail_tools - -__all__ = [ - "build_gmail_tools", - "build_gmail_domain_agent", -] diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/gmail/agent.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/gmail/agent.py deleted file mode 100644 index 76d9c8cef..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/gmail/agent.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Gmail domain agent graph.""" - -from __future__ import annotations - -from collections.abc import Sequence - -import app.agents.multi_agent_chat.expert_agent.connectors.gmail as gmail_pkg -from langchain_core.language_models import BaseChatModel -from langchain_core.tools import BaseTool - -from app.agents.multi_agent_chat.core.agents import build_domain_agent - - -def build_gmail_domain_agent(llm: BaseChatModel, tools: Sequence[BaseTool]): - """Compiled Gmail domain-agent graph.""" - return build_domain_agent( - llm, - tools, - prompt_package=gmail_pkg.__name__, - prompt_stem="domain_prompt", - ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/gmail/domain_prompt.md b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/gmail/domain_prompt.md deleted file mode 100644 index 961100261..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/gmail/domain_prompt.md +++ /dev/null @@ -1,82 +0,0 @@ -You are the Gmail operations sub-agent. -You receive delegated instructions from a supervisor agent and return structured results for supervisor synthesis. - - -Execute Gmail operations accurately: search/read emails, prepare drafts, send, and trash. - - - -- `search_gmail`: find candidate emails with query constraints. -- `read_gmail_email`: read one message in full detail. -- `create_gmail_draft`: create a new draft. -- `update_gmail_draft`: modify an existing draft. -- `send_gmail_email`: send an email. -- `trash_gmail_email`: move an email to trash. - - - -- Use only tools in ``. -- Build precise search queries using Gmail operators when possible (`from:`, `to:`, `subject:`, `after:`, `before:`, `has:attachment`, `is:unread`, `label:`). -- Resolve relative dates against runtime timestamp; prefer narrower interpretation. -- For reply requests, identify the target thread/email via search + read before drafting. -- If required fields are missing or target selection is ambiguous, return `status=blocked` with `missing_fields` and disambiguation candidates. -- Never invent IDs, recipients, timestamps, quoted text, or tool outcomes. - - - -- Do not perform non-Gmail work. -- Filing operations not represented in `` (archive/label/mark-read/move-folder) are unsupported here. - - - -- For send: verify draft `to`, `subject`, and `body` match delegated instructions. -- If any send-critical field was inferred, do not send; return `status=blocked` with inferred values in `assumptions`. -- For trash: ensure explicit target match before deletion. -- If a destructive action appears already completed this session, do not repeat; return prior evidence. - - - -- On tool failure, return `status=error` with concise recovery `next_step`. -- If search has no strong match, return `status=blocked` with suggested tighter filters. -- If multiple strong candidates remain for risky actions, return `status=blocked` with top options. - - - -Return **only** one JSON object (no markdown/prose): -{ - "status": "success" | "partial" | "blocked" | "error", - "action_summary": string, - "evidence": { - "email_id": string | null, - "thread_id": string | null, - "subject": string | null, - "sender": string | null, - "recipients": string[] | null, - "received_at": string (ISO 8601 with timezone) | null, - "sent_message": { - "id": string, - "to": string[], - "subject": string | null, - "sent_at": string (ISO 8601 with timezone) | null - } | null, - "matched_candidates": [ - { - "email_id": string, - "subject": string | null, - "sender": string | null, - "received_at": string (ISO 8601 with timezone) | null - } - ] | null - }, - "next_step": string | null, - "missing_fields": string[] | null, - "assumptions": string[] | null -} - -Rules: -- `status=success` -> `next_step=null`, `missing_fields=null`. -- `status=partial|blocked|error` -> `next_step` must be non-null. -- `status=blocked` due to missing required inputs -> `missing_fields` must be non-null. -- For blocked ambiguity, include options in `evidence.matched_candidates`. -- For trash actions, `evidence.email_id` is the trashed message. - diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/gmail/slice_tools.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/gmail/slice_tools.py deleted file mode 100644 index 87876804e..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/gmail/slice_tools.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Registry-backed Gmail tools.""" - -from __future__ import annotations - -from typing import Any - -from langchain_core.tools import BaseTool - -from app.agents.multi_agent_chat.core.registry import build_registry_tools_for_category - - -def build_gmail_tools(dependencies: dict[str, Any]) -> list[BaseTool]: - """Registry-backed tools for the ``gmail`` category.""" - return build_registry_tools_for_category(dependencies, "gmail") diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/google_drive/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/google_drive/__init__.py deleted file mode 100644 index b1cf3680d..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/google_drive/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Google Drive connector slice.""" - -from app.agents.multi_agent_chat.expert_agent.connectors.google_drive.agent import ( - build_google_drive_domain_agent, -) -from app.agents.multi_agent_chat.expert_agent.connectors.google_drive.slice_tools import ( - build_google_drive_tools, -) - -__all__ = [ - "build_google_drive_tools", - "build_google_drive_domain_agent", -] diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/google_drive/agent.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/google_drive/agent.py deleted file mode 100644 index 674c17188..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/google_drive/agent.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Google Drive domain agent graph.""" - -from __future__ import annotations - -from collections.abc import Sequence - -import app.agents.multi_agent_chat.expert_agent.connectors.google_drive as google_drive_pkg -from langchain_core.language_models import BaseChatModel -from langchain_core.tools import BaseTool - -from app.agents.multi_agent_chat.core.agents import build_domain_agent - - -def build_google_drive_domain_agent(llm: BaseChatModel, tools: Sequence[BaseTool]): - """Compiled Google Drive domain-agent graph.""" - return build_domain_agent( - llm, - tools, - prompt_package=google_drive_pkg.__name__, - prompt_stem="domain_prompt", - ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/google_drive/domain_prompt.md b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/google_drive/domain_prompt.md deleted file mode 100644 index 09dc0caa2..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/google_drive/domain_prompt.md +++ /dev/null @@ -1,54 +0,0 @@ -You are the Google Drive operations sub-agent. -You receive delegated instructions from a supervisor agent and return structured results for supervisor synthesis. - - -Execute Google Drive file operations accurately in the connected account. - - - -- `create_google_drive_file` -- `delete_google_drive_file` - - - -- Use only tools in ``. -- Ensure target file identity/path is explicit before mutate actions. -- If target is ambiguous, return `status=blocked` with candidate files. -- Never invent file IDs/names or mutation outcomes. - - - -- Do not perform non-Google-Drive tasks. - - - -- Never claim file mutation success without tool confirmation. - - - -- On tool failure, return `status=error` with concise recovery `next_step`. -- On target ambiguity, return `status=blocked` with candidate files. - - - -Return **only** one JSON object (no markdown/prose): -{ - "status": "success" | "partial" | "blocked" | "error", - "action_summary": string, - "evidence": { - "file_id": string | null, - "file_name": string | null, - "operation": "create" | "delete" | null, - "matched_candidates": [ - { "file_id": string, "file_name": string | null } - ] | null - }, - "next_step": string | null, - "missing_fields": string[] | null, - "assumptions": string[] | null -} -Rules: -- `status=success` -> `next_step=null`, `missing_fields=null`. -- `status=partial|blocked|error` -> `next_step` must be non-null. -- `status=blocked` due to missing required inputs -> `missing_fields` must be non-null. - diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/google_drive/slice_tools.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/google_drive/slice_tools.py deleted file mode 100644 index ee6defe4b..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/google_drive/slice_tools.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Registry-backed Google Drive tools.""" - -from __future__ import annotations - -from typing import Any - -from langchain_core.tools import BaseTool - -from app.agents.multi_agent_chat.core.registry import build_registry_tools_for_category - - -def build_google_drive_tools(dependencies: dict[str, Any]) -> list[BaseTool]: - """Registry-backed tools for the ``google_drive`` category.""" - return build_registry_tools_for_category(dependencies, "google_drive") diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/luma/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/luma/__init__.py deleted file mode 100644 index 6c070ebdd..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/luma/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Luma vertical slice: registry tools, domain agent, ``domain_prompt.md``.""" - -from app.agents.multi_agent_chat.expert_agent.connectors.luma.agent import build_luma_domain_agent -from app.agents.multi_agent_chat.expert_agent.connectors.luma.slice_tools import build_luma_tools - -__all__ = [ - "build_luma_tools", - "build_luma_domain_agent", -] diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/luma/agent.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/luma/agent.py deleted file mode 100644 index d0d3c11d9..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/luma/agent.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Luma domain agent graph.""" - -from __future__ import annotations - -from collections.abc import Sequence - -import app.agents.multi_agent_chat.expert_agent.connectors.luma as luma_pkg -from langchain_core.language_models import BaseChatModel -from langchain_core.tools import BaseTool - -from app.agents.multi_agent_chat.core.agents import build_domain_agent - - -def build_luma_domain_agent(llm: BaseChatModel, tools: Sequence[BaseTool]): - """Compiled Luma domain-agent graph.""" - return build_domain_agent( - llm, - tools, - prompt_package=luma_pkg.__name__, - prompt_stem="domain_prompt", - ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/luma/domain_prompt.md b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/luma/domain_prompt.md deleted file mode 100644 index a2b4b7391..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/luma/domain_prompt.md +++ /dev/null @@ -1,55 +0,0 @@ -You are the Luma operations sub-agent. -You receive delegated instructions from a supervisor agent and return structured results for supervisor synthesis. - - -Execute Luma event listing, reads, and creation accurately. - - - -- `list_luma_events` -- `read_luma_event` -- `create_luma_event` - - - -- Use only tools in ``. -- Resolve relative dates against runtime timestamp. -- If required event fields are missing, return `status=blocked` with `missing_fields`. -- Never invent event IDs/times or creation outcomes. - - - -- Do not perform non-Luma tasks. - - - -- Never claim event creation success without tool confirmation. - - - -- On tool failure, return `status=error` with concise recovery `next_step`. -- On missing required fields, return `status=blocked` with `missing_fields`. - - - -Return **only** one JSON object (no markdown/prose): -{ - "status": "success" | "partial" | "blocked" | "error", - "action_summary": string, - "evidence": { - "event_id": string | null, - "title": string | null, - "start_at": string (ISO 8601 with timezone) | null, - "matched_candidates": [ - { "event_id": string, "title": string | null, "start_at": string | null } - ] | null - }, - "next_step": string | null, - "missing_fields": string[] | null, - "assumptions": string[] | null -} -Rules: -- `status=success` -> `next_step=null`, `missing_fields=null`. -- `status=partial|blocked|error` -> `next_step` must be non-null. -- `status=blocked` due to missing required inputs -> `missing_fields` must be non-null. - diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/luma/slice_tools.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/luma/slice_tools.py deleted file mode 100644 index bf4efde00..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/luma/slice_tools.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Registry-backed Luma tools.""" - -from __future__ import annotations - -from typing import Any - -from langchain_core.tools import BaseTool - -from app.agents.multi_agent_chat.core.registry import build_registry_tools_for_category - - -def build_luma_tools(dependencies: dict[str, Any]) -> list[BaseTool]: - """Registry-backed tools for the ``luma`` category.""" - return build_registry_tools_for_category(dependencies, "luma") diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/notion/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/notion/__init__.py deleted file mode 100644 index 2e17a4749..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/notion/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Notion connector slice.""" - -from app.agents.multi_agent_chat.expert_agent.connectors.notion.agent import build_notion_domain_agent -from app.agents.multi_agent_chat.expert_agent.connectors.notion.slice_tools import ( - build_notion_tools, -) - -__all__ = [ - "build_notion_tools", - "build_notion_domain_agent", -] diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/notion/agent.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/notion/agent.py deleted file mode 100644 index 3dc971f41..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/notion/agent.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Notion domain agent graph.""" - -from __future__ import annotations - -from collections.abc import Sequence - -import app.agents.multi_agent_chat.expert_agent.connectors.notion as notion_pkg -from langchain_core.language_models import BaseChatModel -from langchain_core.tools import BaseTool - -from app.agents.multi_agent_chat.core.agents import build_domain_agent - - -def build_notion_domain_agent(llm: BaseChatModel, tools: Sequence[BaseTool]): - """Compiled Notion domain-agent graph.""" - return build_domain_agent( - llm, - tools, - prompt_package=notion_pkg.__name__, - prompt_stem="domain_prompt", - ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/notion/domain_prompt.md b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/notion/domain_prompt.md deleted file mode 100644 index a40e9f4d0..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/notion/domain_prompt.md +++ /dev/null @@ -1,56 +0,0 @@ -You are the Notion operations sub-agent. -You receive delegated instructions from a supervisor agent and return structured results for supervisor synthesis. - - -Execute Notion page operations accurately in the connected workspace. - - - -- `create_notion_page` -- `update_notion_page` -- `delete_notion_page` - - - -- Use only tools in ``. -- If target page context is unclear, do not ask the user directly; return `status=blocked` with candidate options and supervisor `next_step`. -- Never invent page IDs, titles, or mutation outcomes. - - - -- Do not perform non-Notion tasks. - - - -- Before update/delete, ensure the target page match is explicit. -- Never claim mutation success without tool confirmation. - - - -- On tool failure, return `status=error` with concise retry/recovery `next_step`. -- On ambiguous target, return `status=blocked` with candidate options. - - - -Return **only** one JSON object (no markdown/prose): -{ - "status": "success" | "partial" | "blocked" | "error", - "action_summary": string, - "evidence": { - "page_id": string | null, - "page_title": string | null, - "matched_candidates": [ - { "page_id": string, "page_title": string | null } - ] | null - }, - "next_step": string | null, - "missing_fields": string[] | null, - "assumptions": string[] | null -} - -Rules: -- `status=success` -> `next_step=null`, `missing_fields=null`. -- `status=partial|blocked|error` -> `next_step` must be non-null. -- `status=blocked` due to missing required inputs -> `missing_fields` must be non-null. -- On ambiguity, include candidate options in `evidence.matched_candidates`. - diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/notion/slice_tools.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/notion/slice_tools.py deleted file mode 100644 index 4fecd13a4..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/notion/slice_tools.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Registry-backed Notion tools.""" - -from __future__ import annotations - -from typing import Any - -from langchain_core.tools import BaseTool - -from app.agents.multi_agent_chat.core.registry import build_registry_tools_for_category - - -def build_notion_tools(dependencies: dict[str, Any]) -> list[BaseTool]: - """Registry-backed tools for the ``notion`` category.""" - return build_registry_tools_for_category(dependencies, "notion") diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/onedrive/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/onedrive/__init__.py deleted file mode 100644 index d350176de..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/onedrive/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Microsoft OneDrive connector slice.""" - -from app.agents.multi_agent_chat.expert_agent.connectors.onedrive.agent import build_onedrive_domain_agent -from app.agents.multi_agent_chat.expert_agent.connectors.onedrive.slice_tools import ( - build_onedrive_tools, -) - -__all__ = [ - "build_onedrive_tools", - "build_onedrive_domain_agent", -] diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/onedrive/agent.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/onedrive/agent.py deleted file mode 100644 index d97083232..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/onedrive/agent.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Microsoft OneDrive domain agent graph.""" - -from __future__ import annotations - -from collections.abc import Sequence - -import app.agents.multi_agent_chat.expert_agent.connectors.onedrive as onedrive_pkg -from langchain_core.language_models import BaseChatModel -from langchain_core.tools import BaseTool - -from app.agents.multi_agent_chat.core.agents import build_domain_agent - - -def build_onedrive_domain_agent(llm: BaseChatModel, tools: Sequence[BaseTool]): - """Compiled OneDrive domain-agent graph.""" - return build_domain_agent( - llm, - tools, - prompt_package=onedrive_pkg.__name__, - prompt_stem="domain_prompt", - ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/onedrive/domain_prompt.md b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/onedrive/domain_prompt.md deleted file mode 100644 index a2f3617ba..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/onedrive/domain_prompt.md +++ /dev/null @@ -1,52 +0,0 @@ -You are the Microsoft OneDrive operations sub-agent. -You receive delegated instructions from a supervisor agent and return structured results for supervisor synthesis. - - -Execute OneDrive file create/delete actions accurately in the connected account. - - - -- `create_onedrive_file` -- `delete_onedrive_file` - - - -- Use only tools in ``. -- Ensure file identity/path is explicit before mutate actions. -- If ambiguous, return `status=blocked` with candidate paths and supervisor next step. -- Never invent IDs/paths or mutation results. - - - -- Do not perform non-OneDrive tasks. - - - -- Never claim file mutation success without tool confirmation. - - - -- On tool failure, return `status=error` with concise recovery `next_step`. -- On ambiguous targets, return `status=blocked` with candidate paths. - - - -Return **only** one JSON object (no markdown/prose): -{ - "status": "success" | "partial" | "blocked" | "error", - "action_summary": string, - "evidence": { - "file_id": string | null, - "file_path": string | null, - "operation": "create" | "delete" | null, - "matched_candidates": string[] | null - }, - "next_step": string | null, - "missing_fields": string[] | null, - "assumptions": string[] | null -} -Rules: -- `status=success` -> `next_step=null`, `missing_fields=null`. -- `status=partial|blocked|error` -> `next_step` must be non-null. -- `status=blocked` due to missing required inputs -> `missing_fields` must be non-null. - diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/onedrive/slice_tools.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/onedrive/slice_tools.py deleted file mode 100644 index 572cc6e36..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/onedrive/slice_tools.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Registry-backed Microsoft OneDrive tools.""" - -from __future__ import annotations - -from typing import Any - -from langchain_core.tools import BaseTool - -from app.agents.multi_agent_chat.core.registry import build_registry_tools_for_category - - -def build_onedrive_tools(dependencies: dict[str, Any]) -> list[BaseTool]: - """Registry-backed tools for the ``onedrive`` category.""" - return build_registry_tools_for_category(dependencies, "onedrive") diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/teams/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/teams/__init__.py deleted file mode 100644 index b9ab5a862..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/teams/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Microsoft Teams vertical slice: registry tools, domain agent, ``domain_prompt.md``.""" - -from app.agents.multi_agent_chat.expert_agent.connectors.teams.agent import build_teams_domain_agent -from app.agents.multi_agent_chat.expert_agent.connectors.teams.slice_tools import build_teams_tools - -__all__ = [ - "build_teams_tools", - "build_teams_domain_agent", -] diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/teams/agent.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/teams/agent.py deleted file mode 100644 index d8c55e462..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/teams/agent.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Microsoft Teams domain agent graph.""" - -from __future__ import annotations - -from collections.abc import Sequence - -import app.agents.multi_agent_chat.expert_agent.connectors.teams as teams_pkg -from langchain_core.language_models import BaseChatModel -from langchain_core.tools import BaseTool - -from app.agents.multi_agent_chat.core.agents import build_domain_agent - - -def build_teams_domain_agent(llm: BaseChatModel, tools: Sequence[BaseTool]): - """Compiled Microsoft Teams domain-agent graph.""" - return build_domain_agent( - llm, - tools, - prompt_package=teams_pkg.__name__, - prompt_stem="domain_prompt", - ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/teams/domain_prompt.md b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/teams/domain_prompt.md deleted file mode 100644 index 8c0eebdd1..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/teams/domain_prompt.md +++ /dev/null @@ -1,55 +0,0 @@ -You are the Microsoft Teams operations sub-agent. -You receive delegated instructions from a supervisor agent and return structured results for supervisor synthesis. - - -Execute Teams channel discovery, message reads, and sends accurately. - - - -- `list_teams_channels` -- `read_teams_messages` -- `send_teams_message` - - - -- Use only tools in ``. -- Resolve team/channel targets before read/send operations. -- If ambiguous, return `status=blocked` with candidate channels and `next_step`. -- Never invent message content, sender identity, timestamps, or delivery outcomes. - - - -- Do not perform non-Teams tasks. - - - -- Never claim send success without tool confirmation. - - - -- On tool failure, return `status=error` with concise recovery `next_step`. -- On unresolved destination ambiguity, return `status=blocked` with candidates. - - - -Return **only** one JSON object (no markdown/prose): -{ - "status": "success" | "partial" | "blocked" | "error", - "action_summary": string, - "evidence": { - "team_id": string | null, - "channel_id": string | null, - "message_id": string | null, - "matched_candidates": [ - { "team_id": string | null, "channel_id": string, "label": string | null } - ] | null - }, - "next_step": string | null, - "missing_fields": string[] | null, - "assumptions": string[] | null -} -Rules: -- `status=success` -> `next_step=null`, `missing_fields=null`. -- `status=partial|blocked|error` -> `next_step` must be non-null. -- `status=blocked` due to missing required inputs -> `missing_fields` must be non-null. - diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/teams/slice_tools.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/teams/slice_tools.py deleted file mode 100644 index e66ed3295..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/connectors/teams/slice_tools.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Registry-backed Microsoft Teams tools.""" - -from __future__ import annotations - -from typing import Any - -from langchain_core.tools import BaseTool - -from app.agents.multi_agent_chat.core.registry import build_registry_tools_for_category - - -def build_teams_tools(dependencies: dict[str, Any]) -> list[BaseTool]: - """Registry-backed tools for the ``teams`` category.""" - return build_registry_tools_for_category(dependencies, "teams") diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/mcp_bridge/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/mcp_bridge/__init__.py deleted file mode 100644 index 2b03b4235..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/mcp_bridge/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Prompt-backed subgraphs for MCP OAuth integrations without a native tool registry slice.""" - -from app.agents.multi_agent_chat.expert_agent.mcp_bridge.agent import build_mcp_route_domain_agent - -__all__ = ["build_mcp_route_domain_agent"] diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/mcp_bridge/agent.py b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/mcp_bridge/agent.py deleted file mode 100644 index be495488e..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/mcp_bridge/agent.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Domain agents for MCP-only OAuth integrations (no native registry slice).""" - -from __future__ import annotations - -from collections.abc import Sequence - -import app.agents.multi_agent_chat.expert_agent.mcp_bridge as mcp_bridge_pkg -from langchain_core.language_models import BaseChatModel -from langchain_core.tools import BaseTool - -from app.agents.multi_agent_chat.core.agents import build_domain_agent - - -def build_mcp_route_domain_agent( - llm: BaseChatModel, - route_key: str, - tools: Sequence[BaseTool], -): - """One subgraph per MCP-only route (``linear``, ``slack``, …); prompt stem ``{route_key}_domain``.""" - return build_domain_agent( - llm, - tools, - prompt_package=mcp_bridge_pkg.__name__, - prompt_stem=f"{route_key}_domain", - ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/mcp_bridge/airtable_domain.md b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/mcp_bridge/airtable_domain.md deleted file mode 100644 index 0f15f137f..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/mcp_bridge/airtable_domain.md +++ /dev/null @@ -1,46 +0,0 @@ -You are the Airtable MCP operations sub-agent. -You receive delegated instructions from a supervisor agent and return structured results for supervisor synthesis. - - -Execute Airtable MCP base/table/record operations accurately. - - - -- Runtime-provided Airtable MCP tools for bases, tables, and records. - - - -- Resolve base and table targets before record-level actions. -- Do not guess IDs or schema fields. -- If targets are ambiguous, return `status=blocked` with candidate options. -- Never claim mutation success without tool confirmation. - - - -- Do not execute non-Airtable tasks. - - - -- Never claim record mutations succeeded without tool confirmation. - - - -- On tool failure, return `status=error` with concise recovery `next_step`. -- On unresolved target/schema ambiguity, return `status=blocked` with required options. - - - -Return **only** one JSON object (no markdown/prose): -{ - "status": "success" | "partial" | "blocked" | "error", - "action_summary": string, - "evidence": { "items": object | null }, - "next_step": string | null, - "missing_fields": string[] | null, - "assumptions": string[] | null -} -Rules: -- `status=success` -> `next_step=null`, `missing_fields=null`. -- `status=partial|blocked|error` -> `next_step` must be non-null. -- `status=blocked` due to missing required inputs -> `missing_fields` must be non-null. - diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/mcp_bridge/clickup_domain.md b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/mcp_bridge/clickup_domain.md deleted file mode 100644 index 84014246d..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/mcp_bridge/clickup_domain.md +++ /dev/null @@ -1,45 +0,0 @@ -You are the ClickUp MCP operations sub-agent. -You receive delegated instructions from a supervisor agent and return structured results for supervisor synthesis. - - -Execute ClickUp MCP operations accurately using only runtime-provided tools. - - - -- Runtime-provided ClickUp MCP tools for task/workspace search and mutation. - - - -- Follow tool descriptions exactly. -- If task/workspace target is ambiguous or missing, return `status=blocked` with required disambiguation fields. -- Never claim mutation success without tool confirmation. - - - -- Do not execute non-ClickUp tasks. - - - -- Never claim update/create success without tool confirmation. - - - -- On tool failure, return `status=error` with concise recovery `next_step`. -- On unresolved ambiguity, return `status=blocked` with candidate options. - - - -Return **only** one JSON object (no markdown/prose): -{ - "status": "success" | "partial" | "blocked" | "error", - "action_summary": string, - "evidence": { "items": object | null }, - "next_step": string | null, - "missing_fields": string[] | null, - "assumptions": string[] | null -} -Rules: -- `status=success` -> `next_step=null`, `missing_fields=null`. -- `status=partial|blocked|error` -> `next_step` must be non-null. -- `status=blocked` due to missing required inputs -> `missing_fields` must be non-null. - diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/mcp_bridge/generic_mcp_domain.md b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/mcp_bridge/generic_mcp_domain.md deleted file mode 100644 index d2d5a2f1f..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/mcp_bridge/generic_mcp_domain.md +++ /dev/null @@ -1,46 +0,0 @@ -You are the generic MCP operations sub-agent for user-defined servers. -You receive delegated instructions from a supervisor agent and return structured results for supervisor synthesis. - - -Execute tasks strictly through runtime-exposed MCP tools while respecting tool contracts. - - - -- Runtime-provided MCP tools exposed by the connected custom server. - - - -- Follow each tool description and argument contract exactly. -- Never assume a capability exists unless a tool explicitly provides it. -- If required inputs are missing, return `status=blocked` with `missing_fields`. -- Never claim success without tool output confirmation. - - - -- Do not claim capabilities that are not present in runtime-exposed tools. - - - -- Never perform destructive operations without explicit delegated instruction and successful tool confirmation. - - - -- On tool failure, return `status=error` with concise recovery `next_step`. -- On missing required inputs, return `status=blocked` with `missing_fields`. - - - -Return **only** one JSON object (no markdown/prose): -{ - "status": "success" | "partial" | "blocked" | "error", - "action_summary": string, - "evidence": { "items": object | null }, - "next_step": string | null, - "missing_fields": string[] | null, - "assumptions": string[] | null -} -Rules: -- `status=success` -> `next_step=null`, `missing_fields=null`. -- `status=partial|blocked|error` -> `next_step` must be non-null. -- `status=blocked` due to missing required inputs -> `missing_fields` must be non-null. - diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/mcp_bridge/jira_domain.md b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/mcp_bridge/jira_domain.md deleted file mode 100644 index 4f4ae8a66..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/mcp_bridge/jira_domain.md +++ /dev/null @@ -1,46 +0,0 @@ -You are the Jira MCP operations sub-agent. -You receive delegated instructions from a supervisor agent and return structured results for supervisor synthesis. - - -Execute Jira MCP operations accurately, including discovery and issue mutation flows. - - - -- Runtime-provided Jira MCP tools for site/project discovery, issue search, create, and update. - - - -- Respect discovery dependencies (site/project/issue-type) before mutate calls. -- If required fields are missing or targets are ambiguous, return `status=blocked` with `missing_fields`. -- Do not guess keys/IDs. -- Never claim create/update success without tool confirmation. - - - -- Do not execute non-Jira tasks. - - - -- Never perform destructive/mutating actions without explicit target resolution. - - - -- On tool failure, return `status=error` with concise recovery `next_step`. -- On unresolved ambiguity, return `status=blocked` with candidates or missing fields. - - - -Return **only** one JSON object (no markdown/prose): -{ - "status": "success" | "partial" | "blocked" | "error", - "action_summary": string, - "evidence": { "items": object | null }, - "next_step": string | null, - "missing_fields": string[] | null, - "assumptions": string[] | null -} -Rules: -- `status=success` -> `next_step=null`, `missing_fields=null`. -- `status=partial|blocked|error` -> `next_step` must be non-null. -- `status=blocked` due to missing required inputs -> `missing_fields` must be non-null. - diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/mcp_bridge/linear_domain.md b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/mcp_bridge/linear_domain.md deleted file mode 100644 index ce91cc49f..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/mcp_bridge/linear_domain.md +++ /dev/null @@ -1,45 +0,0 @@ -You are the Linear MCP operations sub-agent. -You receive delegated instructions from a supervisor agent and return structured results for supervisor synthesis. - - -Execute Linear MCP operations accurately using only available runtime tools. - - - -- Runtime-provided Linear MCP tools for issues/projects/teams/workflows. - - - -- Follow tool descriptions exactly; do not assume unsupported endpoints. -- If required identifiers or context are missing, return `status=blocked` with `missing_fields` and supervisor `next_step`. -- Never invent IDs, statuses, or mutation outcomes. - - - -- Do not execute non-Linear tasks. - - - -- Never claim mutation success without tool confirmation. - - - -- On tool failure, return `status=error` with concise recovery `next_step`. -- On unresolved ambiguity, return `status=blocked` with candidates. - - - -Return **only** one JSON object (no markdown/prose): -{ - "status": "success" | "partial" | "blocked" | "error", - "action_summary": string, - "evidence": { "items": object | null }, - "next_step": string | null, - "missing_fields": string[] | null, - "assumptions": string[] | null -} -Rules: -- `status=success` -> `next_step=null`, `missing_fields=null`. -- `status=partial|blocked|error` -> `next_step` must be non-null. -- `status=blocked` due to missing required inputs -> `missing_fields` must be non-null. - diff --git a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/mcp_bridge/slack_domain.md b/surfsense_backend/app/agents/multi_agent_chat/expert_agent/mcp_bridge/slack_domain.md deleted file mode 100644 index 009a3205c..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/expert_agent/mcp_bridge/slack_domain.md +++ /dev/null @@ -1,45 +0,0 @@ -You are the Slack MCP operations sub-agent. -You receive delegated instructions from a supervisor agent and return structured results for supervisor synthesis. - - -Execute Slack MCP reads/actions accurately in the connected workspace. - - - -- Runtime-provided Slack MCP tools for search, channel/thread reads, and related actions. - - - -- Use only runtime-provided MCP tools and their documented arguments. -- If channel/thread target is ambiguous, return `status=blocked` with candidate options. -- Never invent message content, sender identity, timestamps, or delivery outcomes. - - - -- Do not execute non-Slack tasks. - - - -- Never claim send/read success without tool evidence. - - - -- On tool failure, return `status=error` with concise recovery `next_step`. -- On unresolved channel/thread ambiguity, return `status=blocked` with candidates. - - - -Return **only** one JSON object (no markdown/prose): -{ - "status": "success" | "partial" | "blocked" | "error", - "action_summary": string, - "evidence": { "items": object | null }, - "next_step": string | null, - "missing_fields": string[] | null, - "assumptions": string[] | null -} -Rules: -- `status=success` -> `next_step=null`, `missing_fields=null`. -- `status=partial|blocked|error` -> `next_step` must be non-null. -- `status=blocked` due to missing required inputs -> `missing_fields` must be non-null. - diff --git a/surfsense_backend/app/agents/multi_agent_chat/integration/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/integration/__init__.py deleted file mode 100644 index f73a554ef..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/integration/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Full-stack wiring (DB-scoped) on top of :mod:`routing` and :mod:`supervisor`.""" - -from app.agents.multi_agent_chat.integration.create_multi_agent_chat import ( - create_multi_agent_chat, -) - -__all__ = ["create_multi_agent_chat"] diff --git a/surfsense_backend/app/agents/multi_agent_chat/integration/create_multi_agent_chat.py b/surfsense_backend/app/agents/multi_agent_chat/integration/create_multi_agent_chat.py deleted file mode 100644 index 36c731735..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/integration/create_multi_agent_chat.py +++ /dev/null @@ -1,256 +0,0 @@ -"""Build the multi-agent supervisor graph: MCP partition, registry, routing tools, optional SurfSense middleware.""" - -from __future__ import annotations - -import asyncio -import logging -from typing import Any - -from langchain_core.language_models import BaseChatModel -from langchain_core.tools import BaseTool -from langgraph.types import Checkpointer -from sqlalchemy.ext.asyncio import AsyncSession - -from app.agents.multi_agent_chat.core.mcp_partition import ( - fetch_mcp_connector_metadata_maps, - partition_mcp_tools_by_expert_route, -) -from app.agents.multi_agent_chat.core.registry.dependencies import ( - build_registry_dependencies, - coerce_thread_id_for_registry, -) -from app.agents.multi_agent_chat.middleware.supervisor_stack import ( - build_supervisor_middleware_stack, -) -from app.agents.multi_agent_chat.routing.supervisor_routing import ( - build_supervisor_routing_tools, -) -from app.agents.multi_agent_chat.supervisor import build_supervisor_agent -from app.agents.new_chat.chat_deepagent import _map_connectors_to_searchable_types -from app.agents.new_chat.context import SurfSenseContextSchema -from app.agents.new_chat.feature_flags import get_flags -from app.agents.new_chat.filesystem_backends import build_backend_resolver -from app.agents.new_chat.filesystem_selection import FilesystemSelection -from app.agents.new_chat.tools.mcp_tool import load_mcp_tools -from app.db import ChatVisibility - -logger = logging.getLogger(__name__) - - -async def _discover_connectors_and_doc_types( - *, - connector_service: Any | None, - search_space_id: int, - available_connectors: list[str] | None, - available_document_types: list[str] | None, -) -> tuple[list[str] | None, list[str] | None]: - """Fill connector / document-type lists from ``connector_service`` when callers omit them.""" - connectors = available_connectors - doc_types = available_document_types - if connector_service is None: - return connectors, doc_types - try: - if connectors is None: - raw = await connector_service.get_available_connectors(search_space_id) - if raw: - connectors = _map_connectors_to_searchable_types(raw) - if doc_types is None: - doc_types = await connector_service.get_available_document_types(search_space_id) - except Exception as exc: - logger.warning("Failed to discover available connectors/document types: %s", exc) - return connectors, doc_types - - -async def _mcp_tools_by_expert_route( - *, - db_session: AsyncSession, - search_space_id: int, -) -> dict[str, list[BaseTool]] | None: - mcp_flat = await load_mcp_tools(db_session, search_space_id) - id_map, name_map = await fetch_mcp_connector_metadata_maps(db_session, search_space_id) - return partition_mcp_tools_by_expert_route(mcp_flat, id_map, name_map) - - -def _make_supervisor_routing_tools( - llm: BaseChatModel, - *, - db_session: AsyncSession, - search_space_id: int, - user_id: str, - thread_id: str | int | None, - firecrawl_api_key: str | None, - connector_service: Any | None, - available_connectors: list[str] | None, - available_document_types: list[str] | None, - thread_visibility: ChatVisibility, - mcp_tools_by_route: dict[str, list[BaseTool]] | None, -) -> list[BaseTool]: - registry_dependencies = build_registry_dependencies( - db_session=db_session, - search_space_id=search_space_id, - user_id=user_id, - thread_id=thread_id, - llm=llm, - firecrawl_api_key=firecrawl_api_key, - connector_service=connector_service, - available_connectors=available_connectors, - available_document_types=available_document_types, - thread_visibility=thread_visibility, - ) - return build_supervisor_routing_tools( - llm, - registry_dependencies=registry_dependencies, - include_deliverables=coerce_thread_id_for_registry(thread_id) is not None, - mcp_tools_by_route=mcp_tools_by_route, - available_connectors=available_connectors, - thread_visibility=thread_visibility, - ) - - -def _compile_supervisor_agent_sync( - *, - llm: BaseChatModel, - routing_tools: list[BaseTool], - checkpointer: Checkpointer | None, - backend_resolver: Any, - filesystem_mode: Any, - search_space_id: int, - user_id: str, - thread_id: str | int | None, - thread_visibility: ChatVisibility, - anon_session_id: str | None, - available_connectors: list[str] | None, - available_document_types: list[str] | None, - mentioned_document_ids: list[int] | None, - max_input_tokens: int | None, - citations_enabled: bool, -) -> Any: - """CPU-heavy: middleware stack + ``create_agent`` (intended for ``asyncio.to_thread``).""" - middleware = build_supervisor_middleware_stack( - llm=llm, - tools=routing_tools, - backend_resolver=backend_resolver, - filesystem_mode=filesystem_mode, - search_space_id=search_space_id, - user_id=user_id, - thread_id=thread_id, - visibility=thread_visibility, - anon_session_id=anon_session_id, - available_connectors=available_connectors, - available_document_types=available_document_types, - mentioned_document_ids=mentioned_document_ids, - max_input_tokens=max_input_tokens, - flags=get_flags(), - ) - return build_supervisor_agent( - llm, - tools=routing_tools, - checkpointer=checkpointer, - thread_visibility=thread_visibility, - middleware=middleware, - context_schema=SurfSenseContextSchema, - citations_enabled=citations_enabled, - ) - - -async def create_multi_agent_chat( - llm: BaseChatModel, - *, - db_session: AsyncSession, - search_space_id: int, - user_id: str, - checkpointer: Checkpointer | None = None, - thread_id: str | int | None = None, - firecrawl_api_key: str | None = None, - connector_service: Any | None = None, - available_connectors: list[str] | None = None, - available_document_types: list[str] | None = None, - thread_visibility: ChatVisibility = ChatVisibility.PRIVATE, - include_mcp_tools: bool = True, - filesystem_selection: FilesystemSelection | None = None, - anon_session_id: str | None = None, - mentioned_document_ids: list[int] | None = None, - max_input_tokens: int | None = None, - surfsense_stack: bool = True, - citations_enabled: bool | None = None, -): - """Build the full multi-agent chat graph (supervisor + expert subgraphs via routing tools). - - **Builtins** (:mod:`expert_agent.builtins`): registry-grouped **categories** (research, memory, deliverables). - **Connectors** (:mod:`expert_agent.connectors`): **vendor integrations** — one subgraph per route in - ``TOOL_NAMES_BY_CATEGORY`` (e.g. calendar, confluence, discord, dropbox, gmail, google_drive, luma, notion, onedrive, teams). - - MCP tools (via ``load_mcp_tools``) are partitioned inside this package and attached only - to the matching expert subgraphs — not to the supervisor tool list as raw MCP calls. Inclusion matches - ``app.agents.new_chat.tools.registry.build_tools_async``: all tools returned by ``load_mcp_tools`` are merged - after partitioning (no extra inventory filter on MCP). Connector routing uses ``available_connectors``: - pass explicitly, or provide ``connector_service`` so lists are resolved like - ``create_surfsense_deep_agent`` (``get_available_connectors`` → searchable types). - - Deliverables (thread-scoped reports, podcasts, etc.) are registered only when ``thread_id`` is set. - - When ``surfsense_stack`` is true (default), the supervisor uses the same SurfSense middleware shell as - the main single-agent chat (KB priority/tree, filesystem, compaction, permissions, etc.) except - ``SubAgentMiddleware`` / ``task``, since experts are separate graphs behind routing tools. Graph - compilation runs in ``asyncio.to_thread`` so heavy CPU work does not block the event loop. - - ``citations_enabled``: when ``None``, defaults to ``True`` (same default as ``AgentConfig`` / main chat). - """ - citations = True if citations_enabled is None else citations_enabled - connectors, doc_types = await _discover_connectors_and_doc_types( - connector_service=connector_service, - search_space_id=search_space_id, - available_connectors=available_connectors, - available_document_types=available_document_types, - ) - - mcp_by_route: dict[str, list[BaseTool]] | None = None - if include_mcp_tools: - mcp_by_route = await _mcp_tools_by_expert_route( - db_session=db_session, search_space_id=search_space_id - ) - - routing_tools = _make_supervisor_routing_tools( - llm, - db_session=db_session, - search_space_id=search_space_id, - user_id=user_id, - thread_id=thread_id, - firecrawl_api_key=firecrawl_api_key, - connector_service=connector_service, - available_connectors=connectors, - available_document_types=doc_types, - thread_visibility=thread_visibility, - mcp_tools_by_route=mcp_by_route, - ) - - fs_sel = filesystem_selection or FilesystemSelection() - backend_resolver = build_backend_resolver(fs_sel, search_space_id=search_space_id) - - if not surfsense_stack: - return build_supervisor_agent( - llm, - tools=routing_tools, - checkpointer=checkpointer, - thread_visibility=thread_visibility, - citations_enabled=citations, - ) - - return await asyncio.to_thread( - _compile_supervisor_agent_sync, - llm=llm, - routing_tools=routing_tools, - checkpointer=checkpointer, - backend_resolver=backend_resolver, - filesystem_mode=fs_sel.mode, - search_space_id=search_space_id, - user_id=user_id, - thread_id=thread_id, - thread_visibility=thread_visibility, - anon_session_id=anon_session_id, - available_connectors=connectors, - available_document_types=doc_types, - mentioned_document_ids=mentioned_document_ids, - max_input_tokens=max_input_tokens, - citations_enabled=citations, - ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/middleware/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/middleware/__init__.py deleted file mode 100644 index 058cf705a..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/middleware/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -"""SurfSense supervisor middleware (parity with the main single-agent chat, minus subagents).""" - -from app.agents.multi_agent_chat.middleware.supervisor_stack import ( - build_supervisor_middleware_stack, - parse_thread_id_for_action_log, -) - -__all__ = [ - "build_supervisor_middleware_stack", - "parse_thread_id_for_action_log", -] diff --git a/surfsense_backend/app/agents/multi_agent_chat/middleware/supervisor_stack.py b/surfsense_backend/app/agents/multi_agent_chat/middleware/supervisor_stack.py deleted file mode 100644 index 0cd390949..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/middleware/supervisor_stack.py +++ /dev/null @@ -1,363 +0,0 @@ -"""Supervisor middleware stack matching the main single-agent chat (no ``SubAgentMiddleware`` / ``task``).""" - -from __future__ import annotations - -import logging -from collections.abc import Sequence -from typing import Any - -from deepagents.backends import StateBackend -from deepagents.middleware.patch_tool_calls import PatchToolCallsMiddleware -from deepagents.middleware.skills import SkillsMiddleware -from langchain.agents.middleware import ( - LLMToolSelectorMiddleware, - ModelCallLimitMiddleware, - ModelFallbackMiddleware, - TodoListMiddleware, - ToolCallLimitMiddleware, -) -from langchain_anthropic.middleware import AnthropicPromptCachingMiddleware -from langchain_core.language_models import BaseChatModel -from langchain_core.tools import BaseTool - -from app.agents.new_chat.feature_flags import AgentFeatureFlags, get_flags -from app.agents.new_chat.filesystem_selection import FilesystemMode -from app.agents.new_chat.middleware import ( - ActionLogMiddleware, - AnonymousDocumentMiddleware, - BusyMutexMiddleware, - ClearToolUsesEdit, - DedupHITLToolCallsMiddleware, - DoomLoopMiddleware, - FileIntentMiddleware, - KnowledgeBasePersistenceMiddleware, - KnowledgePriorityMiddleware, - KnowledgeTreeMiddleware, - MemoryInjectionMiddleware, - NoopInjectionMiddleware, - OtelSpanMiddleware, - RetryAfterMiddleware, - SpillingContextEditingMiddleware, - SpillToBackendEdit, - SurfSenseFilesystemMiddleware, - ToolCallNameRepairMiddleware, - build_skills_backend_factory, - create_surfsense_compaction_middleware, - default_skills_sources, -) -from app.agents.new_chat.plugin_loader import ( - PluginContext, - load_allowed_plugin_names_from_env, - load_plugin_middlewares, -) -from app.agents.new_chat.tools.registry import BUILTIN_TOOLS -from app.db import ChatVisibility - -logger = logging.getLogger(__name__) - -# Routing tools with heavy outputs — never prune via context editing when bound. -_SUPERVISOR_PRUNE_PROTECTED: frozenset[str] = frozenset( - { - "deliverables", - "invalid", - # Align with single-agent surfacing of costly connector reads if names overlap later. - "read_email", - "search_emails", - "generate_report", - "generate_resume", - "generate_podcast", - "generate_video_presentation", - "generate_image", - } -) - - -def _safe_exclude_tools_supervisor(tools: Sequence[BaseTool]) -> tuple[str, ...]: - enabled = {t.name for t in tools} - return tuple(n for n in _SUPERVISOR_PRUNE_PROTECTED if n in enabled) - - -def parse_thread_id_for_action_log(thread_id: int | str | None) -> int | None: - """Numeric DB thread ids only — UUID strings skip action logging (no FK row).""" - if thread_id is None: - return None - if isinstance(thread_id, int): - return thread_id - s = str(thread_id).strip() - if s.isdigit(): - return int(s) - return None - - -def build_supervisor_middleware_stack( - *, - llm: BaseChatModel, - tools: Sequence[BaseTool], - backend_resolver: Any, - filesystem_mode: FilesystemMode, - search_space_id: int, - user_id: str | None, - thread_id: int | str | None, - visibility: ChatVisibility, - anon_session_id: str | None, - available_connectors: list[str] | None, - available_document_types: list[str] | None, - mentioned_document_ids: list[int] | None, - max_input_tokens: int | None, - flags: AgentFeatureFlags | None = None, -) -> list[Any]: - """Build middleware list for the multi-agent supervisor (parity with ``_build_compiled_agent_blocking`` minus subagents).""" - flags = flags or get_flags() - - memory_middleware = MemoryInjectionMiddleware( - user_id=user_id, - search_space_id=search_space_id, - thread_visibility=visibility, - ) - - summarization_mw = create_surfsense_compaction_middleware(llm, StateBackend) - _ = flags.enable_compaction_v2 - - context_edit_mw = None - if ( - flags.enable_context_editing - and not flags.disable_new_agent_stack - and max_input_tokens - ): - spill_edit = SpillToBackendEdit( - trigger=int(max_input_tokens * 0.55), - clear_at_least=int(max_input_tokens * 0.15), - keep=5, - exclude_tools=_safe_exclude_tools_supervisor(tools), - clear_tool_inputs=True, - ) - clear_edit = ClearToolUsesEdit( - trigger=int(max_input_tokens * 0.55), - clear_at_least=int(max_input_tokens * 0.15), - keep=5, - exclude_tools=_safe_exclude_tools_supervisor(tools), - clear_tool_inputs=True, - placeholder="[cleared - older tool output trimmed for context]", - ) - context_edit_mw = SpillingContextEditingMiddleware( - edits=[spill_edit, clear_edit], - backend_resolver=backend_resolver, - ) - - retry_mw = ( - RetryAfterMiddleware(max_retries=3) - if flags.enable_retry_after and not flags.disable_new_agent_stack - else None - ) - fallback_mw: ModelFallbackMiddleware | None = None - if flags.enable_model_fallback and not flags.disable_new_agent_stack: - try: - fallback_mw = ModelFallbackMiddleware( - "openai:gpt-4o-mini", - "anthropic:claude-3-5-haiku-20241022", - ) - except Exception: - logger.warning("ModelFallbackMiddleware init failed; skipping.") - fallback_mw = None - model_call_limit_mw = ( - ModelCallLimitMiddleware( - thread_limit=120, - run_limit=80, - exit_behavior="end", - ) - if flags.enable_model_call_limit and not flags.disable_new_agent_stack - else None - ) - tool_call_limit_mw = ( - ToolCallLimitMiddleware( - thread_limit=300, run_limit=80, exit_behavior="continue" - ) - if flags.enable_tool_call_limit and not flags.disable_new_agent_stack - else None - ) - - noop_mw = ( - NoopInjectionMiddleware() - if flags.enable_compaction_v2 and not flags.disable_new_agent_stack - else None - ) - - repair_mw = None - if flags.enable_tool_call_repair and not flags.disable_new_agent_stack: - registered_names: set[str] = {t.name for t in tools} - registered_names |= { - "write_todos", - "ls", - "read_file", - "write_file", - "edit_file", - "glob", - "grep", - "execute", - # No ``task`` — multi-agent uses routing tools instead of SubAgentMiddleware. - } - repair_mw = ToolCallNameRepairMiddleware( - registered_tool_names=registered_names, - fuzzy_match_threshold=None, - ) - - doom_loop_mw = ( - DoomLoopMiddleware(threshold=3) - if flags.enable_doom_loop and not flags.disable_new_agent_stack - else None - ) - - thread_id_action_log = parse_thread_id_for_action_log(thread_id) - action_log_mw: ActionLogMiddleware | None = None - if ( - flags.enable_action_log - and not flags.disable_new_agent_stack - and thread_id_action_log is not None - ): - try: - tool_defs_by_name = {td.name: td for td in BUILTIN_TOOLS} - action_log_mw = ActionLogMiddleware( - thread_id=thread_id_action_log, - search_space_id=search_space_id, - user_id=user_id, - tool_definitions=tool_defs_by_name, - ) - except Exception: # pragma: no cover - defensive - logger.warning( - "ActionLogMiddleware init failed; running without it.", - exc_info=True, - ) - action_log_mw = None - - busy_mutex_mw: BusyMutexMiddleware | None = ( - BusyMutexMiddleware() - if flags.enable_busy_mutex and not flags.disable_new_agent_stack - else None - ) - - otel_mw: OtelSpanMiddleware | None = ( - OtelSpanMiddleware() - if flags.enable_otel and not flags.disable_new_agent_stack - else None - ) - - plugin_middlewares: list[Any] = [] - if flags.enable_plugin_loader and not flags.disable_new_agent_stack: - try: - allowed_names = load_allowed_plugin_names_from_env() - if allowed_names: - plugin_middlewares = load_plugin_middlewares( - PluginContext.build( - search_space_id=search_space_id, - user_id=user_id, - thread_visibility=visibility, - llm=llm, - ), - allowed_plugin_names=allowed_names, - ) - except Exception: # pragma: no cover - defensive - logger.warning( - "Plugin loader failed; continuing without plugins.", - exc_info=True, - ) - plugin_middlewares = [] - - skills_mw: SkillsMiddleware | None = None - if flags.enable_skills and not flags.disable_new_agent_stack: - try: - skills_factory = build_skills_backend_factory( - search_space_id=search_space_id - if filesystem_mode == FilesystemMode.CLOUD - else None, - ) - skills_mw = SkillsMiddleware( - backend=skills_factory, - sources=default_skills_sources(), - ) - except Exception as exc: # pragma: no cover - defensive - logger.warning("SkillsMiddleware init failed; skipping: %s", exc) - skills_mw = None - - names = {t.name for t in tools} - selector_mw: LLMToolSelectorMiddleware | None = None - if ( - flags.enable_llm_tool_selector - and not flags.disable_new_agent_stack - and len(tools) > 30 - ): - try: - selector_mw = LLMToolSelectorMiddleware( - model="openai:gpt-4o-mini", - max_tools=12, - always_include=[ - n - for n in ( - "research", - "memory", - "update_memory", - "get_connected_accounts", - "scrape_webpage", - ) - if n in names - ], - ) - except Exception: - logger.warning("LLMToolSelectorMiddleware init failed; skipping.") - selector_mw = None - - deepagent_middleware = [ - busy_mutex_mw, - otel_mw, - TodoListMiddleware(), - memory_middleware, - AnonymousDocumentMiddleware(anon_session_id=anon_session_id) - if filesystem_mode == FilesystemMode.CLOUD - else None, - KnowledgeTreeMiddleware( - search_space_id=search_space_id, - filesystem_mode=filesystem_mode, - llm=llm, - ) - if filesystem_mode == FilesystemMode.CLOUD - else None, - KnowledgePriorityMiddleware( - llm=llm, - search_space_id=search_space_id, - filesystem_mode=filesystem_mode, - available_connectors=available_connectors, - available_document_types=available_document_types, - mentioned_document_ids=mentioned_document_ids, - ), - FileIntentMiddleware(llm=llm), - SurfSenseFilesystemMiddleware( - backend=backend_resolver, - filesystem_mode=filesystem_mode, - search_space_id=search_space_id, - created_by_id=user_id, - thread_id=thread_id, - ), - KnowledgeBasePersistenceMiddleware( - search_space_id=search_space_id, - created_by_id=user_id, - filesystem_mode=filesystem_mode, - ) - if filesystem_mode == FilesystemMode.CLOUD - else None, - skills_mw, - selector_mw, - model_call_limit_mw, - tool_call_limit_mw, - context_edit_mw, - summarization_mw, - noop_mw, - retry_mw, - fallback_mw, - repair_mw, - doom_loop_mw, - action_log_mw, - PatchToolCallsMiddleware(), - DedupHITLToolCallsMiddleware(agent_tools=list(tools)), - *plugin_middlewares, - AnthropicPromptCachingMiddleware(unsupported_model_behavior="ignore"), - ] - return [m for m in deepagent_middleware if m is not None] diff --git a/surfsense_backend/app/agents/multi_agent_chat/routing/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/routing/__init__.py deleted file mode 100644 index c369aeea5..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/routing/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Supervisor routing: domain-agent wrappers and composed routing tool lists.""" - -from app.agents.multi_agent_chat.routing.domain_routing_spec import DomainRoutingSpec -from app.agents.multi_agent_chat.routing.from_domain_agents import routing_tools_from_specs -from app.agents.multi_agent_chat.routing.supervisor_routing import build_supervisor_routing_tools -from app.agents.multi_agent_chat.core.invocation import extract_last_assistant_text - -__all__ = [ - "DomainRoutingSpec", - "build_supervisor_routing_tools", - "extract_last_assistant_text", - "routing_tools_from_specs", -] diff --git a/surfsense_backend/app/agents/multi_agent_chat/routing/domain_routing_spec.py b/surfsense_backend/app/agents/multi_agent_chat/routing/domain_routing_spec.py deleted file mode 100644 index f61d5b151..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/routing/domain_routing_spec.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Declarative description of one supervisor routing tool → domain agent.""" - -from __future__ import annotations - -from collections.abc import Callable -from dataclasses import dataclass -from typing import Any - - -@dataclass(frozen=True) -class DomainRoutingSpec: - """One supervisor-facing routing ``@tool`` bound to a compiled domain graph. - - ``curated_context`` is optional for **any** route: when set, the routing tool prepends its return - value into the child task via :func:`~app.agents.multi_agent_chat.core.delegation.compose_child_task`. - :func:`build_supervisor_routing_tools` does not pass it (all routes treated the same); use when building specs manually. - """ - - tool_name: str - description: str - domain_agent: Any - curated_context: Callable[[str], str | None] | None = None diff --git a/surfsense_backend/app/agents/multi_agent_chat/routing/from_domain_agents.py b/surfsense_backend/app/agents/multi_agent_chat/routing/from_domain_agents.py deleted file mode 100644 index a2c1513f4..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/routing/from_domain_agents.py +++ /dev/null @@ -1,123 +0,0 @@ -"""LangChain ``@tool`` wrappers that invoke compiled domain-agent graphs (supervisor-facing only).""" - -from __future__ import annotations - -from collections.abc import Sequence -import json -from typing import Any - -from langchain_core.tools import BaseTool, tool - -from app.agents.multi_agent_chat.core.delegation import compose_child_task -from app.agents.multi_agent_chat.core.invocation import extract_last_assistant_text -from app.agents.multi_agent_chat.routing.domain_routing_spec import DomainRoutingSpec - -_ALLOWED_STATUSES = {"success", "partial", "blocked", "error"} -_REQUIRED_KEYS = { - "status", - "action_summary", - "evidence", - "next_step", - "missing_fields", - "assumptions", -} - - -def _fallback_payload(spec: DomainRoutingSpec, reason: str, raw_text: str) -> dict[str, Any]: - preview = raw_text[:800] - return { - "status": "error", - "action_summary": "Domain agent output failed JSON contract validation.", - "evidence": { - "route_tool": spec.tool_name, - "validation_error": reason, - "raw_output_preview": preview, - }, - "next_step": ( - "Re-delegate with a strict reminder to return exactly one JSON object " - "matching the output_contract." - ), - "missing_fields": None, - "assumptions": None, - } - - -def _validate_contract_payload(payload: dict[str, Any]) -> str | None: - missing = sorted(_REQUIRED_KEYS - set(payload)) - if missing: - return f"missing required keys: {', '.join(missing)}" - - status = payload.get("status") - if status not in _ALLOWED_STATUSES: - return "invalid status value" - - action_summary = payload.get("action_summary") - if not isinstance(action_summary, str) or not action_summary.strip(): - return "action_summary must be a non-empty string" - - evidence = payload.get("evidence") - if not isinstance(evidence, dict): - return "evidence must be an object" - - next_step = payload.get("next_step") - if status == "success": - if next_step is not None: - return "next_step must be null when status=success" - if payload.get("missing_fields") is not None: - return "missing_fields must be null when status=success" - else: - if not isinstance(next_step, str) or not next_step.strip(): - return "next_step must be a non-empty string for non-success statuses" - - missing_fields = payload.get("missing_fields") - if missing_fields is not None: - if not isinstance(missing_fields, list) or any( - not isinstance(item, str) or not item.strip() for item in missing_fields - ): - return "missing_fields must be null or a list of non-empty strings" - - assumptions = payload.get("assumptions") - if assumptions is not None: - if not isinstance(assumptions, list) or any( - not isinstance(item, str) or not item.strip() for item in assumptions - ): - return "assumptions must be null or a list of non-empty strings" - - return None - - -def _normalize_domain_output(spec: DomainRoutingSpec, raw_text: str) -> str: - try: - parsed = json.loads(raw_text) - except json.JSONDecodeError as exc: - fallback = _fallback_payload(spec, f"invalid JSON: {exc.msg}", raw_text) - return json.dumps(fallback) - - if not isinstance(parsed, dict): - fallback = _fallback_payload(spec, "top-level JSON must be an object", raw_text) - return json.dumps(fallback) - - validation_error = _validate_contract_payload(parsed) - if validation_error: - fallback = _fallback_payload(spec, validation_error, raw_text) - return json.dumps(fallback) - - return json.dumps(parsed) - - -def _routing_tool_for_spec(spec: DomainRoutingSpec) -> BaseTool: - @tool(spec.tool_name, description=spec.description) - async def _route(task: str) -> str: - curated = spec.curated_context(task) if spec.curated_context else None - content = compose_child_task(task, curated_context=curated) - result = await spec.domain_agent.ainvoke( - {"messages": [{"role": "user", "content": content}]}, - ) - return _normalize_domain_output(spec, extract_last_assistant_text(result)) - - return _route - - -def routing_tools_from_specs(specs: Sequence[DomainRoutingSpec]) -> list[BaseTool]: - """Build one supervisor-facing routing ``@tool`` per :class:`DomainRoutingSpec`.""" - return [_routing_tool_for_spec(spec) for spec in specs] diff --git a/surfsense_backend/app/agents/multi_agent_chat/routing/route_connector_gate.py b/surfsense_backend/app/agents/multi_agent_chat/routing/route_connector_gate.py deleted file mode 100644 index 84e2359e7..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/routing/route_connector_gate.py +++ /dev/null @@ -1,57 +0,0 @@ -"""Gate supervisor routing tools by connected searchable connector types (aligned with main chat KB). - -When ``available_connectors`` is ``None``, all routes are emitted (caller did not pass an inventory). - -When provided, a connector route is emitted only if at least one required searchable type is present. -MCP tools are filtered upstream in :func:`~app.agents.multi_agent_chat.core.mcp_partition.partition_mcp_tools_by_expert_route` -so merges only include tools for connected accounts. -""" - -from __future__ import annotations - -# Route tool_name → searchable connector / doc-type strings (same family as -# ``chat_deepagent._CONNECTOR_TYPE_TO_SEARCHABLE`` values in ``available_connectors``). -_ROUTE_REQUIRES_ANY: dict[str, frozenset[str]] = { - "calendar": frozenset( - {"GOOGLE_CALENDAR_CONNECTOR", "COMPOSIO_GOOGLE_CALENDAR_CONNECTOR"} - ), - "confluence": frozenset({"CONFLUENCE_CONNECTOR"}), - "discord": frozenset({"DISCORD_CONNECTOR"}), - "dropbox": frozenset({"DROPBOX_FILE"}), - "gmail": frozenset({"GOOGLE_GMAIL_CONNECTOR", "COMPOSIO_GMAIL_CONNECTOR"}), - "google_drive": frozenset( - {"GOOGLE_DRIVE_FILE", "COMPOSIO_GOOGLE_DRIVE_CONNECTOR"} - ), - "luma": frozenset({"LUMA_CONNECTOR"}), - "notion": frozenset({"NOTION_CONNECTOR"}), - "onedrive": frozenset({"ONEDRIVE_FILE"}), - "teams": frozenset({"TEAMS_CONNECTOR"}), - # MCP-only supervisor routes (see ``core.mcp_partition.MCP_ONLY_ROUTE_KEYS_IN_ORDER``). - "linear": frozenset({"LINEAR_CONNECTOR"}), - "slack": frozenset({"SLACK_CONNECTOR"}), - "jira": frozenset({"JIRA_CONNECTOR"}), - "clickup": frozenset({"CLICKUP_CONNECTOR"}), - "airtable": frozenset({"AIRTABLE_CONNECTOR"}), - # generic_mcp route intentionally disabled for now. - # "generic_mcp": frozenset({"MCP_CONNECTOR"}), -} - - -def include_connector_route( - route_key: str, - available_connectors: list[str] | None, -) -> bool: - """Return whether to register this connector route on the supervisor. - - If ``available_connectors`` is omitted, preserve legacy behaviour (emit the route). - - Otherwise require at least one matching entry in ``available_connectors`` for connector-backed routes. - Builtin routes (research, memory, …) have no entry in ``_ROUTE_REQUIRES_ANY`` and are always included. - """ - if available_connectors is None: - return True - required = _ROUTE_REQUIRES_ANY.get(route_key) - if required is None: - return True - avail = set(available_connectors) - return bool(required & avail) diff --git a/surfsense_backend/app/agents/multi_agent_chat/routing/supervisor_routing.py b/surfsense_backend/app/agents/multi_agent_chat/routing/supervisor_routing.py deleted file mode 100644 index ab1f5cafc..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/routing/supervisor_routing.py +++ /dev/null @@ -1,327 +0,0 @@ -"""Compose domain agents + tool lists into supervisor routing tools (one ``@tool`` per category).""" - -from __future__ import annotations - -from typing import Any - -from langchain_core.language_models import BaseChatModel -from langchain_core.tools import BaseTool - -from app.agents.multi_agent_chat.core.mcp_partition import MCP_ONLY_ROUTE_KEYS_IN_ORDER -from app.agents.multi_agent_chat.expert_agent.builtins.deliverables import ( - build_deliverables_domain_agent, - build_deliverables_tools, -) -from app.agents.multi_agent_chat.expert_agent.builtins.memory import ( - build_memory_domain_agent, - build_memory_tools, -) -from app.agents.multi_agent_chat.expert_agent.builtins.research import ( - build_research_domain_agent, - build_research_tools, -) -from app.agents.multi_agent_chat.expert_agent.connectors.calendar import ( - build_calendar_domain_agent, - build_calendar_tools, -) -from app.agents.multi_agent_chat.expert_agent.connectors.confluence import ( - build_confluence_domain_agent, - build_confluence_tools, -) -from app.agents.multi_agent_chat.expert_agent.connectors.discord import ( - build_discord_domain_agent, - build_discord_tools, -) -from app.agents.multi_agent_chat.expert_agent.connectors.dropbox import ( - build_dropbox_domain_agent, - build_dropbox_tools, -) -from app.agents.multi_agent_chat.expert_agent.connectors.gmail import ( - build_gmail_domain_agent, - build_gmail_tools, -) -from app.agents.multi_agent_chat.expert_agent.connectors.google_drive import ( - build_google_drive_domain_agent, - build_google_drive_tools, -) -from app.agents.multi_agent_chat.expert_agent.connectors.luma import ( - build_luma_domain_agent, - build_luma_tools, -) -from app.agents.multi_agent_chat.expert_agent.connectors.notion import ( - build_notion_domain_agent, - build_notion_tools, -) -from app.agents.multi_agent_chat.expert_agent.connectors.onedrive import ( - build_onedrive_domain_agent, - build_onedrive_tools, -) -from app.agents.multi_agent_chat.expert_agent.connectors.teams import ( - build_teams_domain_agent, - build_teams_tools, -) -from app.agents.multi_agent_chat.expert_agent.mcp_bridge import ( - build_mcp_route_domain_agent, -) -from app.agents.multi_agent_chat.routing.domain_routing_spec import DomainRoutingSpec -from app.agents.multi_agent_chat.routing.from_domain_agents import ( - routing_tools_from_specs, -) -from app.agents.multi_agent_chat.routing.route_connector_gate import ( - include_connector_route, -) -from app.db import ChatVisibility - -_MCP_ONLY_ROUTE_DESCRIPTIONS: dict[str, str] = { - "linear": ( - "Use for Linear issue/project work: find/create issues, update status/assignees, review project progress, and inspect cycles." - ), - "slack": ( - "Use for Slack channel communication: read channel/thread history, summarize conversations, and post replies." - ), - "jira": ( - "Use for Jira issue/project workflows: search issues, inspect fields, update tickets, and move work through workflow states." - ), - "clickup": ( - "Use for ClickUp task management: find tasks/lists, update task fields, and track execution progress." - ), - "airtable": ( - "Use for Airtable structured data operations: locate bases/tables and create/read/update records." - ), - # generic_mcp intentionally disabled for now. - # "generic_mcp": ( - # "Use as a fallback for custom connected app tasks not covered by a named specialist. " - # "Do not use if another specialist clearly matches." - # ), -} - - -def _memory_route_description(thread_visibility: ChatVisibility | None) -> str: - if thread_visibility == ChatVisibility.SEARCH_SPACE: - return "Use for storing durable team memory: shared team preferences, conventions, and long-lived team facts." - return "Use for storing durable user memory: personal preferences, instructions, and long-lived user facts." - - -def build_supervisor_routing_tools( - llm: BaseChatModel, - *, - registry_dependencies: dict[str, Any] | None = None, - include_deliverables: bool = True, - mcp_tools_by_route: dict[str, list[BaseTool]] | None = None, - available_connectors: list[str] | None = None, - thread_visibility: ChatVisibility | None = None, -) -> list[BaseTool]: - """Build supervisor routing tools: builtins first, then connector experts (same pattern for all). - - Requires ``registry_dependencies`` to produce any routing tools; otherwise returns an empty list. - - Pass ``registry_dependencies`` from - :func:`app.agents.multi_agent_chat.core.registry.build_registry_dependencies` - for builtins (**research**, **memory**, **deliverables** when ``include_deliverables``) and every - registry-backed connector route. - - ``mcp_tools_by_route`` maps route keys to MCP tools merged into the matching expert subgraph. - - When ``available_connectors`` is set (searchable connector strings, same shape as the main chat agent), - a connector-backed route is registered only if its required searchable connector type is available. - """ - if registry_dependencies is None: - return routing_tools_from_specs([]) - - mcp = mcp_tools_by_route or {} - specs: list[DomainRoutingSpec] = [] - - research_tools = build_research_tools(registry_dependencies) - research_agent = build_research_domain_agent(llm, research_tools) - specs.append( - DomainRoutingSpec( - tool_name="research", - description=( - "Use for external research: find sources on the web, extract evidence, and answer documentation questions." - ), - domain_agent=research_agent, - ), - ) - - memory_tools = build_memory_tools(registry_dependencies) - memory_agent = build_memory_domain_agent( - llm, - memory_tools, - thread_visibility=thread_visibility, - ) - specs.append( - DomainRoutingSpec( - tool_name="memory", - description=_memory_route_description(thread_visibility), - domain_agent=memory_agent, - ), - ) - - if include_deliverables: - deliverables_tools = build_deliverables_tools(registry_dependencies) - deliverables_agent = build_deliverables_domain_agent(llm, deliverables_tools) - specs.append( - DomainRoutingSpec( - tool_name="deliverables", - description=( - "Use for deliverables and shareable artifacts: generated reports, podcasts, " - "video presentations, resumes, and images—not for routine lookups or single small edits elsewhere." - ), - domain_agent=deliverables_agent, - ), - ) - - # Connector experts (registry-backed + optional MCP merge): alphabetical by route key. - if include_connector_route("calendar", available_connectors): - calendar_agent = build_calendar_domain_agent( - llm, - build_calendar_tools(registry_dependencies) + mcp.get("calendar", []), - ) - specs.append( - DomainRoutingSpec( - tool_name="calendar", - description=( - "Use for calendar planning and scheduling: check availability, read event details, create events, and update events." - ), - domain_agent=calendar_agent, - ), - ) - - if include_connector_route("confluence", available_connectors): - confluence_tools = build_confluence_tools(registry_dependencies) - confluence_agent = build_confluence_domain_agent(llm, confluence_tools) - specs.append( - DomainRoutingSpec( - tool_name="confluence", - description=( - "Use for Confluence knowledge pages: search/read existing pages, create new pages, and update page content." - ), - domain_agent=confluence_agent, - ), - ) - - if include_connector_route("discord", available_connectors): - discord_tools = build_discord_tools(registry_dependencies) - discord_agent = build_discord_domain_agent(llm, discord_tools + mcp.get("discord", [])) - specs.append( - DomainRoutingSpec( - tool_name="discord", - description=( - "Use for Discord communication: read channel/thread messages, gather context, and send replies." - ), - domain_agent=discord_agent, - ), - ) - - if include_connector_route("dropbox", available_connectors): - dropbox_tools = build_dropbox_tools(registry_dependencies) - dropbox_agent = build_dropbox_domain_agent(llm, dropbox_tools) - specs.append( - DomainRoutingSpec( - tool_name="dropbox", - description=( - "Use for Dropbox file storage tasks: browse folders, read files, and manage Dropbox file content." - ), - domain_agent=dropbox_agent, - ), - ) - - if include_connector_route("gmail", available_connectors): - gmail_agent = build_gmail_domain_agent( - llm, - build_gmail_tools(registry_dependencies) + mcp.get("gmail", []), - ) - specs.append( - DomainRoutingSpec( - tool_name="gmail", - description=( - "Use for Gmail inbox actions: search/read emails, draft or update replies, send messages, and trash emails." - ), - domain_agent=gmail_agent, - ), - ) - - if include_connector_route("google_drive", available_connectors): - google_drive_tools = build_google_drive_tools(registry_dependencies) - google_drive_agent = build_google_drive_domain_agent(llm, google_drive_tools) - specs.append( - DomainRoutingSpec( - tool_name="google_drive", - description=( - "Use for Google Drive document/file tasks: locate files, inspect content, and manage Drive files or folders." - ), - domain_agent=google_drive_agent, - ), - ) - - if include_connector_route("luma", available_connectors): - luma_tools = build_luma_tools(registry_dependencies) - luma_agent = build_luma_domain_agent(llm, luma_tools + mcp.get("luma", [])) - specs.append( - DomainRoutingSpec( - tool_name="luma", - description=( - "Use for Luma event operations: list events, inspect event details, and create new events." - ), - domain_agent=luma_agent, - ), - ) - - if include_connector_route("notion", available_connectors): - notion_tools = build_notion_tools(registry_dependencies) - notion_agent = build_notion_domain_agent(llm, notion_tools) - specs.append( - DomainRoutingSpec( - tool_name="notion", - description=( - "Use for Notion workspace pages: create pages, update page content, and delete pages." - ), - domain_agent=notion_agent, - ), - ) - - if include_connector_route("onedrive", available_connectors): - onedrive_tools = build_onedrive_tools(registry_dependencies) - onedrive_agent = build_onedrive_domain_agent(llm, onedrive_tools) - specs.append( - DomainRoutingSpec( - tool_name="onedrive", - description=( - "Use for OneDrive file storage tasks: browse folders, read files, and manage OneDrive file content." - ), - domain_agent=onedrive_agent, - ), - ) - - if include_connector_route("teams", available_connectors): - teams_tools = build_teams_tools(registry_dependencies) - teams_agent = build_teams_domain_agent(llm, teams_tools + mcp.get("teams", [])) - specs.append( - DomainRoutingSpec( - tool_name="teams", - description=( - "Use for Microsoft Teams communication: read channel/thread messages, gather context, and post replies." - ), - domain_agent=teams_agent, - ), - ) - - for route_key in MCP_ONLY_ROUTE_KEYS_IN_ORDER: - only_mcp = mcp.get(route_key) or [] - if not only_mcp: - continue - if not include_connector_route(route_key, available_connectors): - continue - desc = _MCP_ONLY_ROUTE_DESCRIPTIONS.get( - route_key, - f"Use for {route_key} tasks related to that system's core work objects and workflows.", - ) - specs.append( - DomainRoutingSpec( - tool_name=route_key, - description=desc, - domain_agent=build_mcp_route_domain_agent(llm, route_key, only_mcp), - ), - ) - - return routing_tools_from_specs(specs) diff --git a/surfsense_backend/app/agents/multi_agent_chat/supervisor/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/supervisor/__init__.py deleted file mode 100644 index d96ee3e39..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/supervisor/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Supervisor agent graph only; supply routing ``tools`` from ``build_supervisor_routing_tools``.""" - -from app.agents.multi_agent_chat.supervisor.graph import build_supervisor_agent - -__all__ = ["build_supervisor_agent"] diff --git a/surfsense_backend/app/agents/multi_agent_chat/supervisor/graph.py b/surfsense_backend/app/agents/multi_agent_chat/supervisor/graph.py deleted file mode 100644 index 7823a0380..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/supervisor/graph.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Compile the supervisor agent graph (LangChain ``create_agent`` + caller routing tools).""" - -from __future__ import annotations - -from collections.abc import Sequence -from typing import Any - -from langchain.agents import create_agent -from langchain_core.language_models import BaseChatModel -from langchain_core.tools import BaseTool -from langgraph.types import Checkpointer - -from app.agents.multi_agent_chat.supervisor.prompt_assembly import ( - build_supervisor_system_prompt, -) - - -def build_supervisor_agent( - llm: BaseChatModel, - *, - tools: Sequence[BaseTool], - checkpointer: Checkpointer | None = None, - thread_visibility: Any | None = None, - middleware: Sequence[Any] | None = None, - context_schema: Any | None = None, - citations_enabled: bool = True, -): - """Compile the supervisor **agent** (graph). ``tools`` = output of ``build_supervisor_routing_tools``.""" - system_prompt = build_supervisor_system_prompt( - tools, - thread_visibility=thread_visibility, - citations_enabled=citations_enabled, - ) - kwargs: dict[str, Any] = { - "system_prompt": system_prompt, - "tools": list(tools), - "checkpointer": checkpointer, - } - if middleware is not None: - kwargs["middleware"] = list(middleware) - if context_schema is not None: - kwargs["context_schema"] = context_schema - agent = create_agent(llm, **kwargs) - if middleware is not None or context_schema is not None: - return agent.with_config( - { - "recursion_limit": 10_000, - "metadata": {"ls_integration": "multi_agent_supervisor"}, - } - ) - return agent diff --git a/surfsense_backend/app/agents/multi_agent_chat/supervisor/prompt_assembly.py b/surfsense_backend/app/agents/multi_agent_chat/supervisor/prompt_assembly.py deleted file mode 100644 index ac7140c7d..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/supervisor/prompt_assembly.py +++ /dev/null @@ -1,128 +0,0 @@ -"""Supervisor system prompt: template load, shared agent-identity injection, specialist list.""" - -from __future__ import annotations - -from collections.abc import Sequence -from datetime import UTC, datetime -from typing import Any - -from langchain_core.tools import BaseTool - -import app.agents.multi_agent_chat.supervisor as supervisor_pkg -from app.agents.multi_agent_chat.core.prompts import read_prompt_md -from app.agents.new_chat.prompts.composer import _build_citation_block, _read_fragment -from app.db import ChatVisibility - -_MEMORY_SPECIALIST_PHRASE = "invoke the **memory** specialist" - -_BUILTIN_SPECIALISTS: frozenset[str] = frozenset({"research", "memory", "deliverables"}) -_SPECIALIST_CAPABILITIES: dict[str, str] = { - "research": "external research: web lookup, source gathering, and SurfSense documentation help.", - "memory": "save durable long-lived memory items.", - "deliverables": "deliverables and shareable artifacts: reports, podcasts, video presentations, resumes, and images.", - "gmail": "email inbox actions: search/read emails, draft updates, send messages, and trash emails.", - "calendar": "scheduling actions: check availability, inspect events, create events, and update events.", - "google_drive": "Drive file/document actions: locate files, inspect content, and manage files/folders.", - "notion": "Notion page actions: create pages, update content, and delete pages.", - "confluence": "Confluence page actions: find/read pages and create/update pages.", - "dropbox": "Dropbox file storage actions: browse folders, read files, and manage file content.", - "onedrive": "OneDrive file storage actions: browse folders, read files, and manage file content.", - "discord": "Discord communication actions: read channels/threads and post replies.", - "teams": "Microsoft Teams communication actions: read channels/threads and post replies.", - "luma": "Luma event actions: list events, inspect event details, and create events.", - "linear": "Linear workflow actions: search/update issues and inspect projects/cycles.", - "jira": "Jira workflow actions: search/update issues and manage workflow transitions.", - "clickup": "ClickUp workflow actions: find/update tasks and lists.", - "airtable": "Airtable data actions: locate bases/tables and create/read/update records.", - "slack": "Slack communication actions: read channel/thread history and post replies.", - # generic_mcp specialist intentionally disabled for now. - # "generic_mcp": "handle tasks through user-defined custom app integration tools not covered above.", -} -_SPECIALIST_ORDER: tuple[str, ...] = tuple(_SPECIALIST_CAPABILITIES.keys()) - - -def _normalize_chat_visibility(thread_visibility: Any | None) -> ChatVisibility: - if thread_visibility is None: - return ChatVisibility.PRIVATE - if thread_visibility == ChatVisibility.SEARCH_SPACE: - return ChatVisibility.SEARCH_SPACE - raw = getattr(thread_visibility, "value", thread_visibility) - if str(raw).upper() == "SEARCH_SPACE": - return ChatVisibility.SEARCH_SPACE - return ChatVisibility.PRIVATE - - -def _identity_fragment_key(thread_visibility: Any | None) -> str: - """``private`` / ``team`` suffix for ``agent_*`` and ``memory_protocol_*`` fragments.""" - return ( - "team" - if _normalize_chat_visibility(thread_visibility) == ChatVisibility.SEARCH_SPACE - else "private" - ) - - -def _compose_identity_memory_citations( - *, - thread_visibility: Any | None, - citations_enabled: bool, -) -> str: - """Main-chat identity, memory protocol, and citation fragments (supervisor slice only).""" - key = _identity_fragment_key(thread_visibility) - today = datetime.now(UTC).date().isoformat() - - intro = _read_fragment(f"base/agent_{key}.md") - if intro: - intro = intro.format(resolved_today=today) - - memory = _read_fragment(f"base/memory_protocol_{key}.md").replace( - "call update_memory", - _MEMORY_SPECIALIST_PHRASE, - ) - tail = ( - f"\n{memory}\n\n\n" - + _build_citation_block(citations_enabled) - ) - return "\n\n".join(part for part in (intro.strip(), tail.strip()) if part) - - -def _memory_specialist_capability(thread_visibility: Any | None) -> str: - vis = str(getattr(thread_visibility, "value", thread_visibility)).upper() - if vis == "SEARCH_SPACE": - return "team memory actions: save shared team preferences, conventions, and long-lived team facts." - return "user memory actions: save personal preferences, instructions, and long-lived user facts." - - -def _specialists_markdown( - tools: Sequence[BaseTool], - *, - thread_visibility: Any | None, -) -> str: - available_names = { - tool.name for tool in tools if isinstance(getattr(tool, "name", None), str) - } - capabilities = dict(_SPECIALIST_CAPABILITIES) - capabilities["memory"] = _memory_specialist_capability(thread_visibility) - lines: list[str] = [] - for name in _SPECIALIST_ORDER: - if name in _BUILTIN_SPECIALISTS or name in available_names: - lines.append(f"- {name}: {capabilities[name]}") - return "\n".join(lines) - - -def build_supervisor_system_prompt( - tools: Sequence[BaseTool], - *, - thread_visibility: Any | None, - citations_enabled: bool, -) -> str: - """Load ``supervisor_prompt.md`` and fill placeholders.""" - template = read_prompt_md(supervisor_pkg.__name__, "supervisor_prompt") - specialists = _specialists_markdown(tools, thread_visibility=thread_visibility) - injected = _compose_identity_memory_citations( - thread_visibility=thread_visibility, - citations_enabled=citations_enabled, - ) - return template.replace("{{AVAILABLE_SPECIALISTS_LIST}}", specialists).replace( - "{{SUPERVISOR_BASE_INJECTION}}", - injected, - ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/supervisor/supervisor_prompt.md b/surfsense_backend/app/agents/multi_agent_chat/supervisor/supervisor_prompt.md deleted file mode 100644 index 632c888c9..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/supervisor/supervisor_prompt.md +++ /dev/null @@ -1,67 +0,0 @@ -{{SUPERVISOR_BASE_INJECTION}} - - -In this **multi-agent** session you also **coordinate specialists** (listed below): call a specialist only when their domain matches the need; give each call a compact, outcome-focused task; merge structured results into one clear user-facing reply. When you can satisfy the turn with your own tools and reasoning, do so without delegating. - - - -Use only the specialists listed below. -{{AVAILABLE_SPECIALISTS_LIST}} - - - -1) Delegate when the request clearly belongs to a specialist's capabilities. -2) Answer directly when no expert tool is needed. -3) For multi-domain work, decompose into sequential expert calls (or parallel only when independent). -4) Do not call a specialist "just in case". Every delegation must have a clear purpose. -5) Specialists are best for **one clear step at a time**—for example “find this,” “show that record,” “make this one change.” Do **not** hand them an entire “analyze everything and write me a trends report” brief in one go. -6) When the user wants **big-picture synthesis**—patterns across lots of items, comparisons across time, or an executive-style overview—**you** split the work: several **small** asks to whoever actually holds that information (each with a clear cap: how many items, how far back, which fields), then **you** combine the answers into one clear reply. If they need a **deliverable**—a real **artifact** others can read, hear, or watch (report, slide-style video, podcast, resume, image)—delegate to the **deliverables** specialist. Do not ask other specialists to replace that: their job is smaller steps (lookups and targeted changes), not producing the final artifact. -7) Each specialist answers in a **single short structured reply** (no extra chatter after it). Ask them only for what that reply can reasonably hold. If the user needs a long narrative or full report, **you** combine steps—or use the **deliverables** specialist—not one overloaded ask. -8) Prefer **a few clear, small asks** over one huge vague ask that invites guessing, cut-off answers, or broken replies. - - - -When delegating to a specialist, pass a compact but complete task that includes: -- the **outcome** they should produce, in **your own words** as clear instructions (do **not** paste or forward the user’s message verbatim), -- concrete limits (dates, names, “last N items,” which details matter), -- how you will judge success, -- any identifiers or links the user already gave. - -When asking for lists or searches, always say **how many** items at most and **which details** you need back. - -Never pass implementation chatter. Pass only actionable instructions. -Each delegation should sound like **one clear action** (or two that belong together), not a full project brief—unless you are intentionally speaking to **research** or to **deliverables** for a **deliverable artifact** (report, slide-style video, podcast, resume, image). - - - -Every specialist returns **one structured reply** in a fixed layout. Treat it like a small form, not prose. It includes: -- **outcome**: succeeded, partly done, blocked, or failed -- **short summary** of what they did -- **proof**: what they actually saw or changed (when relevant) -- **what to do next** if they are not done -- **what you must ask the user** if something was missing -- **what they assumed** if they had to fill a gap - -How to use it: -1) **Succeeded**: only treat it as done if the **proof** backs it up. -2) **Partly done**: use what they proved, then follow their **what to do next**. -3) **Blocked**: do not blindly retry; ask the user only what they said was missing (or pick from options they listed). -4) **Failed**: do not pretend it worked; either retry with a clearer small ask or explain honestly and follow their suggested recovery. -5) If the reply is missing, garbled, or contradicts itself, treat it as failed, do not invent facts, and recover with a safer smaller ask or a question to the user. - - - -Ask a concise clarifying question only when a missing detail blocks execution. -If one reasonable default is safe and obvious, use it and state the assumption. - - - -After expert calls, produce one coherent final answer: -- what was done, -- key results/artifacts, -- unresolved items and the next best step. -- include assumptions only when they affected outcomes. -- when multiple experts are used, merge outputs into one user-facing narrative (do not paste their raw structured reply verbatim). - -Never claim an action succeeded unless their reply includes proof that matches what you claim. -