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

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

View file

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

View file

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

View file

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

View file

@ -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": []}
]

View file

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

View file

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