mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-06 14:22:47 +02:00
Compose async multi-agent chat entrypoint and drop legacy supervisor scaffolding.
This commit is contained in:
parent
5497f472b2
commit
388e86ebc9
27 changed files with 149 additions and 779 deletions
|
|
@ -1,20 +1,14 @@
|
|||
"""
|
||||
Multi-agent chat (LangChain Subagents pattern).
|
||||
|
||||
**Vertical slices**
|
||||
**Layout (SRP)**
|
||||
|
||||
- :mod:`gmail` — connector tools, domain agent, ``domain_prompt.md``
|
||||
- :mod:`calendar` — connector tools, domain agent, ``domain_prompt.md``
|
||||
|
||||
**Shared**
|
||||
|
||||
- :mod:`shared` — prompt loader, ``build_domain_agent``, connector deps, invoke result parsing
|
||||
|
||||
**Cross-cutting**
|
||||
|
||||
- :mod:`routing` — supervisor routing tools + invoke helpers
|
||||
- :mod:`supervisor` — top graph + ``supervisor_prompt.md``
|
||||
- :mod:`integration` — ``create_multi_agent_chat``
|
||||
- :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
|
||||
|
|
@ -23,38 +17,116 @@ 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.calendar import (
|
||||
build_calendar_domain_agent,
|
||||
build_google_calendar_connector_tools,
|
||||
from app.agents.multi_agent_chat.expert_agent.builtins.deliverables import (
|
||||
build_deliverables_tools,
|
||||
build_deliverables_domain_agent,
|
||||
)
|
||||
from app.agents.multi_agent_chat.gmail import (
|
||||
build_gmail_connector_tools,
|
||||
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.integration import create_multi_agent_chat
|
||||
from app.agents.multi_agent_chat.shared import (
|
||||
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_domain_agents,
|
||||
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_gmail_connector_tools",
|
||||
"build_dropbox_tools",
|
||||
"build_dropbox_domain_agent",
|
||||
"build_gmail_tools",
|
||||
"build_gmail_domain_agent",
|
||||
"build_google_calendar_connector_tools",
|
||||
"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_domain_agents",
|
||||
"routing_tools_from_specs",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
"""Google Calendar vertical slice: connector tools, domain agent, ``domain_prompt.md``."""
|
||||
|
||||
from app.agents.multi_agent_chat.calendar.agent import build_calendar_domain_agent
|
||||
from app.agents.multi_agent_chat.calendar.connector_tools import (
|
||||
build_google_calendar_connector_tools,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"build_calendar_domain_agent",
|
||||
"build_google_calendar_connector_tools",
|
||||
]
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
"""Google Calendar domain agent graph."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import app.agents.multi_agent_chat.calendar as calendar_pkg
|
||||
from langchain_core.language_models import BaseChatModel
|
||||
from langchain_core.tools import BaseTool
|
||||
|
||||
from app.agents.multi_agent_chat.shared.domain_agent_factory import build_domain_agent
|
||||
|
||||
|
||||
def build_calendar_domain_agent(llm: BaseChatModel, tools: Sequence[BaseTool]):
|
||||
"""Compiled Calendar domain-agent graph (prompt + tools co-located under ``calendar``)."""
|
||||
return build_domain_agent(
|
||||
llm,
|
||||
tools,
|
||||
prompt_package=calendar_pkg.__name__,
|
||||
prompt_stem="domain_prompt",
|
||||
)
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
"""Google Calendar connector LangChain tools (``new_chat`` factories)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from langchain_core.tools import BaseTool
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.agents.multi_agent_chat.shared.deps import connector_binding
|
||||
from app.agents.new_chat.tools.google_calendar import (
|
||||
create_create_calendar_event_tool,
|
||||
create_delete_calendar_event_tool,
|
||||
create_search_calendar_events_tool,
|
||||
create_update_calendar_event_tool,
|
||||
)
|
||||
|
||||
|
||||
def build_google_calendar_connector_tools(
|
||||
*,
|
||||
db_session: AsyncSession,
|
||||
search_space_id: int,
|
||||
user_id: str,
|
||||
) -> list[BaseTool]:
|
||||
d = connector_binding(
|
||||
db_session=db_session,
|
||||
search_space_id=search_space_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
return [
|
||||
create_search_calendar_events_tool(**d),
|
||||
create_create_calendar_event_tool(**d),
|
||||
create_update_calendar_event_tool(**d),
|
||||
create_delete_calendar_event_tool(**d),
|
||||
]
|
||||
|
|
@ -1 +0,0 @@
|
|||
You are the Google Calendar domain agent. Use only the tools provided to complete calendar-related tasks. Stay focused on scheduling and calendar operations and respond concisely.
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
"""Gmail vertical slice: connector tools, domain agent, ``domain_prompt.md``."""
|
||||
|
||||
from app.agents.multi_agent_chat.gmail.agent import build_gmail_domain_agent
|
||||
from app.agents.multi_agent_chat.gmail.connector_tools import build_gmail_connector_tools
|
||||
|
||||
__all__ = [
|
||||
"build_gmail_connector_tools",
|
||||
"build_gmail_domain_agent",
|
||||
]
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
"""Gmail domain agent graph."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import app.agents.multi_agent_chat.gmail as gmail_pkg
|
||||
from langchain_core.language_models import BaseChatModel
|
||||
from langchain_core.tools import BaseTool
|
||||
|
||||
from app.agents.multi_agent_chat.shared.domain_agent_factory import build_domain_agent
|
||||
|
||||
|
||||
def build_gmail_domain_agent(llm: BaseChatModel, tools: Sequence[BaseTool]):
|
||||
"""Compiled Gmail domain-agent graph (prompt + tools co-located under ``gmail``)."""
|
||||
return build_domain_agent(
|
||||
llm,
|
||||
tools,
|
||||
prompt_package=gmail_pkg.__name__,
|
||||
prompt_stem="domain_prompt",
|
||||
)
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
"""Gmail connector LangChain tools (``new_chat`` factories; order matches registry)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from langchain_core.tools import BaseTool
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.agents.multi_agent_chat.shared.deps import connector_binding
|
||||
from app.agents.new_chat.tools.gmail import (
|
||||
create_create_gmail_draft_tool,
|
||||
create_read_gmail_email_tool,
|
||||
create_search_gmail_tool,
|
||||
create_send_gmail_email_tool,
|
||||
create_trash_gmail_email_tool,
|
||||
create_update_gmail_draft_tool,
|
||||
)
|
||||
|
||||
|
||||
def build_gmail_connector_tools(
|
||||
*,
|
||||
db_session: AsyncSession,
|
||||
search_space_id: int,
|
||||
user_id: str,
|
||||
) -> list[BaseTool]:
|
||||
d = connector_binding(
|
||||
db_session=db_session,
|
||||
search_space_id=search_space_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
return [
|
||||
create_search_gmail_tool(**d),
|
||||
create_read_gmail_email_tool(**d),
|
||||
create_create_gmail_draft_tool(**d),
|
||||
create_send_gmail_email_tool(**d),
|
||||
create_trash_gmail_email_tool(**d),
|
||||
create_update_gmail_draft_tool(**d),
|
||||
]
|
||||
|
|
@ -1 +0,0 @@
|
|||
You are the Gmail domain agent. Use only the tools provided to complete Gmail-related tasks. Stay focused on email operations and respond concisely.
|
||||
|
|
@ -2,36 +2,74 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
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.calendar import build_google_calendar_connector_tools
|
||||
from app.agents.multi_agent_chat.gmail import build_gmail_connector_tools
|
||||
from app.db import ChatVisibility
|
||||
|
||||
from app.agents.new_chat.tools.mcp_tool import load_mcp_tools
|
||||
|
||||
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 import build_registry_dependencies
|
||||
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
|
||||
|
||||
|
||||
def create_multi_agent_chat(
|
||||
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 | 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,
|
||||
):
|
||||
"""Build the full multi-agent chat graph (supervisor + Gmail + Calendar sub-agents via ``new_chat`` tools)."""
|
||||
"""Build the full multi-agent chat graph (supervisor + domain subgraphs via routing tools).
|
||||
|
||||
**Builtins** (:mod:`expert_agent.builtins`): registry-grouped **categories** (research, memory, deliverables).
|
||||
**Connectors** (:mod:`expert_agent.connectors`): **vendor integrations** — one subgraph each where split
|
||||
(e.g. Gmail, Calendar, Discord, Teams, Notion, Confluence, Google Drive, Dropbox, OneDrive, Luma).
|
||||
|
||||
MCP tools from ``new_chat`` (``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.
|
||||
|
||||
Deliverables (thread-scoped reports, podcasts, etc.) are registered only when ``thread_id`` is set.
|
||||
"""
|
||||
mcp_tools_by_route: dict[str, list[BaseTool]] | None = None
|
||||
if include_mcp_tools:
|
||||
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)
|
||||
mcp_tools_by_route = partition_mcp_tools_by_expert_route(mcp_flat, id_map, name_map)
|
||||
|
||||
registry_dependencies = build_registry_dependencies(
|
||||
db_session=db_session,
|
||||
search_space_id=search_space_id,
|
||||
user_id=user_id,
|
||||
thread_id=thread_id or "",
|
||||
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,
|
||||
)
|
||||
routing_tools = build_supervisor_routing_tools(
|
||||
llm,
|
||||
gmail_tools=build_gmail_connector_tools(
|
||||
db_session=db_session,
|
||||
search_space_id=search_space_id,
|
||||
user_id=user_id,
|
||||
),
|
||||
calendar_tools=build_google_calendar_connector_tools(
|
||||
db_session=db_session,
|
||||
search_space_id=search_space_id,
|
||||
user_id=user_id,
|
||||
),
|
||||
registry_dependencies=registry_dependencies,
|
||||
include_deliverables=thread_id is not None,
|
||||
mcp_tools_by_route=mcp_tools_by_route,
|
||||
)
|
||||
return build_supervisor_agent(llm, tools=routing_tools, checkpointer=checkpointer)
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
"""Cross-cutting helpers: prompt loading, domain agent factory, connector deps."""
|
||||
|
||||
from app.agents.multi_agent_chat.shared.deps import connector_binding
|
||||
from app.agents.multi_agent_chat.shared.domain_agent_factory import build_domain_agent
|
||||
from app.agents.multi_agent_chat.shared.invoke_output import extract_last_assistant_text
|
||||
from app.agents.multi_agent_chat.shared.prompt_loader import read_prompt_md
|
||||
|
||||
__all__ = [
|
||||
"build_domain_agent",
|
||||
"connector_binding",
|
||||
"extract_last_assistant_text",
|
||||
"read_prompt_md",
|
||||
]
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
"""Shared kwargs for ``new_chat`` connector tool factories."""
|
||||
|
||||
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,
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
"""Compile a domain agent graph 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.shared.prompt_loader 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),
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
"""Load ``*.md`` from any package (vertical slices use co-located prompts)."""
|
||||
|
||||
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. ``app.agents.multi_agent_chat.gmail``)."""
|
||||
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
|
||||
|
|
@ -11,7 +11,7 @@ from langchain_core.language_models import BaseChatModel
|
|||
from langchain_core.tools import BaseTool
|
||||
from langgraph.types import Checkpointer
|
||||
|
||||
from app.agents.multi_agent_chat.shared.prompt_loader import read_prompt_md
|
||||
from app.agents.multi_agent_chat.core.prompts import read_prompt_md
|
||||
|
||||
|
||||
def build_supervisor_agent(
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
"""Baseline deep-agent factory without SurfSense specialist subagents.
|
||||
|
||||
Swap imports manually while building supervisor-style delegation::
|
||||
|
||||
# from app.agents.new_chat.chat_deepagent import create_surfsense_deep_agent
|
||||
from app.agents.new_chat_supervisor_baseline.chat_deepagent import (
|
||||
create_surfsense_deep_agent,
|
||||
)
|
||||
|
||||
"""
|
||||
|
||||
from app.agents.new_chat_supervisor_baseline.chat_deepagent import (
|
||||
create_surfsense_deep_agent,
|
||||
)
|
||||
|
||||
__all__ = ["create_surfsense_deep_agent"]
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
"""
|
||||
Supervisor baseline: **no registry tools** and **no tool-injecting middleware**
|
||||
(no ``task`` / subagents, filesystem, todos, skills, permission, pruning, repair, …).
|
||||
|
||||
Connector/document discovery still feeds :class:`KnowledgePriorityMiddleware` so turns
|
||||
can include KB priority hints.
|
||||
|
||||
System prompt: :func:`build_supervisor_system_prompt` — SurfSense ``agent_*`` identity
|
||||
fragments plus supervisor-scoped KB/memory text and composer citation/provider blocks,
|
||||
without tool lists or ``tool_routing`` (see module docstring there).
|
||||
|
||||
See :mod:`app.agents.new_chat.chat_deepagent` for the full production agent.
|
||||
|
||||
Implementation: :mod:`app.agents.new_chat_supervisor_baseline.deep_agent`.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
from collections.abc import Sequence
|
||||
|
||||
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.new_chat.feature_flags import AgentFeatureFlags, get_flags
|
||||
from app.agents.new_chat.filesystem_selection import FilesystemSelection
|
||||
from app.agents.new_chat.llm_config import AgentConfig
|
||||
from app.db import ChatVisibility
|
||||
from app.services.connector_service import ConnectorService
|
||||
from app.utils.perf import get_perf_logger
|
||||
|
||||
from app.agents.new_chat_supervisor_baseline.deep_agent.compiled_agent import (
|
||||
build_compiled_agent_blocking,
|
||||
)
|
||||
from app.agents.new_chat_supervisor_baseline.deep_agent.connector_searchable import (
|
||||
map_connectors_to_searchable_types,
|
||||
)
|
||||
from app.agents.new_chat_supervisor_baseline.supervisor_system_prompt import (
|
||||
build_supervisor_system_prompt,
|
||||
)
|
||||
|
||||
_perf_log = get_perf_logger()
|
||||
|
||||
|
||||
async def create_surfsense_deep_agent(
|
||||
llm: BaseChatModel,
|
||||
search_space_id: int,
|
||||
db_session: AsyncSession,
|
||||
connector_service: ConnectorService,
|
||||
checkpointer: Checkpointer,
|
||||
user_id: str | None = None,
|
||||
thread_id: int | None = None,
|
||||
agent_config: AgentConfig | None = None,
|
||||
enabled_tools: list[str] | None = None,
|
||||
disabled_tools: list[str] | None = None,
|
||||
additional_tools: Sequence[BaseTool] | None = None,
|
||||
firecrawl_api_key: str | None = None,
|
||||
thread_visibility: ChatVisibility | None = None,
|
||||
mentioned_document_ids: list[int] | None = None,
|
||||
anon_session_id: str | None = None,
|
||||
filesystem_selection: FilesystemSelection | None = None,
|
||||
):
|
||||
"""
|
||||
Build the supervisor baseline agent: registry tools are not loaded.
|
||||
|
||||
Parameters such as ``enabled_tools``, ``additional_tools``, and ``firecrawl_api_key``
|
||||
are ignored for now; kept so call sites stay compatible.
|
||||
"""
|
||||
_ = (enabled_tools, disabled_tools, additional_tools, firecrawl_api_key, db_session)
|
||||
|
||||
_t_agent_total = time.perf_counter()
|
||||
|
||||
filesystem_selection = filesystem_selection or FilesystemSelection()
|
||||
_fs_mode = filesystem_selection.mode
|
||||
|
||||
available_connectors: list[str] | None = None
|
||||
available_document_types: list[str] | None = None
|
||||
|
||||
_t0 = time.perf_counter()
|
||||
try:
|
||||
connector_types = await connector_service.get_available_connectors(
|
||||
search_space_id
|
||||
)
|
||||
if connector_types:
|
||||
available_connectors = map_connectors_to_searchable_types(connector_types)
|
||||
|
||||
available_document_types = await connector_service.get_available_document_types(
|
||||
search_space_id
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to discover available connectors/document types: {e}")
|
||||
_perf_log.info(
|
||||
"[create_agent] Connector/doc-type discovery in %.3fs",
|
||||
time.perf_counter() - _t0,
|
||||
)
|
||||
|
||||
visibility = thread_visibility or ChatVisibility.PRIVATE
|
||||
|
||||
tools: list[BaseTool] = []
|
||||
|
||||
_flags: AgentFeatureFlags = get_flags()
|
||||
_perf_log.info("[create_agent] supervisor baseline: 0 registry tools")
|
||||
|
||||
_t0 = time.perf_counter()
|
||||
|
||||
final_system_prompt = build_supervisor_system_prompt(
|
||||
agent_config=agent_config,
|
||||
thread_visibility=thread_visibility,
|
||||
llm=llm,
|
||||
)
|
||||
_perf_log.info(
|
||||
"[create_agent] System prompt built in %.3fs", time.perf_counter() - _t0
|
||||
)
|
||||
|
||||
_t0 = time.perf_counter()
|
||||
agent = await asyncio.to_thread(
|
||||
build_compiled_agent_blocking,
|
||||
llm=llm,
|
||||
tools=tools,
|
||||
final_system_prompt=final_system_prompt,
|
||||
filesystem_mode=_fs_mode,
|
||||
search_space_id=search_space_id,
|
||||
user_id=user_id,
|
||||
thread_id=thread_id,
|
||||
visibility=visibility,
|
||||
anon_session_id=anon_session_id,
|
||||
available_connectors=available_connectors,
|
||||
available_document_types=available_document_types,
|
||||
mentioned_document_ids=mentioned_document_ids,
|
||||
flags=_flags,
|
||||
checkpointer=checkpointer,
|
||||
)
|
||||
_perf_log.info(
|
||||
"[create_agent] Middleware stack + graph compiled in %.3fs",
|
||||
time.perf_counter() - _t0,
|
||||
)
|
||||
|
||||
_perf_log.info(
|
||||
"[create_agent] Total agent creation in %.3fs",
|
||||
time.perf_counter() - _t_agent_total,
|
||||
)
|
||||
return agent
|
||||
|
|
@ -1 +0,0 @@
|
|||
"""Helpers used only by :mod:`app.agents.new_chat_supervisor_baseline.chat_deepagent`."""
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
"""Compile a minimal supervisor graph: no bound tools, no tool-injecting middleware."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from deepagents import __version__ as deepagents_version
|
||||
from deepagents.backends import StateBackend
|
||||
from langchain.agents import create_agent
|
||||
from langchain_anthropic.middleware import AnthropicPromptCachingMiddleware
|
||||
from langchain_core.language_models import BaseChatModel
|
||||
from langchain_core.tools import BaseTool
|
||||
from langgraph.types import Checkpointer
|
||||
|
||||
from app.agents.new_chat.context import SurfSenseContextSchema
|
||||
from app.agents.new_chat.feature_flags import AgentFeatureFlags
|
||||
from app.agents.new_chat.filesystem_selection import FilesystemMode
|
||||
from app.agents.new_chat.middleware import (
|
||||
AnonymousDocumentMiddleware,
|
||||
FileIntentMiddleware,
|
||||
KnowledgeBasePersistenceMiddleware,
|
||||
KnowledgePriorityMiddleware,
|
||||
KnowledgeTreeMiddleware,
|
||||
MemoryInjectionMiddleware,
|
||||
create_surfsense_compaction_middleware,
|
||||
)
|
||||
from app.db import ChatVisibility
|
||||
|
||||
|
||||
def build_compiled_agent_blocking(
|
||||
*,
|
||||
llm: BaseChatModel,
|
||||
tools: Sequence[BaseTool],
|
||||
final_system_prompt: str,
|
||||
filesystem_mode: FilesystemMode,
|
||||
search_space_id: int,
|
||||
user_id: str | None,
|
||||
thread_id: int | 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,
|
||||
flags: AgentFeatureFlags,
|
||||
checkpointer: Checkpointer,
|
||||
):
|
||||
"""Build middleware + compile graph synchronously (typically ``asyncio.to_thread``).
|
||||
|
||||
Intentionally excludes registry tools (``tools`` should be ``[]``), SubAgent/task,
|
||||
filesystem/todo/skills middleware, and tool-centric hygiene (repair, dedup, permission).
|
||||
"""
|
||||
_ = flags # retained for API parity with callers; stack is fixed minimal for now
|
||||
|
||||
_memory_middleware = MemoryInjectionMiddleware(
|
||||
user_id=user_id,
|
||||
search_space_id=search_space_id,
|
||||
thread_visibility=visibility,
|
||||
)
|
||||
|
||||
summarization_mw = create_surfsense_compaction_middleware(llm, StateBackend)
|
||||
|
||||
deepagent_middleware = [
|
||||
_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),
|
||||
KnowledgeBasePersistenceMiddleware(
|
||||
search_space_id=search_space_id,
|
||||
created_by_id=user_id,
|
||||
filesystem_mode=filesystem_mode,
|
||||
thread_id=thread_id,
|
||||
)
|
||||
if filesystem_mode == FilesystemMode.CLOUD
|
||||
else None,
|
||||
summarization_mw,
|
||||
AnthropicPromptCachingMiddleware(unsupported_model_behavior="ignore"),
|
||||
]
|
||||
deepagent_middleware = [m for m in deepagent_middleware if m is not None]
|
||||
|
||||
agent = create_agent(
|
||||
llm,
|
||||
system_prompt=final_system_prompt,
|
||||
tools=list(tools),
|
||||
middleware=deepagent_middleware,
|
||||
context_schema=SurfSenseContextSchema,
|
||||
checkpointer=checkpointer,
|
||||
)
|
||||
return agent.with_config(
|
||||
{
|
||||
"recursion_limit": 10_000,
|
||||
"metadata": {
|
||||
"ls_integration": "deepagents",
|
||||
"versions": {"deepagents": deepagents_version},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
"""Map connector enum values to searchable document/connector type strings."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
_CONNECTOR_TYPE_TO_SEARCHABLE: dict[str, str] = {
|
||||
"TAVILY_API": "TAVILY_API",
|
||||
"LINKUP_API": "LINKUP_API",
|
||||
"BAIDU_SEARCH_API": "BAIDU_SEARCH_API",
|
||||
"SLACK_CONNECTOR": "SLACK_CONNECTOR",
|
||||
"TEAMS_CONNECTOR": "TEAMS_CONNECTOR",
|
||||
"NOTION_CONNECTOR": "NOTION_CONNECTOR",
|
||||
"GITHUB_CONNECTOR": "GITHUB_CONNECTOR",
|
||||
"LINEAR_CONNECTOR": "LINEAR_CONNECTOR",
|
||||
"DISCORD_CONNECTOR": "DISCORD_CONNECTOR",
|
||||
"JIRA_CONNECTOR": "JIRA_CONNECTOR",
|
||||
"CONFLUENCE_CONNECTOR": "CONFLUENCE_CONNECTOR",
|
||||
"CLICKUP_CONNECTOR": "CLICKUP_CONNECTOR",
|
||||
"GOOGLE_CALENDAR_CONNECTOR": "GOOGLE_CALENDAR_CONNECTOR",
|
||||
"GOOGLE_GMAIL_CONNECTOR": "GOOGLE_GMAIL_CONNECTOR",
|
||||
"GOOGLE_DRIVE_CONNECTOR": "GOOGLE_DRIVE_FILE",
|
||||
"AIRTABLE_CONNECTOR": "AIRTABLE_CONNECTOR",
|
||||
"LUMA_CONNECTOR": "LUMA_CONNECTOR",
|
||||
"ELASTICSEARCH_CONNECTOR": "ELASTICSEARCH_CONNECTOR",
|
||||
"WEBCRAWLER_CONNECTOR": "CRAWLED_URL",
|
||||
"BOOKSTACK_CONNECTOR": "BOOKSTACK_CONNECTOR",
|
||||
"CIRCLEBACK_CONNECTOR": "CIRCLEBACK",
|
||||
"OBSIDIAN_CONNECTOR": "OBSIDIAN_CONNECTOR",
|
||||
"DROPBOX_CONNECTOR": "DROPBOX_FILE",
|
||||
"ONEDRIVE_CONNECTOR": "ONEDRIVE_FILE",
|
||||
"COMPOSIO_GOOGLE_DRIVE_CONNECTOR": "GOOGLE_DRIVE_FILE",
|
||||
"COMPOSIO_GMAIL_CONNECTOR": "GOOGLE_GMAIL_CONNECTOR",
|
||||
"COMPOSIO_GOOGLE_CALENDAR_CONNECTOR": "GOOGLE_CALENDAR_CONNECTOR",
|
||||
}
|
||||
|
||||
_ALWAYS_AVAILABLE_DOC_TYPES: tuple[str, ...] = (
|
||||
"EXTENSION",
|
||||
"FILE",
|
||||
"NOTE",
|
||||
"YOUTUBE_VIDEO",
|
||||
)
|
||||
|
||||
|
||||
def map_connectors_to_searchable_types(connector_types: list[Any]) -> list[str]:
|
||||
"""Map connector types to searchable strings; dedupe preserving order."""
|
||||
result_set: set[str] = set()
|
||||
result_list: list[str] = []
|
||||
|
||||
for doc_type in _ALWAYS_AVAILABLE_DOC_TYPES:
|
||||
if doc_type not in result_set:
|
||||
result_set.add(doc_type)
|
||||
result_list.append(doc_type)
|
||||
|
||||
for ct in connector_types:
|
||||
ct_str = ct.value if hasattr(ct, "value") else str(ct)
|
||||
searchable = _CONNECTOR_TYPE_TO_SEARCHABLE.get(ct_str)
|
||||
if searchable and searchable not in result_set:
|
||||
result_set.add(searchable)
|
||||
result_list.append(searchable)
|
||||
|
||||
return result_list
|
||||
|
|
@ -1 +0,0 @@
|
|||
"""Supervisor-scoped prompt fragments (adaptations of ``new_chat/prompts/base``)."""
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<knowledge_base_only_policy>
|
||||
Adapted from ``prompts/base/kb_only_policy_private.md`` for supervisor-only runs (no web
|
||||
search / scrape / connector tools on this node).
|
||||
|
||||
CRITICAL RULE — KNOWLEDGE CONTEXT FIRST FOR FACTUAL QUESTIONS:
|
||||
- For factual or informational questions, rely on information in this thread and on
|
||||
knowledge SurfSense surfaces in your prompt (for example priority document excerpts
|
||||
or injected memory text). Do not substitute unchecked general knowledge unless the
|
||||
user explicitly opts in.
|
||||
- If nothing in the conversation or injected context answers the question, you MUST:
|
||||
1. Say you could not find it in the available SurfSense context for this turn.
|
||||
2. Ask: "Would you like me to answer from my general knowledge instead?"
|
||||
3. ONLY provide a general-knowledge answer AFTER the user explicitly says yes.
|
||||
- This policy does NOT apply to:
|
||||
* Casual conversation, greetings, or meta-questions about SurfSense itself
|
||||
* Formatting, summarization, or analysis of content already present in the conversation
|
||||
* Following user instructions that are clearly task-oriented (e.g., "rewrite this in bullet points")
|
||||
</knowledge_base_only_policy>
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<knowledge_base_only_policy>
|
||||
Adapted from ``prompts/base/kb_only_policy_team.md`` for supervisor-only runs (no web
|
||||
search / scrape / connector tools on this node).
|
||||
|
||||
CRITICAL RULE — TEAM KNOWLEDGE CONTEXT FIRST FOR FACTUAL QUESTIONS:
|
||||
- For factual or informational questions, rely on information in this thread and on
|
||||
knowledge SurfSense surfaces in your prompt from the shared space (for example
|
||||
priority document excerpts or injected memory text). Do not substitute unchecked
|
||||
general knowledge unless a team member explicitly opts in.
|
||||
- If nothing in the conversation or injected context answers the question, you MUST:
|
||||
1. Say you could not find it in the available SurfSense context for this turn.
|
||||
2. Ask: "Would you like me to answer from my general knowledge instead?"
|
||||
3. ONLY provide a general-knowledge answer AFTER a team member explicitly says yes.
|
||||
- This policy does NOT apply to:
|
||||
* Casual conversation, greetings, or meta-questions about SurfSense itself
|
||||
* Formatting, summarization, or analysis of content already present in the conversation
|
||||
* Following user instructions that are clearly task-oriented (e.g., "rewrite this in bullet points")
|
||||
</knowledge_base_only_policy>
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<memory_context>
|
||||
Derived from ``prompts/base/memory_protocol_*.md``, without requiring ``update_memory``
|
||||
calls (this supervisor node does not expose that tool).
|
||||
|
||||
Personalized memory text may be injected into your prompt when configured. You cannot
|
||||
persist new long-term memory from this supervisor node; if the user asks you to
|
||||
remember something permanently, explain that doing so requires the full SurfSense
|
||||
agent with memory tools enabled or another persistence path they configure.
|
||||
</memory_context>
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<supervisor_graph_role>
|
||||
This node follows the LangGraph multi-agent **supervisor** pattern: the supervisor
|
||||
language model responds from the current conversation and optional supervisor-scoped
|
||||
system prompt (see LangChain Reference: ``langgraph_supervisor.create_supervisor``,
|
||||
parameter ``prompt`` — typically a ``SystemMessage`` that scopes routing and handoff
|
||||
behavior). In this SurfSense deployment the supervisor graph does **not** attach
|
||||
registry tools or worker subgraphs—answer from messages and system-injected context,
|
||||
and state plainly when the user expects tools or delegations that are not wired here.
|
||||
</supervisor_graph_role>
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
"""Supervisor-scoped system prompt for ``new_chat_supervisor_baseline``.
|
||||
|
||||
Composition follows the same fragment discipline as
|
||||
:func:`app.agents.new_chat.prompts.composer.compose_system_prompt`, but **omits**
|
||||
sections that assume registry tools: ``base/tool_routing_*.md``, ``tools/_preamble.md``,
|
||||
the tools/examples blocks, ``base/parameter_resolution.md`` (discovery lists concrete
|
||||
tools), and ``base/memory_protocol_*.md`` (requires ``update_memory`` calls).
|
||||
|
||||
**Authoritative supervisor semantics:** LangChain Reference documents
|
||||
``langgraph_supervisor.create_supervisor`` — the supervisor graph accepts an optional
|
||||
``prompt`` (typically a ``SystemMessage``) that scopes the supervisor LLM alongside
|
||||
managed worker graphs.
|
||||
|
||||
**SurfSense sources reused verbatim where applicable:** ``prompts/base/agent_private.md`` /
|
||||
``agent_team.md`` from :mod:`app.agents.new_chat.prompts`. KB policy is adapted from
|
||||
``base/kb_only_policy_*.md`` into supervisor-local fragments that reference injected
|
||||
context instead of tool outputs. Provider and citation blocks reuse
|
||||
``composer._build_provider_block`` / ``_build_citation_block`` and
|
||||
``composer.detect_provider_variant`` unchanged.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from importlib import resources
|
||||
|
||||
from langchain_core.language_models import BaseChatModel
|
||||
|
||||
from app.agents.new_chat.llm_config import AgentConfig
|
||||
from app.agents.new_chat.prompts import composer as pc
|
||||
from app.db import ChatVisibility
|
||||
|
||||
_SUP_PROMPTS_PKG = "app.agents.new_chat_supervisor_baseline.prompts"
|
||||
|
||||
|
||||
def _read_supervisor_fragment(filename: str) -> str:
|
||||
try:
|
||||
ref = resources.files(_SUP_PROMPTS_PKG).joinpath(filename)
|
||||
if not ref.is_file():
|
||||
return ""
|
||||
text = ref.read_text(encoding="utf-8")
|
||||
except (FileNotFoundError, ModuleNotFoundError, OSError):
|
||||
return ""
|
||||
if text.endswith("\n"):
|
||||
text = text[:-1]
|
||||
return text
|
||||
|
||||
|
||||
def _build_supervisor_system_instruction_block(
|
||||
*,
|
||||
visibility: ChatVisibility,
|
||||
resolved_today: str,
|
||||
) -> str:
|
||||
"""``<system_instruction>`` body: LangGraph supervisor scope + SurfSense identity + adapted KB + memory limits."""
|
||||
variant = "team" if visibility == ChatVisibility.SEARCH_SPACE else "private"
|
||||
sections = [
|
||||
_read_supervisor_fragment("supervisor_graph_role.md"),
|
||||
pc._read_fragment(f"base/agent_{variant}.md"),
|
||||
_read_supervisor_fragment(f"kb_policy_supervisor_{variant}.md"),
|
||||
_read_supervisor_fragment("memory_context_supervisor.md"),
|
||||
]
|
||||
body = "\n\n".join(s for s in sections if s)
|
||||
block = f"\n<system_instruction>\n{body}\n\n</system_instruction>\n"
|
||||
return block.format(resolved_today=resolved_today)
|
||||
|
||||
|
||||
def resolve_llm_model_name(llm: BaseChatModel) -> str | None:
|
||||
"""Best-effort model id string for :func:`composer.detect_provider_variant`."""
|
||||
name = getattr(llm, "model_name", None)
|
||||
if isinstance(name, str) and name.strip():
|
||||
return name.strip()
|
||||
model = getattr(llm, "model", None)
|
||||
if isinstance(model, str) and model.strip():
|
||||
return model.strip()
|
||||
profile = getattr(llm, "profile", None)
|
||||
if isinstance(profile, dict):
|
||||
for key in ("model", "model_name"):
|
||||
m = profile.get(key)
|
||||
if isinstance(m, str) and m.strip():
|
||||
return m.strip()
|
||||
return None
|
||||
|
||||
|
||||
def build_supervisor_system_prompt(
|
||||
*,
|
||||
agent_config: AgentConfig | None,
|
||||
thread_visibility: ChatVisibility | None,
|
||||
llm: BaseChatModel,
|
||||
) -> str:
|
||||
"""Assemble the supervisor system prompt (no tool-list or tool-routing fragments)."""
|
||||
resolved_today = datetime.now(UTC).astimezone(UTC).date().isoformat()
|
||||
visibility = thread_visibility or ChatVisibility.PRIVATE
|
||||
model_name = resolve_llm_model_name(llm)
|
||||
|
||||
if agent_config is not None:
|
||||
custom = (agent_config.system_instructions or "").strip()
|
||||
if custom:
|
||||
sys_block = agent_config.system_instructions.format(resolved_today=resolved_today)
|
||||
elif agent_config.use_default_system_instructions:
|
||||
sys_block = _build_supervisor_system_instruction_block(
|
||||
visibility=visibility,
|
||||
resolved_today=resolved_today,
|
||||
)
|
||||
else:
|
||||
sys_block = ""
|
||||
else:
|
||||
sys_block = _build_supervisor_system_instruction_block(
|
||||
visibility=visibility,
|
||||
resolved_today=resolved_today,
|
||||
)
|
||||
|
||||
provider_variant = pc.detect_provider_variant(model_name)
|
||||
sys_block += pc._build_provider_block(provider_variant)
|
||||
|
||||
if agent_config is None:
|
||||
citations_enabled = True
|
||||
else:
|
||||
citations_enabled = agent_config.citations_enabled
|
||||
|
||||
sys_block += pc._build_citation_block(citations_enabled)
|
||||
return sys_block
|
||||
Loading…
Add table
Add a link
Reference in a new issue