mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-06 22:32:39 +02:00
feat(subagents): add shared subagent utility helpers.
This commit is contained in:
parent
8d0a679805
commit
755d20f6ea
1 changed files with 143 additions and 0 deletions
143
surfsense_backend/app/agents/multi_agent_v1/subagents/utils.py
Normal file
143
surfsense_backend/app/agents/multi_agent_v1/subagents/utils.py
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
"""Shared helpers for multi-agent v1 subagents."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.agents.new_chat.llm_config import (
|
||||||
|
create_chat_litellm_from_agent_config,
|
||||||
|
load_agent_config,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def load_llm_for_request(
|
||||||
|
*,
|
||||||
|
session: AsyncSession,
|
||||||
|
llm_config_id: int,
|
||||||
|
search_space_id: int,
|
||||||
|
) -> Any | None:
|
||||||
|
"""Load the configured chat model for a subagent run."""
|
||||||
|
agent_config = await load_agent_config(
|
||||||
|
session=session,
|
||||||
|
config_id=llm_config_id,
|
||||||
|
search_space_id=search_space_id,
|
||||||
|
)
|
||||||
|
if agent_config is None:
|
||||||
|
return None
|
||||||
|
return create_chat_litellm_from_agent_config(agent_config)
|
||||||
|
|
||||||
|
|
||||||
|
def build_subagent_input_state(
|
||||||
|
*,
|
||||||
|
goal: str,
|
||||||
|
stream_kwargs: dict[str, Any],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Build the initial state payload passed to a subagent."""
|
||||||
|
return {
|
||||||
|
"messages": [HumanMessage(content=goal)],
|
||||||
|
"search_space_id": stream_kwargs["search_space_id"],
|
||||||
|
"request_id": read_optional_nonempty_string(
|
||||||
|
stream_kwargs, "request_id", "unknown"
|
||||||
|
),
|
||||||
|
"turn_id": f"subagent:{read_optional_integer(stream_kwargs, 'chat_id') or 'unknown'}",
|
||||||
|
"architecture_mode": "multi_agent_v1_subagent",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_subagent_run_config(
|
||||||
|
*,
|
||||||
|
stream_kwargs: dict[str, Any],
|
||||||
|
scope: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Build runnable config with a scope-specific thread id."""
|
||||||
|
return {
|
||||||
|
"configurable": {
|
||||||
|
"thread_id": build_subagent_thread_id(stream_kwargs=stream_kwargs, scope=scope),
|
||||||
|
"request_id": read_optional_nonempty_string(
|
||||||
|
stream_kwargs, "request_id", "unknown"
|
||||||
|
),
|
||||||
|
"turn_id": f"subagent:{read_optional_integer(stream_kwargs, 'chat_id') or 'unknown'}",
|
||||||
|
"architecture_mode": "multi_agent_v1_subagent",
|
||||||
|
},
|
||||||
|
"recursion_limit": 40,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_subagent_error_result(error_class: str) -> dict[str, Any]:
|
||||||
|
"""Build a standardized error result payload for subagents."""
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"summary": "",
|
||||||
|
"evidence": [],
|
||||||
|
"artifacts": [],
|
||||||
|
"needs_human": False,
|
||||||
|
"error_class": error_class,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def extract_final_ai_message_text_from_state(state: Any) -> str:
|
||||||
|
"""Return the latest AI message text from an agent state payload."""
|
||||||
|
if not isinstance(state, dict):
|
||||||
|
return ""
|
||||||
|
messages = state.get("messages")
|
||||||
|
if not isinstance(messages, Sequence):
|
||||||
|
return ""
|
||||||
|
for message in reversed(messages):
|
||||||
|
if isinstance(message, AIMessage):
|
||||||
|
return extract_plain_text_from_message_content(message).strip()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def extract_plain_text_from_message_content(message: BaseMessage) -> str:
|
||||||
|
"""Flatten a LangChain message content payload into plain text."""
|
||||||
|
content = getattr(message, "content", "")
|
||||||
|
if isinstance(content, str):
|
||||||
|
return content
|
||||||
|
if isinstance(content, list):
|
||||||
|
parts: list[str] = []
|
||||||
|
for item in content:
|
||||||
|
if isinstance(item, str):
|
||||||
|
parts.append(item)
|
||||||
|
elif isinstance(item, dict) and item.get("type") == "text":
|
||||||
|
parts.append(str(item.get("text", "")))
|
||||||
|
return "\n".join(part for part in parts if part)
|
||||||
|
return str(content)
|
||||||
|
|
||||||
|
|
||||||
|
def build_disabled_tools_list(disabled_tools: Any) -> list[str]:
|
||||||
|
"""Normalize disabled tools input to a list of tool names."""
|
||||||
|
if not isinstance(disabled_tools, list):
|
||||||
|
return []
|
||||||
|
return [tool_name for tool_name in disabled_tools if isinstance(tool_name, str)]
|
||||||
|
|
||||||
|
|
||||||
|
def read_optional_nonempty_string(
|
||||||
|
payload: dict[str, Any],
|
||||||
|
key: str,
|
||||||
|
default: str | None = None,
|
||||||
|
) -> str | None:
|
||||||
|
"""Read a non-empty string from payload, otherwise return default."""
|
||||||
|
value = payload.get(key)
|
||||||
|
if isinstance(value, str) and value.strip():
|
||||||
|
return value.strip()
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def read_optional_integer(payload: dict[str, Any], key: str) -> int | None:
|
||||||
|
"""Read an integer from payload when present and valid."""
|
||||||
|
value = payload.get(key)
|
||||||
|
if isinstance(value, int):
|
||||||
|
return value
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def build_subagent_thread_id(*, stream_kwargs: dict[str, Any], scope: str) -> str:
|
||||||
|
"""Build a stable thread id for a scope-specific subagent run."""
|
||||||
|
chat_id = read_optional_integer(stream_kwargs, "chat_id")
|
||||||
|
if chat_id is None:
|
||||||
|
return "ma-subagent:unknown"
|
||||||
|
return f"ma-subagent:{chat_id}:{scope}"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue