mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-17 18:35:19 +02:00
multi_agent_chat: pack_subagent owns per-subagent PermissionMiddleware via Ruleset
This commit is contained in:
parent
67142e68b1
commit
d45dfbfbd6
4 changed files with 38 additions and 27 deletions
|
|
@ -12,7 +12,6 @@ from deepagents.middleware.subagents import (
|
||||||
SubAgentMiddleware,
|
SubAgentMiddleware,
|
||||||
)
|
)
|
||||||
from langchain.agents import create_agent
|
from langchain.agents import create_agent
|
||||||
from langchain.agents.middleware import HumanInTheLoopMiddleware
|
|
||||||
from langchain.chat_models import init_chat_model
|
from langchain.chat_models import init_chat_model
|
||||||
from langgraph.types import Checkpointer
|
from langgraph.types import Checkpointer
|
||||||
|
|
||||||
|
|
@ -81,10 +80,6 @@ class SurfSenseCheckpointedSubAgentMiddleware(SubAgentMiddleware):
|
||||||
|
|
||||||
middleware: list[Any] = list(spec.get("middleware", []))
|
middleware: list[Any] = list(spec.get("middleware", []))
|
||||||
|
|
||||||
interrupt_on = spec.get("interrupt_on")
|
|
||||||
if interrupt_on:
|
|
||||||
middleware.append(HumanInTheLoopMiddleware(interrupt_on=interrupt_on))
|
|
||||||
|
|
||||||
specs.append(
|
specs.append(
|
||||||
{
|
{
|
||||||
"name": spec["name"],
|
"name": spec["name"],
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@ matching OpenCode's ``permission/index.ts`` evaluation order):
|
||||||
needs to *deny* what the user has explicitly forbidden; the default
|
needs to *deny* what the user has explicitly forbidden; the default
|
||||||
``ask`` fallback would otherwise double-prompt every safe read-only
|
``ask`` fallback would otherwise double-prompt every safe read-only
|
||||||
call.
|
call.
|
||||||
2. ``extra_rulesets`` — caller-supplied rulesets. The KB subagent contributes
|
2. ``extra_rulesets`` — caller-supplied rulesets. Each subagent
|
||||||
its destructive-FS ``ask`` rules here; connectors will follow once
|
contributes its own (KB: destructive-FS ``ask`` rules; connectors:
|
||||||
they migrate off ``interrupt_on``.
|
per-tool ``allow``/``ask``).
|
||||||
|
|
||||||
Connector deny synthesis from ``new_chat._synthesize_connector_deny_rules``
|
Connector deny synthesis from ``new_chat._synthesize_connector_deny_rules``
|
||||||
is intentionally NOT replicated: the multi-agent orchestrator already
|
is intentionally NOT replicated: the multi-agent orchestrator already
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,12 @@ from deepagents.middleware.patch_tool_calls import PatchToolCallsMiddleware
|
||||||
from langchain_core.language_models import BaseChatModel
|
from langchain_core.language_models import BaseChatModel
|
||||||
from langchain_core.tools import BaseTool
|
from langchain_core.tools import BaseTool
|
||||||
|
|
||||||
from app.agents.new_chat.middleware import DedupHITLToolCallsMiddleware
|
from app.agents.multi_agent_chat.middleware.shared.permissions import (
|
||||||
|
build_permission_mw,
|
||||||
|
)
|
||||||
|
from app.agents.multi_agent_chat.subagents.shared.spec import SurfSenseSubagentSpec
|
||||||
|
from app.agents.new_chat.feature_flags import AgentFeatureFlags
|
||||||
|
from app.agents.new_chat.permissions import Ruleset
|
||||||
|
|
||||||
|
|
||||||
def pack_subagent(
|
def pack_subagent(
|
||||||
|
|
@ -18,27 +23,35 @@ def pack_subagent(
|
||||||
description: str,
|
description: str,
|
||||||
system_prompt: str,
|
system_prompt: str,
|
||||||
tools: list[BaseTool],
|
tools: list[BaseTool],
|
||||||
|
ruleset: Ruleset,
|
||||||
|
flags: AgentFeatureFlags,
|
||||||
model: BaseChatModel | None = None,
|
model: BaseChatModel | None = None,
|
||||||
middleware_stack: dict[str, Any] | None = None,
|
middleware_stack: dict[str, Any] | None = None,
|
||||||
interrupt_on: dict[str, bool] | None = None,
|
) -> SurfSenseSubagentSpec:
|
||||||
) -> SubAgent:
|
"""Pack the route-local pieces into one sub-agent spec + its Ruleset.
|
||||||
"""Pack the route-local pieces passed in into one sub-agent spec.
|
|
||||||
|
|
||||||
``middleware_stack`` is the shared subagent middleware stack (see
|
Tool gating is uniformly performed by a per-subagent
|
||||||
``build_subagent_middleware_stack``). Every non-``None`` value is
|
:class:`PermissionMiddleware` built from the subagent's own
|
||||||
prepended to this subagent's middleware list in insertion order.
|
``ruleset`` (layered on top of the SurfSense defaults). The shared
|
||||||
|
``permission`` slot from ``middleware_stack`` is dropped so each
|
||||||
|
subagent owns its own rule surface.
|
||||||
"""
|
"""
|
||||||
if not system_prompt.strip():
|
if not system_prompt.strip():
|
||||||
msg = f"Subagent {name!r}: system_prompt is empty"
|
msg = f"Subagent {name!r}: system_prompt is empty"
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
prepended = [m for m in (middleware_stack or {}).values() if m is not None]
|
per_subagent_perm = build_permission_mw(flags=flags, extra_rulesets=[ruleset])
|
||||||
middleware: list[Any] = [
|
prepended: list[Any] = []
|
||||||
*prepended,
|
for slot, mw in (middleware_stack or {}).items():
|
||||||
PatchToolCallsMiddleware(),
|
if mw is None:
|
||||||
DedupHITLToolCallsMiddleware(agent_tools=tools),
|
continue
|
||||||
]
|
if slot == "permission":
|
||||||
spec: dict[str, Any] = {
|
continue
|
||||||
|
prepended.append(mw)
|
||||||
|
if per_subagent_perm is not None:
|
||||||
|
prepended.append(per_subagent_perm)
|
||||||
|
middleware: list[Any] = [*prepended, PatchToolCallsMiddleware()]
|
||||||
|
spec_dict: dict[str, Any] = {
|
||||||
"name": name,
|
"name": name,
|
||||||
"description": description,
|
"description": description,
|
||||||
"system_prompt": system_prompt,
|
"system_prompt": system_prompt,
|
||||||
|
|
@ -46,7 +59,5 @@ def pack_subagent(
|
||||||
"middleware": middleware,
|
"middleware": middleware,
|
||||||
}
|
}
|
||||||
if model is not None:
|
if model is not None:
|
||||||
spec["model"] = model
|
spec_dict["model"] = model
|
||||||
if interrupt_on:
|
return SurfSenseSubagentSpec(spec=cast(SubAgent, spec_dict), ruleset=ruleset)
|
||||||
spec["interrupt_on"] = interrupt_on
|
|
||||||
return cast(SubAgent, spec)
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ from langchain_core.outputs import ChatGeneration, ChatResult
|
||||||
from app.agents.multi_agent_chat.subagents.shared.subagent_builder import (
|
from app.agents.multi_agent_chat.subagents.shared.subagent_builder import (
|
||||||
pack_subagent,
|
pack_subagent,
|
||||||
)
|
)
|
||||||
|
from app.agents.new_chat.feature_flags import AgentFeatureFlags
|
||||||
|
from app.agents.new_chat.permissions import Ruleset
|
||||||
|
|
||||||
|
|
||||||
class RateLimitError(Exception):
|
class RateLimitError(Exception):
|
||||||
|
|
@ -73,14 +75,17 @@ async def test_subagent_recovers_when_primary_llm_fails():
|
||||||
responses=[AIMessage(content="recovered via fallback")]
|
responses=[AIMessage(content="recovered via fallback")]
|
||||||
)
|
)
|
||||||
|
|
||||||
spec = pack_subagent(
|
result = pack_subagent(
|
||||||
name="resilience_test",
|
name="resilience_test",
|
||||||
description="test subagent",
|
description="test subagent",
|
||||||
system_prompt="be helpful",
|
system_prompt="be helpful",
|
||||||
tools=[],
|
tools=[],
|
||||||
|
ruleset=Ruleset(origin="resilience_test", rules=[]),
|
||||||
|
flags=AgentFeatureFlags(),
|
||||||
model=primary,
|
model=primary,
|
||||||
middleware_stack={"fallback": ModelFallbackMiddleware(fallback)},
|
middleware_stack={"fallback": ModelFallbackMiddleware(fallback)},
|
||||||
)
|
)
|
||||||
|
spec = result.spec
|
||||||
|
|
||||||
agent = create_agent(
|
agent = create_agent(
|
||||||
model=spec["model"],
|
model=spec["model"],
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue