From 5a00df8e48c64e9ce7584f217fdd637b8e090e24 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Thu, 14 May 2026 20:09:55 +0200 Subject: [PATCH] multi_agent_chat/builtins: KB+deliverables+memory+research adopt RULESET + flat load_tools() --- .../subagents/builtins/deliverables/agent.py | 52 +++++------- .../builtins/deliverables/tools/index.py | 84 +++++++++---------- .../builtins/knowledge_base/agent.py | 9 +- .../knowledge_base/middleware_stack.py | 5 +- .../subagents/builtins/memory/agent.py | 47 ++++------- .../subagents/builtins/memory/tools/index.py | 48 ++++++----- .../subagents/builtins/research/agent.py | 47 ++++------- .../builtins/research/tools/index.py | 45 +++++----- 8 files changed, 141 insertions(+), 196 deletions(-) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/deliverables/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/deliverables/agent.py index d57480958..9fad9f93b 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/deliverables/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/deliverables/agent.py @@ -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 typing import Any -from deepagents import SubAgent 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 ( - middleware_gated_interrupt_on, -) -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 app.agents.multi_agent_chat.subagents.shared.md_file_reader import read_md_file +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 .tools.index import load_tools - -NAME = "deliverables" +from .tools.index import NAME, RULESET, load_tools def build_subagent( @@ -31,26 +24,21 @@ def build_subagent( dependencies: dict[str, Any], model: BaseChatModel | None = None, middleware_stack: dict[str, Any] | None = None, - extra_tools_bucket: ToolsPermissions | None = None, -) -> SubAgent: - buckets = load_tools(dependencies=dependencies) - merged_tools_bucket = merge_tools_permissions(buckets, extra_tools_bucket) - tools = [ - row["tool"] - 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." + mcp_tools: list[BaseTool] | None = None, +) -> SurfSenseSubagentSpec: + tools = [*load_tools(dependencies=dependencies), *(mcp_tools or [])] + description = ( + read_md_file(__package__, "description").strip() + or "Handles deliverables tasks for this workspace." + ) system_prompt = read_md_file(__package__, "system_prompt").strip() return pack_subagent( name=NAME, description=description, system_prompt=system_prompt, tools=tools, - interrupt_on=interrupt_on, + ruleset=RULESET, + flags=dependencies["flags"], model=model, middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/deliverables/tools/index.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/deliverables/tools/index.py index 3b5f1acab..5f76f1d52 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/deliverables/tools/index.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/deliverables/tools/index.py @@ -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 typing import Any -from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.self_gated import ( - self_gated_tool_permission_row, -) -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, -) +from langchain_core.tools import BaseTool + +from app.agents.new_chat.permissions import Ruleset from .generate_image import create_generate_image_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 .video_presentation import create_generate_video_presentation_tool +NAME = "deliverables" + +RULESET = Ruleset(origin=NAME, rules=[]) + def load_tools( *, dependencies: dict[str, Any] | None = None, **kwargs: Any -) -> ToolsPermissions: - resolved_dependencies = {**(dependencies or {}), **kwargs} - podcast = create_generate_podcast_tool( - search_space_id=resolved_dependencies["search_space_id"], - db_session=resolved_dependencies["db_session"], - thread_id=resolved_dependencies["thread_id"], - ) - video = create_generate_video_presentation_tool( - search_space_id=resolved_dependencies["search_space_id"], - db_session=resolved_dependencies["db_session"], - thread_id=resolved_dependencies["thread_id"], - ) - report = create_generate_report_tool( - search_space_id=resolved_dependencies["search_space_id"], - thread_id=resolved_dependencies["thread_id"], - connector_service=resolved_dependencies.get("connector_service"), - available_connectors=resolved_dependencies.get("available_connectors"), - available_document_types=resolved_dependencies.get("available_document_types"), - ) - resume = create_generate_resume_tool( - search_space_id=resolved_dependencies["search_space_id"], - thread_id=resolved_dependencies["thread_id"], - ) - image = create_generate_image_tool( - search_space_id=resolved_dependencies["search_space_id"], - db_session=resolved_dependencies["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": [], - } +) -> list[BaseTool]: + d = {**(dependencies or {}), **kwargs} + return [ + create_generate_podcast_tool( + search_space_id=d["search_space_id"], + db_session=d["db_session"], + thread_id=d["thread_id"], + ), + create_generate_video_presentation_tool( + search_space_id=d["search_space_id"], + db_session=d["db_session"], + thread_id=d["thread_id"], + ), + create_generate_report_tool( + search_space_id=d["search_space_id"], + thread_id=d["thread_id"], + connector_service=d.get("connector_service"), + available_connectors=d.get("available_connectors"), + available_document_types=d.get("available_document_types"), + ), + create_generate_resume_tool( + search_space_id=d["search_space_id"], + thread_id=d["thread_id"], + ), + create_generate_image_tool( + search_space_id=d["search_space_id"], + db_session=d["db_session"], + ), + ] diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/knowledge_base/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/knowledge_base/agent.py index ac1e9cdb7..d3ed2885f 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/knowledge_base/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/knowledge_base/agent.py @@ -2,8 +2,7 @@ KB owns its destructive-FS approval ruleset (:data:`KB_RULESET`); rules are layered into KB's :class:`PermissionMiddleware` (built inside -``build_kb_middleware``). The legacy ``interrupt_on`` kwarg is gone — one -emitter, one wire format, one source of truth. +``build_kb_middleware``). One emitter, one wire format, one source of truth. """ from __future__ import annotations @@ -12,9 +11,9 @@ from typing import Any, cast from deepagents import SubAgent 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.tool_kinds import ToolsPermissions from app.agents.new_chat.filesystem_selection import FilesystemMode from app.agents.new_chat.permissions import Rule, Ruleset @@ -38,9 +37,9 @@ def build_subagent( dependencies: dict[str, Any], model: BaseChatModel | None = None, middleware_stack: dict[str, Any] | None = None, - extra_tools_bucket: ToolsPermissions | None = None, + mcp_tools: list[BaseTool] | None = None, ) -> SurfSenseSubagentSpec: - del extra_tools_bucket + del mcp_tools llm = model if model is not None else dependencies["llm"] filesystem_mode: FilesystemMode = dependencies["filesystem_mode"] spec = cast( diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/knowledge_base/middleware_stack.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/knowledge_base/middleware_stack.py index 707ae535f..5f4256448 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/knowledge_base/middleware_stack.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/knowledge_base/middleware_stack.py @@ -1,8 +1,7 @@ """Middleware list shared by the full and read-only knowledge_base compiles. The KB-owned :class:`PermissionMiddleware` slot is what enforces -"ask before destructive FS op" for KB tools — replacing the legacy -``interrupt_on`` kwarg that used to live on the subagent spec. +"ask before destructive FS op" for KB tools. """ from __future__ import annotations @@ -47,7 +46,7 @@ def build_kb_middleware( ``ruleset`` is the KB-owned permission ruleset (typically the destructive-FS ask rules). When provided, a dedicated :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 {} filesystem_mode: FilesystemMode = dependencies["filesystem_mode"] diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/memory/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/memory/agent.py index f84546ca0..103bb0cfb 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/memory/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/memory/agent.py @@ -1,29 +1,17 @@ -"""`memory` route: ``SubAgent`` spec for deepagents.""" +"""``memory`` route: ``SurfSenseSubagentSpec`` builder for deepagents.""" from __future__ import annotations from typing import Any -from deepagents import SubAgent 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 ( - middleware_gated_interrupt_on, -) -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 app.agents.multi_agent_chat.subagents.shared.md_file_reader import read_md_file +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 .tools.index import load_tools - -NAME = "memory" +from .tools.index import NAME, RULESET, load_tools def build_subagent( @@ -31,26 +19,21 @@ def build_subagent( dependencies: dict[str, Any], model: BaseChatModel | None = None, middleware_stack: dict[str, Any] | None = None, - extra_tools_bucket: ToolsPermissions | None = None, -) -> SubAgent: - buckets = load_tools(dependencies=dependencies) - merged_tools_bucket = merge_tools_permissions(buckets, extra_tools_bucket) - tools = [ - row["tool"] - 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." + mcp_tools: list[BaseTool] | None = None, +) -> SurfSenseSubagentSpec: + tools = [*load_tools(dependencies=dependencies), *(mcp_tools or [])] + description = ( + read_md_file(__package__, "description").strip() + or "Handles memory tasks for this workspace." + ) system_prompt = read_md_file(__package__, "system_prompt").strip() return pack_subagent( name=NAME, description=description, system_prompt=system_prompt, tools=tools, - interrupt_on=interrupt_on, + ruleset=RULESET, + flags=dependencies["flags"], model=model, middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/memory/tools/index.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/memory/tools/index.py index e70bb8366..b6e06dcdd 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/memory/tools/index.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/memory/tools/index.py @@ -1,35 +1,37 @@ +"""``memory`` native tools and (empty) permission ruleset.""" + from __future__ import annotations from typing import Any -from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.self_gated import ( - self_gated_tool_permission_row, -) -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, -) +from langchain_core.tools import BaseTool + +from app.agents.new_chat.permissions import Ruleset from app.db import ChatVisibility from .update_memory import create_update_memory_tool, create_update_team_memory_tool +NAME = "memory" + +RULESET = Ruleset(origin=NAME, rules=[]) + def load_tools( *, dependencies: dict[str, Any] | None = None, **kwargs: Any -) -> ToolsPermissions: - resolved_dependencies = {**(dependencies or {}), **kwargs} - if resolved_dependencies.get("thread_visibility") == ChatVisibility.SEARCH_SPACE: - mem = create_update_team_memory_tool( - search_space_id=resolved_dependencies["search_space_id"], - db_session=resolved_dependencies["db_session"], - llm=resolved_dependencies.get("llm"), +) -> list[BaseTool]: + d = {**(dependencies or {}), **kwargs} + if d.get("thread_visibility") == ChatVisibility.SEARCH_SPACE: + return [ + create_update_team_memory_tool( + search_space_id=d["search_space_id"], + 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": []} + ] diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/research/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/research/agent.py index 715bc34fd..7b37b4228 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/research/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/research/agent.py @@ -1,29 +1,17 @@ -"""`research` route: ``SubAgent`` spec for deepagents.""" +"""``research`` route: ``SurfSenseSubagentSpec`` builder for deepagents.""" from __future__ import annotations from typing import Any -from deepagents import SubAgent 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 ( - middleware_gated_interrupt_on, -) -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 app.agents.multi_agent_chat.subagents.shared.md_file_reader import read_md_file +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 .tools.index import load_tools - -NAME = "research" +from .tools.index import NAME, RULESET, load_tools def build_subagent( @@ -31,26 +19,21 @@ def build_subagent( dependencies: dict[str, Any], model: BaseChatModel | None = None, middleware_stack: dict[str, Any] | None = None, - extra_tools_bucket: ToolsPermissions | None = None, -) -> SubAgent: - buckets = load_tools(dependencies=dependencies) - merged_tools_bucket = merge_tools_permissions(buckets, extra_tools_bucket) - tools = [ - row["tool"] - 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." + mcp_tools: list[BaseTool] | None = None, +) -> SurfSenseSubagentSpec: + tools = [*load_tools(dependencies=dependencies), *(mcp_tools or [])] + description = ( + read_md_file(__package__, "description").strip() + or "Handles research tasks for this workspace." + ) system_prompt = read_md_file(__package__, "system_prompt").strip() return pack_subagent( name=NAME, description=description, system_prompt=system_prompt, tools=tools, - interrupt_on=interrupt_on, + ruleset=RULESET, + flags=dependencies["flags"], model=model, middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/research/tools/index.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/research/tools/index.py index 4500fbdf8..ea544a8da 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/research/tools/index.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/research/tools/index.py @@ -1,38 +1,31 @@ +"""``research`` native tools and (empty) permission ruleset.""" + from __future__ import annotations from typing import Any -from app.agents.multi_agent_chat.subagents.shared.hitl.approvals.self_gated import ( - self_gated_tool_permission_row, -) -from app.agents.multi_agent_chat.subagents.shared.tool_kinds import ( - ToolsPermissions, -) +from langchain_core.tools import BaseTool + +from app.agents.new_chat.permissions import Ruleset from .scrape_webpage import create_scrape_webpage_tool from .search_surfsense_docs import create_search_surfsense_docs_tool from .web_search import create_web_search_tool +NAME = "research" + +RULESET = Ruleset(origin=NAME, rules=[]) + def load_tools( *, dependencies: dict[str, Any] | None = None, **kwargs: Any -) -> ToolsPermissions: - resolved_dependencies = {**(dependencies or {}), **kwargs} - web = create_web_search_tool( - search_space_id=resolved_dependencies.get("search_space_id"), - available_connectors=resolved_dependencies.get("available_connectors"), - ) - scrape = create_scrape_webpage_tool( - firecrawl_api_key=resolved_dependencies.get("firecrawl_api_key") - ) - 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": [], - } +) -> list[BaseTool]: + d = {**(dependencies or {}), **kwargs} + return [ + create_web_search_tool( + search_space_id=d.get("search_space_id"), + available_connectors=d.get("available_connectors"), + ), + create_scrape_webpage_tool(firecrawl_api_key=d.get("firecrawl_api_key")), + create_search_surfsense_docs_tool(db_session=d["db_session"]), + ]