multi_agent_chat/builtins: KB+deliverables+memory+research adopt RULESET + flat load_tools()

This commit is contained in:
CREDO23 2026-05-14 20:09:55 +02:00
parent 3bb90124d2
commit 5a00df8e48
8 changed files with 141 additions and 196 deletions

View file

@ -1,29 +1,22 @@
"""`deliverables` route: ``SubAgent`` spec for deepagents.""" """``deliverables`` route: ``SurfSenseSubagentSpec`` builder for deepagents.
Tools self-gate inside their bodies via :func:`request_approval`; the
empty :data:`tools.index.RULESET` is layered into a per-subagent
:class:`PermissionMiddleware` for uniformity.
"""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any
from deepagents import SubAgent
from langchain_core.language_models import BaseChatModel from langchain_core.language_models import BaseChatModel
from langchain_core.tools import BaseTool
from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.middleware_gated import ( from app.agents.multi_agent_chat.subagents.shared.md_file_reader import read_md_file
middleware_gated_interrupt_on, from app.agents.multi_agent_chat.subagents.shared.spec import SurfSenseSubagentSpec
) from app.agents.multi_agent_chat.subagents.shared.subagent_builder import pack_subagent
from app.agents.multi_agent_chat.subagents.shared.md_file_reader import (
read_md_file,
)
from app.agents.multi_agent_chat.subagents.shared.subagent_builder import (
pack_subagent,
)
from app.agents.multi_agent_chat.subagents.shared.tool_kinds import (
ToolsPermissions,
merge_tools_permissions,
)
from .tools.index import load_tools from .tools.index import NAME, RULESET, load_tools
NAME = "deliverables"
def build_subagent( def build_subagent(
@ -31,26 +24,21 @@ def build_subagent(
dependencies: dict[str, Any], dependencies: dict[str, Any],
model: BaseChatModel | None = None, model: BaseChatModel | None = None,
middleware_stack: dict[str, Any] | None = None, middleware_stack: dict[str, Any] | None = None,
extra_tools_bucket: ToolsPermissions | None = None, mcp_tools: list[BaseTool] | None = None,
) -> SubAgent: ) -> SurfSenseSubagentSpec:
buckets = load_tools(dependencies=dependencies) tools = [*load_tools(dependencies=dependencies), *(mcp_tools or [])]
merged_tools_bucket = merge_tools_permissions(buckets, extra_tools_bucket) description = (
tools = [ read_md_file(__package__, "description").strip()
row["tool"] or "Handles deliverables tasks for this workspace."
for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"]) )
if row.get("tool") is not None
]
interrupt_on = middleware_gated_interrupt_on(merged_tools_bucket)
description = read_md_file(__package__, "description").strip()
if not description:
description = "Handles deliverables tasks for this workspace."
system_prompt = read_md_file(__package__, "system_prompt").strip() system_prompt = read_md_file(__package__, "system_prompt").strip()
return pack_subagent( return pack_subagent(
name=NAME, name=NAME,
description=description, description=description,
system_prompt=system_prompt, system_prompt=system_prompt,
tools=tools, tools=tools,
interrupt_on=interrupt_on, ruleset=RULESET,
flags=dependencies["flags"],
model=model, model=model,
middleware_stack=middleware_stack, middleware_stack=middleware_stack,
) )

View file

@ -1,13 +1,15 @@
"""``deliverables`` native tools and (empty) permission ruleset.
Tools self-gate via :func:`request_approval` in their bodies.
"""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any
from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.self_gated import ( from langchain_core.tools import BaseTool
self_gated_tool_permission_row,
) from app.agents.new_chat.permissions import Ruleset
from app.agents.multi_agent_chat.subagents.shared.tool_kinds import (
ToolsPermissions,
)
from .generate_image import create_generate_image_tool from .generate_image import create_generate_image_tool
from .podcast import create_generate_podcast_tool from .podcast import create_generate_podcast_tool
@ -15,43 +17,39 @@ from .report import create_generate_report_tool
from .resume import create_generate_resume_tool from .resume import create_generate_resume_tool
from .video_presentation import create_generate_video_presentation_tool from .video_presentation import create_generate_video_presentation_tool
NAME = "deliverables"
RULESET = Ruleset(origin=NAME, rules=[])
def load_tools( def load_tools(
*, dependencies: dict[str, Any] | None = None, **kwargs: Any *, dependencies: dict[str, Any] | None = None, **kwargs: Any
) -> ToolsPermissions: ) -> list[BaseTool]:
resolved_dependencies = {**(dependencies or {}), **kwargs} d = {**(dependencies or {}), **kwargs}
podcast = create_generate_podcast_tool( return [
search_space_id=resolved_dependencies["search_space_id"], create_generate_podcast_tool(
db_session=resolved_dependencies["db_session"], search_space_id=d["search_space_id"],
thread_id=resolved_dependencies["thread_id"], db_session=d["db_session"],
) thread_id=d["thread_id"],
video = create_generate_video_presentation_tool( ),
search_space_id=resolved_dependencies["search_space_id"], create_generate_video_presentation_tool(
db_session=resolved_dependencies["db_session"], search_space_id=d["search_space_id"],
thread_id=resolved_dependencies["thread_id"], db_session=d["db_session"],
) thread_id=d["thread_id"],
report = create_generate_report_tool( ),
search_space_id=resolved_dependencies["search_space_id"], create_generate_report_tool(
thread_id=resolved_dependencies["thread_id"], search_space_id=d["search_space_id"],
connector_service=resolved_dependencies.get("connector_service"), thread_id=d["thread_id"],
available_connectors=resolved_dependencies.get("available_connectors"), connector_service=d.get("connector_service"),
available_document_types=resolved_dependencies.get("available_document_types"), available_connectors=d.get("available_connectors"),
) available_document_types=d.get("available_document_types"),
resume = create_generate_resume_tool( ),
search_space_id=resolved_dependencies["search_space_id"], create_generate_resume_tool(
thread_id=resolved_dependencies["thread_id"], search_space_id=d["search_space_id"],
) thread_id=d["thread_id"],
image = create_generate_image_tool( ),
search_space_id=resolved_dependencies["search_space_id"], create_generate_image_tool(
db_session=resolved_dependencies["db_session"], search_space_id=d["search_space_id"],
) db_session=d["db_session"],
return { ),
"allow": [ ]
self_gated_tool_permission_row(podcast),
self_gated_tool_permission_row(video),
self_gated_tool_permission_row(report),
self_gated_tool_permission_row(resume),
self_gated_tool_permission_row(image),
],
"ask": [],
}

View file

@ -2,8 +2,7 @@
KB owns its destructive-FS approval ruleset (:data:`KB_RULESET`); rules KB owns its destructive-FS approval ruleset (:data:`KB_RULESET`); rules
are layered into KB's :class:`PermissionMiddleware` (built inside are layered into KB's :class:`PermissionMiddleware` (built inside
``build_kb_middleware``). The legacy ``interrupt_on`` kwarg is gone one ``build_kb_middleware``). One emitter, one wire format, one source of truth.
emitter, one wire format, one source of truth.
""" """
from __future__ import annotations from __future__ import annotations
@ -12,9 +11,9 @@ from typing import Any, cast
from deepagents import SubAgent from deepagents import SubAgent
from langchain_core.language_models import BaseChatModel from langchain_core.language_models import BaseChatModel
from langchain_core.tools import BaseTool
from app.agents.multi_agent_chat.subagents.shared.spec import SurfSenseSubagentSpec from app.agents.multi_agent_chat.subagents.shared.spec import SurfSenseSubagentSpec
from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ToolsPermissions
from app.agents.new_chat.filesystem_selection import FilesystemMode from app.agents.new_chat.filesystem_selection import FilesystemMode
from app.agents.new_chat.permissions import Rule, Ruleset from app.agents.new_chat.permissions import Rule, Ruleset
@ -38,9 +37,9 @@ def build_subagent(
dependencies: dict[str, Any], dependencies: dict[str, Any],
model: BaseChatModel | None = None, model: BaseChatModel | None = None,
middleware_stack: dict[str, Any] | None = None, middleware_stack: dict[str, Any] | None = None,
extra_tools_bucket: ToolsPermissions | None = None, mcp_tools: list[BaseTool] | None = None,
) -> SurfSenseSubagentSpec: ) -> SurfSenseSubagentSpec:
del extra_tools_bucket del mcp_tools
llm = model if model is not None else dependencies["llm"] llm = model if model is not None else dependencies["llm"]
filesystem_mode: FilesystemMode = dependencies["filesystem_mode"] filesystem_mode: FilesystemMode = dependencies["filesystem_mode"]
spec = cast( spec = cast(

View file

@ -1,8 +1,7 @@
"""Middleware list shared by the full and read-only knowledge_base compiles. """Middleware list shared by the full and read-only knowledge_base compiles.
The KB-owned :class:`PermissionMiddleware` slot is what enforces The KB-owned :class:`PermissionMiddleware` slot is what enforces
"ask before destructive FS op" for KB tools replacing the legacy "ask before destructive FS op" for KB tools.
``interrupt_on`` kwarg that used to live on the subagent spec.
""" """
from __future__ import annotations from __future__ import annotations
@ -47,7 +46,7 @@ def build_kb_middleware(
``ruleset`` is the KB-owned permission ruleset (typically the ``ruleset`` is the KB-owned permission ruleset (typically the
destructive-FS ask rules). When provided, a dedicated destructive-FS ask rules). When provided, a dedicated
:class:`PermissionMiddleware` is appended so KB enforces approval at :class:`PermissionMiddleware` is appended so KB enforces approval at
the rule layer instead of the legacy ``interrupt_on`` kwarg. the rule layer.
""" """
mws = middleware_stack or {} mws = middleware_stack or {}
filesystem_mode: FilesystemMode = dependencies["filesystem_mode"] filesystem_mode: FilesystemMode = dependencies["filesystem_mode"]

View file

@ -1,29 +1,17 @@
"""`memory` route: ``SubAgent`` spec for deepagents.""" """``memory`` route: ``SurfSenseSubagentSpec`` builder for deepagents."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any
from deepagents import SubAgent
from langchain_core.language_models import BaseChatModel from langchain_core.language_models import BaseChatModel
from langchain_core.tools import BaseTool
from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.middleware_gated import ( from app.agents.multi_agent_chat.subagents.shared.md_file_reader import read_md_file
middleware_gated_interrupt_on, from app.agents.multi_agent_chat.subagents.shared.spec import SurfSenseSubagentSpec
) from app.agents.multi_agent_chat.subagents.shared.subagent_builder import pack_subagent
from app.agents.multi_agent_chat.subagents.shared.md_file_reader import (
read_md_file,
)
from app.agents.multi_agent_chat.subagents.shared.subagent_builder import (
pack_subagent,
)
from app.agents.multi_agent_chat.subagents.shared.tool_kinds import (
ToolsPermissions,
merge_tools_permissions,
)
from .tools.index import load_tools from .tools.index import NAME, RULESET, load_tools
NAME = "memory"
def build_subagent( def build_subagent(
@ -31,26 +19,21 @@ def build_subagent(
dependencies: dict[str, Any], dependencies: dict[str, Any],
model: BaseChatModel | None = None, model: BaseChatModel | None = None,
middleware_stack: dict[str, Any] | None = None, middleware_stack: dict[str, Any] | None = None,
extra_tools_bucket: ToolsPermissions | None = None, mcp_tools: list[BaseTool] | None = None,
) -> SubAgent: ) -> SurfSenseSubagentSpec:
buckets = load_tools(dependencies=dependencies) tools = [*load_tools(dependencies=dependencies), *(mcp_tools or [])]
merged_tools_bucket = merge_tools_permissions(buckets, extra_tools_bucket) description = (
tools = [ read_md_file(__package__, "description").strip()
row["tool"] or "Handles memory tasks for this workspace."
for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"]) )
if row.get("tool") is not None
]
interrupt_on = middleware_gated_interrupt_on(merged_tools_bucket)
description = read_md_file(__package__, "description").strip()
if not description:
description = "Handles memory tasks for this workspace."
system_prompt = read_md_file(__package__, "system_prompt").strip() system_prompt = read_md_file(__package__, "system_prompt").strip()
return pack_subagent( return pack_subagent(
name=NAME, name=NAME,
description=description, description=description,
system_prompt=system_prompt, system_prompt=system_prompt,
tools=tools, tools=tools,
interrupt_on=interrupt_on, ruleset=RULESET,
flags=dependencies["flags"],
model=model, model=model,
middleware_stack=middleware_stack, middleware_stack=middleware_stack,
) )

View file

@ -1,35 +1,37 @@
"""``memory`` native tools and (empty) permission ruleset."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any
from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.self_gated import ( from langchain_core.tools import BaseTool
self_gated_tool_permission_row,
) from app.agents.new_chat.permissions import Ruleset
from app.agents.multi_agent_chat.subagents.shared.tool_kinds import (
ToolsPermissions,
)
from app.db import ChatVisibility from app.db import ChatVisibility
from .update_memory import create_update_memory_tool, create_update_team_memory_tool from .update_memory import create_update_memory_tool, create_update_team_memory_tool
NAME = "memory"
RULESET = Ruleset(origin=NAME, rules=[])
def load_tools( def load_tools(
*, dependencies: dict[str, Any] | None = None, **kwargs: Any *, dependencies: dict[str, Any] | None = None, **kwargs: Any
) -> ToolsPermissions: ) -> list[BaseTool]:
resolved_dependencies = {**(dependencies or {}), **kwargs} d = {**(dependencies or {}), **kwargs}
if resolved_dependencies.get("thread_visibility") == ChatVisibility.SEARCH_SPACE: if d.get("thread_visibility") == ChatVisibility.SEARCH_SPACE:
mem = create_update_team_memory_tool( return [
search_space_id=resolved_dependencies["search_space_id"], create_update_team_memory_tool(
db_session=resolved_dependencies["db_session"], search_space_id=d["search_space_id"],
llm=resolved_dependencies.get("llm"), db_session=d["db_session"],
llm=d.get("llm"),
)
]
return [
create_update_memory_tool(
user_id=d["user_id"],
db_session=d["db_session"],
llm=d.get("llm"),
) )
return { ]
"allow": [self_gated_tool_permission_row(mem)],
"ask": [],
}
mem = create_update_memory_tool(
user_id=resolved_dependencies["user_id"],
db_session=resolved_dependencies["db_session"],
llm=resolved_dependencies.get("llm"),
)
return {"allow": [self_gated_tool_permission_row(mem)], "ask": []}

View file

@ -1,29 +1,17 @@
"""`research` route: ``SubAgent`` spec for deepagents.""" """``research`` route: ``SurfSenseSubagentSpec`` builder for deepagents."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any
from deepagents import SubAgent
from langchain_core.language_models import BaseChatModel from langchain_core.language_models import BaseChatModel
from langchain_core.tools import BaseTool
from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.middleware_gated import ( from app.agents.multi_agent_chat.subagents.shared.md_file_reader import read_md_file
middleware_gated_interrupt_on, from app.agents.multi_agent_chat.subagents.shared.spec import SurfSenseSubagentSpec
) from app.agents.multi_agent_chat.subagents.shared.subagent_builder import pack_subagent
from app.agents.multi_agent_chat.subagents.shared.md_file_reader import (
read_md_file,
)
from app.agents.multi_agent_chat.subagents.shared.subagent_builder import (
pack_subagent,
)
from app.agents.multi_agent_chat.subagents.shared.tool_kinds import (
ToolsPermissions,
merge_tools_permissions,
)
from .tools.index import load_tools from .tools.index import NAME, RULESET, load_tools
NAME = "research"
def build_subagent( def build_subagent(
@ -31,26 +19,21 @@ def build_subagent(
dependencies: dict[str, Any], dependencies: dict[str, Any],
model: BaseChatModel | None = None, model: BaseChatModel | None = None,
middleware_stack: dict[str, Any] | None = None, middleware_stack: dict[str, Any] | None = None,
extra_tools_bucket: ToolsPermissions | None = None, mcp_tools: list[BaseTool] | None = None,
) -> SubAgent: ) -> SurfSenseSubagentSpec:
buckets = load_tools(dependencies=dependencies) tools = [*load_tools(dependencies=dependencies), *(mcp_tools or [])]
merged_tools_bucket = merge_tools_permissions(buckets, extra_tools_bucket) description = (
tools = [ read_md_file(__package__, "description").strip()
row["tool"] or "Handles research tasks for this workspace."
for row in (*merged_tools_bucket["allow"], *merged_tools_bucket["ask"]) )
if row.get("tool") is not None
]
interrupt_on = middleware_gated_interrupt_on(merged_tools_bucket)
description = read_md_file(__package__, "description").strip()
if not description:
description = "Handles research tasks for this workspace."
system_prompt = read_md_file(__package__, "system_prompt").strip() system_prompt = read_md_file(__package__, "system_prompt").strip()
return pack_subagent( return pack_subagent(
name=NAME, name=NAME,
description=description, description=description,
system_prompt=system_prompt, system_prompt=system_prompt,
tools=tools, tools=tools,
interrupt_on=interrupt_on, ruleset=RULESET,
flags=dependencies["flags"],
model=model, model=model,
middleware_stack=middleware_stack, middleware_stack=middleware_stack,
) )

View file

@ -1,38 +1,31 @@
"""``research`` native tools and (empty) permission ruleset."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any
from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.self_gated import ( from langchain_core.tools import BaseTool
self_gated_tool_permission_row,
) from app.agents.new_chat.permissions import Ruleset
from app.agents.multi_agent_chat.subagents.shared.tool_kinds import (
ToolsPermissions,
)
from .scrape_webpage import create_scrape_webpage_tool from .scrape_webpage import create_scrape_webpage_tool
from .search_surfsense_docs import create_search_surfsense_docs_tool from .search_surfsense_docs import create_search_surfsense_docs_tool
from .web_search import create_web_search_tool from .web_search import create_web_search_tool
NAME = "research"
RULESET = Ruleset(origin=NAME, rules=[])
def load_tools( def load_tools(
*, dependencies: dict[str, Any] | None = None, **kwargs: Any *, dependencies: dict[str, Any] | None = None, **kwargs: Any
) -> ToolsPermissions: ) -> list[BaseTool]:
resolved_dependencies = {**(dependencies or {}), **kwargs} d = {**(dependencies or {}), **kwargs}
web = create_web_search_tool( return [
search_space_id=resolved_dependencies.get("search_space_id"), create_web_search_tool(
available_connectors=resolved_dependencies.get("available_connectors"), search_space_id=d.get("search_space_id"),
) available_connectors=d.get("available_connectors"),
scrape = create_scrape_webpage_tool( ),
firecrawl_api_key=resolved_dependencies.get("firecrawl_api_key") create_scrape_webpage_tool(firecrawl_api_key=d.get("firecrawl_api_key")),
) create_search_surfsense_docs_tool(db_session=d["db_session"]),
docs = create_search_surfsense_docs_tool( ]
db_session=resolved_dependencies["db_session"]
)
return {
"allow": [
self_gated_tool_permission_row(web),
self_gated_tool_permission_row(scrape),
self_gated_tool_permission_row(docs),
],
"ask": [],
}