Compose async multi-agent chat entrypoint and drop legacy supervisor scaffolding.

This commit is contained in:
CREDO23 2026-04-30 02:36:22 +02:00
parent 5497f472b2
commit 388e86ebc9
27 changed files with 149 additions and 779 deletions

View file

@ -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",
]

View file

@ -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",
]

View file

@ -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",
)

View file

@ -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),
]

View file

@ -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.

View file

@ -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",
]

View file

@ -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",
)

View file

@ -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),
]

View file

@ -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.

View file

@ -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)

View file

@ -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",
]

View file

@ -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,
}

View file

@ -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),
)

View file

@ -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)

View file

@ -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

View file

@ -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(