mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-04-25 11:18:22 +00:00
fix(gateway): forward assistant_id as agent_name in build_run_config (#1667)
* fix(gateway): forward assistant_id as agent_name in build_run_config Fixes #1644 When the LangGraph Platform-compatible /runs endpoint receives a custom assistant_id (e.g. 'finalis'), the Gateway's build_run_config() silently ignored it — configurable['agent_name'] was never set, so make_lead_agent fell through to the default lead agent and SOUL.md was never loaded. Root cause (introduced in #1403): resolve_agent_factory() correctly falls back to make_lead_agent for all assistant_id values, but build_run_config() had no assistant_id parameter and never injected configurable['agent_name']. The full call chain: POST /runs (assistant_id='finalis') → resolve_agent_factory('finalis') # returns make_lead_agent ✓ → build_run_config(thread_id, ...) # no agent_name injected ✗ → make_lead_agent(config) → cfg.get('agent_name') → None → load_agent_soul(None) → base SOUL.md (doesn't exist) → None Fix: - Add keyword-only parameter to build_run_config(). - When assistant_id is set and differs from 'lead_agent', inject it as configurable['agent_name'] (matching the channel manager's existing _resolve_run_params() logic for IM channels). - Honour an explicit configurable['agent_name'] in the request body; assistant_id mapping only fills the gap when it is absent. - Remove stale log-only branch from resolve_agent_factory(); update docstring to explain the factory/configurable split. Tests added (test_gateway_services.py): - Custom assistant_id injects configurable['agent_name'] - 'lead_agent' assistant_id does NOT inject agent_name - None assistant_id does NOT inject agent_name - Explicit configurable['agent_name'] in request is not overwritten - resolve_agent_factory returns make_lead_agent for all inputs * style: format with ruff * fix: validate and normalize assistant_id to prevent path traversal Addresses Copilot review: strip/lowercase/replace underscores and reject names that don't match [a-z0-9-]+, consistent with ChannelManager._normalize_custom_agent_name(). --------- Co-authored-by: voidborne-d <voidborne-d@users.noreply.github.com>
This commit is contained in:
parent
a3bfea631c
commit
6ff60f2af1
@ -10,6 +10,7 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@ -93,20 +94,56 @@ def normalize_input(raw_input: dict[str, Any] | None) -> dict[str, Any]:
|
|||||||
return raw_input
|
return raw_input
|
||||||
|
|
||||||
|
|
||||||
|
_DEFAULT_ASSISTANT_ID = "lead_agent"
|
||||||
|
|
||||||
|
|
||||||
def resolve_agent_factory(assistant_id: str | None):
|
def resolve_agent_factory(assistant_id: str | None):
|
||||||
"""Resolve the agent factory callable from config."""
|
"""Resolve the agent factory callable from config.
|
||||||
|
|
||||||
|
Custom agents are implemented as ``lead_agent`` + an ``agent_name``
|
||||||
|
injected into ``configurable`` — see :func:`build_run_config`. All
|
||||||
|
``assistant_id`` values therefore map to the same factory; the routing
|
||||||
|
happens inside ``make_lead_agent`` when it reads ``cfg["agent_name"]``.
|
||||||
|
"""
|
||||||
from deerflow.agents.lead_agent.agent import make_lead_agent
|
from deerflow.agents.lead_agent.agent import make_lead_agent
|
||||||
|
|
||||||
if assistant_id and assistant_id != "lead_agent":
|
|
||||||
logger.info("assistant_id=%s requested; falling back to lead_agent", assistant_id)
|
|
||||||
return make_lead_agent
|
return make_lead_agent
|
||||||
|
|
||||||
|
|
||||||
def build_run_config(thread_id: str, request_config: dict[str, Any] | None, metadata: dict[str, Any] | None) -> dict[str, Any]:
|
def build_run_config(
|
||||||
"""Build a RunnableConfig dict for the agent."""
|
thread_id: str,
|
||||||
configurable = {"thread_id": thread_id}
|
request_config: dict[str, Any] | None,
|
||||||
|
metadata: dict[str, Any] | None,
|
||||||
|
*,
|
||||||
|
assistant_id: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Build a RunnableConfig dict for the agent.
|
||||||
|
|
||||||
|
When *assistant_id* refers to a custom agent (anything other than
|
||||||
|
``"lead_agent"`` / ``None``), the name is forwarded as
|
||||||
|
``configurable["agent_name"]``. ``make_lead_agent`` reads this key to
|
||||||
|
load the matching ``agents/<name>/SOUL.md`` and per-agent config —
|
||||||
|
without it the agent silently runs as the default lead agent.
|
||||||
|
|
||||||
|
This mirrors the channel manager's ``_resolve_run_params`` logic so that
|
||||||
|
the LangGraph Platform-compatible HTTP API and the IM channel path behave
|
||||||
|
identically.
|
||||||
|
"""
|
||||||
|
configurable: dict[str, Any] = {"thread_id": thread_id}
|
||||||
if request_config:
|
if request_config:
|
||||||
configurable.update(request_config.get("configurable", {}))
|
configurable.update(request_config.get("configurable", {}))
|
||||||
|
|
||||||
|
# Inject custom agent name when the caller specified a non-default assistant.
|
||||||
|
# Honour an explicit configurable["agent_name"] in the request if already set.
|
||||||
|
if assistant_id and assistant_id != _DEFAULT_ASSISTANT_ID and "agent_name" not in configurable:
|
||||||
|
# Normalize the same way ChannelManager does: strip, lowercase,
|
||||||
|
# replace underscores with hyphens, then validate to prevent path
|
||||||
|
# traversal and invalid agent directory lookups.
|
||||||
|
normalized = assistant_id.strip().lower().replace("_", "-")
|
||||||
|
if not normalized or not re.fullmatch(r"[a-z0-9-]+", normalized):
|
||||||
|
raise ValueError(f"Invalid assistant_id {assistant_id!r}: must contain only letters, digits, and hyphens after normalization.")
|
||||||
|
configurable["agent_name"] = normalized
|
||||||
|
|
||||||
config: dict[str, Any] = {"configurable": configurable, "recursion_limit": 100}
|
config: dict[str, Any] = {"configurable": configurable, "recursion_limit": 100}
|
||||||
if request_config:
|
if request_config:
|
||||||
for k, v in request_config.items():
|
for k, v in request_config.items():
|
||||||
@ -233,7 +270,7 @@ async def start_run(
|
|||||||
|
|
||||||
agent_factory = resolve_agent_factory(body.assistant_id)
|
agent_factory = resolve_agent_factory(body.assistant_id)
|
||||||
graph_input = normalize_input(body.input)
|
graph_input = normalize_input(body.input)
|
||||||
config = build_run_config(thread_id, body.config, body.metadata)
|
config = build_run_config(thread_id, body.config, body.metadata, assistant_id=body.assistant_id)
|
||||||
stream_modes = normalize_stream_modes(body.stream_mode)
|
stream_modes = normalize_stream_modes(body.stream_mode)
|
||||||
|
|
||||||
task = asyncio.create_task(
|
task = asyncio.create_task(
|
||||||
|
|||||||
@ -100,3 +100,63 @@ def test_build_run_config_with_overrides():
|
|||||||
assert config["configurable"]["model_name"] == "gpt-4"
|
assert config["configurable"]["model_name"] == "gpt-4"
|
||||||
assert config["tags"] == ["test"]
|
assert config["tags"] == ["test"]
|
||||||
assert config["metadata"]["user"] == "alice"
|
assert config["metadata"]["user"] == "alice"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Regression tests for issue #1644:
|
||||||
|
# assistant_id not mapped to agent_name → custom agent SOUL.md never loaded
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_run_config_custom_agent_injects_agent_name():
|
||||||
|
"""Custom assistant_id must be forwarded as configurable['agent_name'].
|
||||||
|
|
||||||
|
Regression test for #1644: when the LangGraph Platform-compatible
|
||||||
|
/runs endpoint receives a custom assistant_id (e.g. 'finalis'), the
|
||||||
|
Gateway must inject configurable['agent_name'] so that make_lead_agent
|
||||||
|
loads the correct agents/finalis/SOUL.md.
|
||||||
|
"""
|
||||||
|
from app.gateway.services import build_run_config
|
||||||
|
|
||||||
|
config = build_run_config("thread-1", None, None, assistant_id="finalis")
|
||||||
|
assert config["configurable"]["agent_name"] == "finalis", "Custom assistant_id must be forwarded as configurable['agent_name'] so that make_lead_agent loads the correct SOUL.md"
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_run_config_lead_agent_no_agent_name():
|
||||||
|
"""'lead_agent' assistant_id must NOT inject configurable['agent_name']."""
|
||||||
|
from app.gateway.services import build_run_config
|
||||||
|
|
||||||
|
config = build_run_config("thread-1", None, None, assistant_id="lead_agent")
|
||||||
|
assert "agent_name" not in config["configurable"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_run_config_none_assistant_id_no_agent_name():
|
||||||
|
"""None assistant_id must NOT inject configurable['agent_name']."""
|
||||||
|
from app.gateway.services import build_run_config
|
||||||
|
|
||||||
|
config = build_run_config("thread-1", None, None, assistant_id=None)
|
||||||
|
assert "agent_name" not in config["configurable"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_run_config_explicit_agent_name_not_overwritten():
|
||||||
|
"""An explicit configurable['agent_name'] in the request must take precedence."""
|
||||||
|
from app.gateway.services import build_run_config
|
||||||
|
|
||||||
|
config = build_run_config(
|
||||||
|
"thread-1",
|
||||||
|
{"configurable": {"agent_name": "explicit-agent"}},
|
||||||
|
None,
|
||||||
|
assistant_id="other-agent",
|
||||||
|
)
|
||||||
|
assert config["configurable"]["agent_name"] == "explicit-agent", "An explicit configurable['agent_name'] in the request body must not be overwritten by the assistant_id mapping"
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_agent_factory_returns_make_lead_agent():
|
||||||
|
"""resolve_agent_factory always returns make_lead_agent regardless of assistant_id."""
|
||||||
|
from app.gateway.services import resolve_agent_factory
|
||||||
|
from deerflow.agents.lead_agent.agent import make_lead_agent
|
||||||
|
|
||||||
|
assert resolve_agent_factory(None) is make_lead_agent
|
||||||
|
assert resolve_agent_factory("lead_agent") is make_lead_agent
|
||||||
|
assert resolve_agent_factory("finalis") is make_lead_agent
|
||||||
|
assert resolve_agent_factory("custom-agent-123") is make_lead_agent
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user