multi_agent_chat/subagents: dict-keyed middleware_stack + always-on KB

This commit is contained in:
CREDO23 2026-05-12 18:04:54 +02:00
parent eee861bb3d
commit d843468256
39 changed files with 232 additions and 203 deletions

View file

@ -2,7 +2,6 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Any
from deepagents import SubAgent
@ -29,7 +28,7 @@ def build_subagent(
*,
dependencies: dict[str, Any],
model: BaseChatModel | None = None,
extra_middleware: Sequence[Any] | None = None,
middleware_stack: dict[str, Any] | None = None,
extra_tools_bucket: ToolsPermissions | None = None,
) -> SubAgent:
buckets = load_tools(dependencies=dependencies)
@ -51,5 +50,5 @@ def build_subagent(
tools=tools,
interrupt_on=interrupt_on,
model=model,
extra_middleware=extra_middleware,
middleware_stack=middleware_stack,
)

View file

@ -1,9 +1,12 @@
"""`knowledge_base` route: ``SubAgent`` spec for the SurfSense KB specialist.
The KB subagent owns the `/documents/` workspace: reading, writing, editing,
searching, and organising user documents. It shares the orchestrator's
``workspace_tree_text`` and ``kb_priority`` via state and re-emits them as
SystemMessages through the projection middleware (no extra DB / LLM calls).
Owns the ``/documents/`` workspace (read, write, edit, search, organise)
and shares the orchestrator's ``workspace_tree_text`` and ``kb_priority``
via state. KB conforms to :class:`SubagentBuilder` but composes its
middleware list itself: it picks individual entries from
``middleware_stack`` by key so resilience lands just outside the
Anthropic cache (inside the filesystem and projection middlewares),
which a flat prepend can't satisfy.
"""
from __future__ import annotations
@ -11,7 +14,6 @@ from __future__ import annotations
from typing import Any, cast
from deepagents import SubAgent
from langchain_anthropic.middleware import AnthropicPromptCachingMiddleware
from langchain_core.language_models import BaseChatModel
from app.agents.multi_agent_chat.middleware.shared.anthropic_cache import (
@ -29,13 +31,12 @@ from app.agents.multi_agent_chat.middleware.shared.kb_context_projection import
from app.agents.multi_agent_chat.middleware.shared.patch_tool_calls import (
build_patch_tool_calls_mw,
)
from app.agents.multi_agent_chat.middleware.shared.resilience import (
ResilienceBundle,
)
from app.agents.multi_agent_chat.middleware.shared.todos import build_todos_mw
from app.agents.multi_agent_chat.subagents.shared.md_file_reader import (
read_md_file,
)
from app.agents.multi_agent_chat.subagents.shared.permissions import (
ToolsPermissions,
)
from app.agents.new_chat.filesystem_selection import FilesystemMode
from .tools.index import destructive_fs_interrupt_on
@ -45,20 +46,19 @@ NAME = "knowledge_base"
def build_subagent(
*,
llm: BaseChatModel,
backend_resolver: Any,
filesystem_mode: FilesystemMode,
search_space_id: int,
user_id: str | None,
thread_id: int | None,
resilience: ResilienceBundle,
dependencies: dict[str, Any],
model: BaseChatModel | None = None,
middleware_stack: dict[str, Any] | None = None,
extra_tools_bucket: ToolsPermissions | None = None, # noqa: ARG001 — KB ships fixed tools
) -> SubAgent:
"""Resilience inserts encapsulated here so the orchestrator never mutates the list."""
description = read_md_file(__package__, "description").strip()
if not description:
description = (
"Handles knowledge-base reads, writes, edits, and organisation."
)
"""Conforms to :class:`SubagentBuilder`; KB splices the shared stack itself."""
llm = model if model is not None else dependencies["llm"]
filesystem_mode: FilesystemMode = dependencies["filesystem_mode"]
mws = middleware_stack or {}
description = read_md_file(__package__, "description").strip() or (
"Handles knowledge-base reads, writes, edits, and organisation."
)
prompt_stem = (
"system_prompt_cloud"
if filesystem_mode == FilesystemMode.CLOUD
@ -66,40 +66,39 @@ def build_subagent(
)
system_prompt = read_md_file(__package__, prompt_stem).strip()
resilience_mws = [
m
for m in (
mws.get("retry"),
mws.get("fallback"),
mws.get("model_call_limit"),
mws.get("tool_call_limit"),
)
if m is not None
]
middleware: list[Any] = [
build_todos_mw(),
mws["todos"],
build_kb_context_projection_mw(),
build_filesystem_mw(
backend_resolver=backend_resolver,
backend_resolver=dependencies["backend_resolver"],
filesystem_mode=filesystem_mode,
search_space_id=search_space_id,
user_id=user_id,
thread_id=thread_id,
search_space_id=dependencies["search_space_id"],
user_id=dependencies.get("user_id"),
thread_id=dependencies.get("thread_id"),
),
build_compaction_mw(llm),
build_patch_tool_calls_mw(),
*resilience_mws,
build_anthropic_cache_mw(),
]
resilience_mws = resilience.as_list()
if resilience_mws:
cache_idx = next(
(
i
for i, m in enumerate(middleware)
if isinstance(m, AnthropicPromptCachingMiddleware)
),
len(middleware),
)
for offset, mw in enumerate(resilience_mws):
middleware.insert(cache_idx + offset, mw)
spec: dict[str, Any] = {
"name": NAME,
"description": description,
"system_prompt": system_prompt,
"model": llm,
"tools": [],
"tools": [], # KB virtual FS tools are injected at runtime by SurfSenseFilesystemMiddleware
"middleware": middleware,
"interrupt_on": destructive_fs_interrupt_on(),
}

View file

@ -2,7 +2,6 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Any
from deepagents import SubAgent
@ -29,7 +28,7 @@ def build_subagent(
*,
dependencies: dict[str, Any],
model: BaseChatModel | None = None,
extra_middleware: Sequence[Any] | None = None,
middleware_stack: dict[str, Any] | None = None,
extra_tools_bucket: ToolsPermissions | None = None,
) -> SubAgent:
buckets = load_tools(dependencies=dependencies)
@ -51,5 +50,5 @@ def build_subagent(
tools=tools,
interrupt_on=interrupt_on,
model=model,
extra_middleware=extra_middleware,
middleware_stack=middleware_stack,
)

View file

@ -2,7 +2,6 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Any
from deepagents import SubAgent
@ -29,7 +28,7 @@ def build_subagent(
*,
dependencies: dict[str, Any],
model: BaseChatModel | None = None,
extra_middleware: Sequence[Any] | None = None,
middleware_stack: dict[str, Any] | None = None,
extra_tools_bucket: ToolsPermissions | None = None,
) -> SubAgent:
buckets = load_tools(dependencies=dependencies)
@ -51,5 +50,5 @@ def build_subagent(
tools=tools,
interrupt_on=interrupt_on,
model=model,
extra_middleware=extra_middleware,
middleware_stack=middleware_stack,
)

View file

@ -2,7 +2,6 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Any
from deepagents import SubAgent
@ -29,7 +28,7 @@ def build_subagent(
*,
dependencies: dict[str, Any],
model: BaseChatModel | None = None,
extra_middleware: Sequence[Any] | None = None,
middleware_stack: dict[str, Any] | None = None,
extra_tools_bucket: ToolsPermissions | None = None,
) -> SubAgent:
buckets = load_tools(dependencies=dependencies)
@ -51,5 +50,5 @@ def build_subagent(
tools=tools,
interrupt_on=interrupt_on,
model=model,
extra_middleware=extra_middleware,
middleware_stack=middleware_stack,
)

View file

@ -2,7 +2,6 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Any
from deepagents import SubAgent
@ -29,7 +28,7 @@ def build_subagent(
*,
dependencies: dict[str, Any],
model: BaseChatModel | None = None,
extra_middleware: Sequence[Any] | None = None,
middleware_stack: dict[str, Any] | None = None,
extra_tools_bucket: ToolsPermissions | None = None,
) -> SubAgent:
buckets = load_tools(dependencies=dependencies)
@ -51,5 +50,5 @@ def build_subagent(
tools=tools,
interrupt_on=interrupt_on,
model=model,
extra_middleware=extra_middleware,
middleware_stack=middleware_stack,
)

View file

@ -2,7 +2,6 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Any
from deepagents import SubAgent
@ -29,7 +28,7 @@ def build_subagent(
*,
dependencies: dict[str, Any],
model: BaseChatModel | None = None,
extra_middleware: Sequence[Any] | None = None,
middleware_stack: dict[str, Any] | None = None,
extra_tools_bucket: ToolsPermissions | None = None,
) -> SubAgent:
buckets = load_tools(dependencies=dependencies)
@ -51,5 +50,5 @@ def build_subagent(
tools=tools,
interrupt_on=interrupt_on,
model=model,
extra_middleware=extra_middleware,
middleware_stack=middleware_stack,
)

View file

@ -2,7 +2,6 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Any
from deepagents import SubAgent
@ -29,7 +28,7 @@ def build_subagent(
*,
dependencies: dict[str, Any],
model: BaseChatModel | None = None,
extra_middleware: Sequence[Any] | None = None,
middleware_stack: dict[str, Any] | None = None,
extra_tools_bucket: ToolsPermissions | None = None,
) -> SubAgent:
buckets = load_tools(dependencies=dependencies)
@ -51,5 +50,5 @@ def build_subagent(
tools=tools,
interrupt_on=interrupt_on,
model=model,
extra_middleware=extra_middleware,
middleware_stack=middleware_stack,
)

View file

@ -2,7 +2,6 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Any
from deepagents import SubAgent
@ -29,7 +28,7 @@ def build_subagent(
*,
dependencies: dict[str, Any],
model: BaseChatModel | None = None,
extra_middleware: Sequence[Any] | None = None,
middleware_stack: dict[str, Any] | None = None,
extra_tools_bucket: ToolsPermissions | None = None,
) -> SubAgent:
buckets = load_tools(dependencies=dependencies)
@ -51,5 +50,5 @@ def build_subagent(
tools=tools,
interrupt_on=interrupt_on,
model=model,
extra_middleware=extra_middleware,
middleware_stack=middleware_stack,
)

View file

@ -2,7 +2,6 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Any
from deepagents import SubAgent
@ -29,7 +28,7 @@ def build_subagent(
*,
dependencies: dict[str, Any],
model: BaseChatModel | None = None,
extra_middleware: Sequence[Any] | None = None,
middleware_stack: dict[str, Any] | None = None,
extra_tools_bucket: ToolsPermissions | None = None,
) -> SubAgent:
buckets = load_tools(dependencies=dependencies)
@ -51,5 +50,5 @@ def build_subagent(
tools=tools,
interrupt_on=interrupt_on,
model=model,
extra_middleware=extra_middleware,
middleware_stack=middleware_stack,
)

View file

@ -2,7 +2,6 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Any
from deepagents import SubAgent
@ -29,7 +28,7 @@ def build_subagent(
*,
dependencies: dict[str, Any],
model: BaseChatModel | None = None,
extra_middleware: Sequence[Any] | None = None,
middleware_stack: dict[str, Any] | None = None,
extra_tools_bucket: ToolsPermissions | None = None,
) -> SubAgent:
buckets = load_tools(dependencies=dependencies)
@ -51,5 +50,5 @@ def build_subagent(
tools=tools,
interrupt_on=interrupt_on,
model=model,
extra_middleware=extra_middleware,
middleware_stack=middleware_stack,
)

View file

@ -2,7 +2,6 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Any
from deepagents import SubAgent
@ -29,7 +28,7 @@ def build_subagent(
*,
dependencies: dict[str, Any],
model: BaseChatModel | None = None,
extra_middleware: Sequence[Any] | None = None,
middleware_stack: dict[str, Any] | None = None,
extra_tools_bucket: ToolsPermissions | None = None,
) -> SubAgent:
buckets = load_tools(dependencies=dependencies)
@ -51,5 +50,5 @@ def build_subagent(
tools=tools,
interrupt_on=interrupt_on,
model=model,
extra_middleware=extra_middleware,
middleware_stack=middleware_stack,
)

View file

@ -2,7 +2,6 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Any
from deepagents import SubAgent
@ -29,7 +28,7 @@ def build_subagent(
*,
dependencies: dict[str, Any],
model: BaseChatModel | None = None,
extra_middleware: Sequence[Any] | None = None,
middleware_stack: dict[str, Any] | None = None,
extra_tools_bucket: ToolsPermissions | None = None,
) -> SubAgent:
buckets = load_tools(dependencies=dependencies)
@ -51,5 +50,5 @@ def build_subagent(
tools=tools,
interrupt_on=interrupt_on,
model=model,
extra_middleware=extra_middleware,
middleware_stack=middleware_stack,
)

View file

@ -2,7 +2,6 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Any
from deepagents import SubAgent
@ -29,7 +28,7 @@ def build_subagent(
*,
dependencies: dict[str, Any],
model: BaseChatModel | None = None,
extra_middleware: Sequence[Any] | None = None,
middleware_stack: dict[str, Any] | None = None,
extra_tools_bucket: ToolsPermissions | None = None,
) -> SubAgent:
buckets = load_tools(dependencies=dependencies)
@ -51,5 +50,5 @@ def build_subagent(
tools=tools,
interrupt_on=interrupt_on,
model=model,
extra_middleware=extra_middleware,
middleware_stack=middleware_stack,
)

View file

@ -2,7 +2,6 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Any
from deepagents import SubAgent
@ -29,7 +28,7 @@ def build_subagent(
*,
dependencies: dict[str, Any],
model: BaseChatModel | None = None,
extra_middleware: Sequence[Any] | None = None,
middleware_stack: dict[str, Any] | None = None,
extra_tools_bucket: ToolsPermissions | None = None,
) -> SubAgent:
buckets = load_tools(dependencies=dependencies)
@ -51,5 +50,5 @@ def build_subagent(
tools=tools,
interrupt_on=interrupt_on,
model=model,
extra_middleware=extra_middleware,
middleware_stack=middleware_stack,
)

View file

@ -2,7 +2,6 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Any
from deepagents import SubAgent
@ -29,7 +28,7 @@ def build_subagent(
*,
dependencies: dict[str, Any],
model: BaseChatModel | None = None,
extra_middleware: Sequence[Any] | None = None,
middleware_stack: dict[str, Any] | None = None,
extra_tools_bucket: ToolsPermissions | None = None,
) -> SubAgent:
buckets = load_tools(dependencies=dependencies)
@ -51,5 +50,5 @@ def build_subagent(
tools=tools,
interrupt_on=interrupt_on,
model=model,
extra_middleware=extra_middleware,
middleware_stack=middleware_stack,
)

View file

@ -2,7 +2,6 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Any
from deepagents import SubAgent
@ -29,7 +28,7 @@ def build_subagent(
*,
dependencies: dict[str, Any],
model: BaseChatModel | None = None,
extra_middleware: Sequence[Any] | None = None,
middleware_stack: dict[str, Any] | None = None,
extra_tools_bucket: ToolsPermissions | None = None,
) -> SubAgent:
buckets = load_tools(dependencies=dependencies)
@ -51,5 +50,5 @@ def build_subagent(
tools=tools,
interrupt_on=interrupt_on,
model=model,
extra_middleware=extra_middleware,
middleware_stack=middleware_stack,
)

View file

@ -2,7 +2,6 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Any
from deepagents import SubAgent
@ -29,7 +28,7 @@ def build_subagent(
*,
dependencies: dict[str, Any],
model: BaseChatModel | None = None,
extra_middleware: Sequence[Any] | None = None,
middleware_stack: dict[str, Any] | None = None,
extra_tools_bucket: ToolsPermissions | None = None,
) -> SubAgent:
buckets = load_tools(dependencies=dependencies)
@ -51,5 +50,5 @@ def build_subagent(
tools=tools,
interrupt_on=interrupt_on,
model=model,
extra_middleware=extra_middleware,
middleware_stack=middleware_stack,
)

View file

@ -2,7 +2,6 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Any
from deepagents import SubAgent
@ -29,7 +28,7 @@ def build_subagent(
*,
dependencies: dict[str, Any],
model: BaseChatModel | None = None,
extra_middleware: Sequence[Any] | None = None,
middleware_stack: dict[str, Any] | None = None,
extra_tools_bucket: ToolsPermissions | None = None,
) -> SubAgent:
buckets = load_tools(dependencies=dependencies)
@ -51,5 +50,5 @@ def build_subagent(
tools=tools,
interrupt_on=interrupt_on,
model=model,
extra_middleware=extra_middleware,
middleware_stack=middleware_stack,
)

View file

@ -2,7 +2,6 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Any, Protocol
from deepagents import SubAgent
@ -14,6 +13,9 @@ from app.agents.multi_agent_chat.constants import (
from app.agents.multi_agent_chat.subagents.builtins.deliverables.agent import (
build_subagent as build_deliverables_subagent,
)
from app.agents.multi_agent_chat.subagents.builtins.knowledge_base.agent import (
build_subagent as build_knowledge_base_subagent,
)
from app.agents.multi_agent_chat.subagents.builtins.memory.agent import (
build_subagent as build_memory_subagent,
)
@ -79,7 +81,7 @@ class SubagentBuilder(Protocol):
*,
dependencies: dict[str, Any],
model: BaseChatModel | None = None,
extra_middleware: Sequence[Any] | None = None,
middleware_stack: dict[str, Any] | None = None,
extra_tools_bucket: ToolsPermissions | None = None,
) -> SubAgent: ...
@ -95,6 +97,7 @@ SUBAGENT_BUILDERS_BY_NAME: dict[str, SubagentBuilder] = {
"gmail": build_gmail_subagent,
"google_drive": build_google_drive_subagent,
"jira": build_jira_subagent,
"knowledge_base": build_knowledge_base_subagent,
"linear": build_linear_subagent,
"luma": build_luma_subagent,
"memory": build_memory_subagent,
@ -169,7 +172,7 @@ def build_subagents(
*,
dependencies: dict[str, Any],
model: BaseChatModel | None = None,
extra_middleware: Sequence[Any] | None = None,
middleware_stack: dict[str, Any] | None = None,
mcp_tools_by_agent: dict[str, ToolsPermissions] | None = None,
exclude: list[str] | None = None,
disabled_tools: list[str] | None = None,
@ -188,7 +191,7 @@ def build_subagents(
spec = builder(
dependencies=dependencies,
model=model,
extra_middleware=extra_middleware,
middleware_stack=middleware_stack,
extra_tools_bucket=mcp.get(name),
)
_filter_disabled_tools_in_place(spec, disabled_names)

View file

@ -2,7 +2,6 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Any, cast
from deepagents import SubAgent
@ -20,16 +19,22 @@ def pack_subagent(
system_prompt: str,
tools: list[BaseTool],
model: BaseChatModel | None = None,
extra_middleware: Sequence[Any] | None = None,
middleware_stack: dict[str, Any] | None = None,
interrupt_on: dict[str, bool] | None = None,
) -> SubAgent:
"""Pack the route-local pieces passed in into one sub-agent spec."""
"""Pack the route-local pieces passed in into one sub-agent spec.
``middleware_stack`` is the shared subagent middleware stack (see
``build_subagent_middleware_stack``). Every non-``None`` value is
prepended to this subagent's middleware list in insertion order.
"""
if not system_prompt.strip():
msg = f"Subagent {name!r}: system_prompt is empty"
raise ValueError(msg)
prepended = [m for m in (middleware_stack or {}).values() if m is not None]
middleware: list[Any] = [
*(extra_middleware or []),
*prepended,
PatchToolCallsMiddleware(),
DedupHITLToolCallsMiddleware(agent_tools=tools),
]