mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-06-14 11:25:59 +00:00
f43aa78107
* fix(agents): sync agent_name across context/configurable and reject empty soul (#3549) Two independent issues caused custom agent creation to silently fail: 1. build_run_config only wrote agent_name into one container (configurable or context), so setup_agent — which reads ToolRuntime.context exclusively since LangGraph >=1.1.9 — saw agent_name=None and wrote SOUL.md to the global base_dir instead of users/{user_id}/agents/{name}/. Mirror the dual-write pattern already used by merge_run_context_overrides and naming.py so both containers always carry the same value. 2. setup_agent persisted whatever soul string it received, including empty or whitespace-only content, and still reported success. The frontend then surfaced an unusable agent and the global default SOUL.md could be silently overwritten with empty content. Reject empty soul before any filesystem operation so the model can retry. Tests: - test_gateway_services.py: dual-write regressions for both configurable and context entry paths, explicit-agent-name precedence on both sides, and a shape-parity test against merge_run_context_overrides. - test_setup_agent_tool.py: empty/whitespace soul rejection, plus no-overwrite guarantees for existing global and per-agent SOUL.md. * Update services.py
99 lines
3.9 KiB
Python
99 lines
3.9 KiB
Python
import logging
|
|
|
|
import yaml
|
|
from langchain_core.messages import ToolMessage
|
|
from langchain_core.tools import tool
|
|
from langgraph.types import Command
|
|
|
|
from deerflow.config.agents_config import validate_agent_name
|
|
from deerflow.config.paths import get_paths
|
|
from deerflow.runtime.user_context import resolve_runtime_user_id
|
|
from deerflow.tools.types import Runtime
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@tool(parse_docstring=True)
|
|
def setup_agent(
|
|
soul: str,
|
|
description: str,
|
|
runtime: Runtime,
|
|
skills: list[str] | None = None,
|
|
) -> Command:
|
|
"""Setup the custom DeerFlow agent.
|
|
|
|
Args:
|
|
soul: Full SOUL.md content defining the agent's personality and behavior.
|
|
description: One-line description of what the agent does.
|
|
skills: Optional list of skill names this agent should use. None means use all enabled skills, empty list means no skills.
|
|
"""
|
|
|
|
# Reject empty / whitespace-only soul before touching the filesystem.
|
|
# Without this guard the tool would happily persist an empty SOUL.md and
|
|
# still report success, which caused the frontend to enter the "agent
|
|
# created" state for an unusable agent (issue #3549). Failing loud lets
|
|
# the model retry instead of silently producing a broken artifact and,
|
|
# together with the upstream agent_name fix, prevents the global default
|
|
# SOUL.md from being overwritten with empty content.
|
|
if not soul or not soul.strip():
|
|
return Command(
|
|
update={
|
|
"messages": [
|
|
ToolMessage(
|
|
content="Error: soul content is empty; refusing to create agent with an empty SOUL.md",
|
|
tool_call_id=runtime.tool_call_id,
|
|
)
|
|
]
|
|
}
|
|
)
|
|
|
|
agent_name: str | None = runtime.context.get("agent_name") if runtime.context else None
|
|
agent_dir = None
|
|
is_new_dir = False
|
|
|
|
try:
|
|
agent_name = validate_agent_name(agent_name)
|
|
paths = get_paths()
|
|
if agent_name:
|
|
# Custom agents are persisted under the current user's bucket so
|
|
# different users do not see each other's agents.
|
|
user_id = resolve_runtime_user_id(runtime)
|
|
agent_dir = paths.user_agent_dir(user_id, agent_name)
|
|
else:
|
|
# Default agent (no agent_name): SOUL.md lives at the global base dir.
|
|
agent_dir = paths.base_dir
|
|
is_new_dir = not agent_dir.exists()
|
|
agent_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
if agent_name:
|
|
# If agent_name is provided, we are creating a custom agent in the agents/ directory
|
|
config_data: dict = {"name": agent_name}
|
|
if description:
|
|
config_data["description"] = description
|
|
if skills is not None:
|
|
config_data["skills"] = skills
|
|
|
|
config_file = agent_dir / "config.yaml"
|
|
with open(config_file, "w", encoding="utf-8") as f:
|
|
yaml.dump(config_data, f, default_flow_style=False, allow_unicode=True)
|
|
|
|
soul_file = agent_dir / "SOUL.md"
|
|
soul_file.write_text(soul, encoding="utf-8")
|
|
|
|
logger.info(f"[agent_creator] Created agent '{agent_name}' at {agent_dir}")
|
|
return Command(
|
|
update={
|
|
"created_agent_name": agent_name,
|
|
"messages": [ToolMessage(content=f"Agent '{agent_name}' created successfully!", tool_call_id=runtime.tool_call_id)],
|
|
}
|
|
)
|
|
|
|
except Exception as e:
|
|
import shutil
|
|
|
|
if agent_name and is_new_dir and agent_dir is not None and agent_dir.exists():
|
|
# Cleanup the custom agent directory only if it was newly created during this call
|
|
shutil.rmtree(agent_dir)
|
|
logger.error(f"[agent_creator] Failed to create agent '{agent_name}': {e}", exc_info=True)
|
|
return Command(update={"messages": [ToolMessage(content=f"Error: {e}", tool_call_id=runtime.tool_call_id)]})
|