From 18e3487888d1b23b0dd211c23b4aa8bfaca4b92a Mon Sep 17 00:00:00 2001 From: DanielWalnut <45447813+hetaoBackend@users.noreply.github.com> Date: Sat, 28 Mar 2026 19:07:38 +0800 Subject: [PATCH] Support custom channel assistant IDs via lead_agent (#1500) * Support custom channel assistant IDs via lead agent * Normalize custom channel agent names --- README.md | 10 +++++-- README_zh.md | 10 +++++-- backend/app/channels/manager.py | 36 ++++++++++++++++++++++++ backend/tests/test_channels.py | 49 +++++++++++++++++++++++++++++++-- config.example.yaml | 6 ++-- 5 files changed, 99 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 03740e4..7d2d41a 100644 --- a/README.md +++ b/README.md @@ -304,7 +304,7 @@ channels: # Optional: global session defaults for all mobile channels session: - assistant_id: lead_agent + assistant_id: lead_agent # or a custom agent name; custom agents are routed via lead_agent + agent_name config: recursion_limit: 100 context: @@ -330,12 +330,12 @@ channels: # Optional: per-channel / per-user session settings session: - assistant_id: mobile_agent + assistant_id: mobile-agent # custom agent names are also supported here context: thinking_enabled: false users: "123456789": - assistant_id: vip_agent + assistant_id: vip-agent config: recursion_limit: 150 context: @@ -343,6 +343,10 @@ channels: subagent_enabled: true ``` +Notes: +- `assistant_id: lead_agent` calls the default LangGraph assistant directly. +- If `assistant_id` is set to a custom agent name, DeerFlow still routes through `lead_agent` and injects that value as `agent_name`, so the custom agent's SOUL/config takes effect for IM channels. + Set the corresponding API keys in your `.env` file: ```bash diff --git a/README_zh.md b/README_zh.md index 14a559c..713b607 100644 --- a/README_zh.md +++ b/README_zh.md @@ -243,7 +243,7 @@ channels: # 可选:所有移动端渠道共用的全局 session 默认值 session: - assistant_id: lead_agent + assistant_id: lead_agent # 也可以填自定义 agent 名;渠道层会自动转换为 lead_agent + agent_name config: recursion_limit: 100 context: @@ -269,12 +269,12 @@ channels: # 可选:按渠道 / 按用户单独覆盖 session 配置 session: - assistant_id: mobile_agent + assistant_id: mobile-agent # 这里同样支持自定义 agent 名 context: thinking_enabled: false users: "123456789": - assistant_id: vip_agent + assistant_id: vip-agent config: recursion_limit: 150 context: @@ -282,6 +282,10 @@ channels: subagent_enabled: true ``` +说明: +- `assistant_id: lead_agent` 会直接调用默认的 LangGraph assistant。 +- 如果 `assistant_id` 填的是自定义 agent 名,DeerFlow 仍然会走 `lead_agent`,同时把该值注入为 `agent_name`,这样 IM 渠道也会生效对应 agent 的 SOUL 和配置。 + 在 `.env` 里设置对应的 API key: ```bash diff --git a/backend/app/channels/manager.py b/backend/app/channels/manager.py index 68a7888..9b37831 100644 --- a/backend/app/channels/manager.py +++ b/backend/app/channels/manager.py @@ -5,6 +5,7 @@ from __future__ import annotations import asyncio import logging import mimetypes +import re import time from collections.abc import Mapping from typing import Any @@ -17,6 +18,7 @@ logger = logging.getLogger(__name__) DEFAULT_LANGGRAPH_URL = "http://localhost:2024" DEFAULT_GATEWAY_URL = "http://localhost:8001" DEFAULT_ASSISTANT_ID = "lead_agent" +CUSTOM_AGENT_NAME_PATTERN = re.compile(r"^[A-Za-z0-9-]+$") DEFAULT_RUN_CONFIG: dict[str, Any] = {"recursion_limit": 100} DEFAULT_RUN_CONTEXT: dict[str, Any] = { @@ -33,6 +35,10 @@ CHANNEL_CAPABILITIES = { } +class InvalidChannelSessionConfigError(ValueError): + """Raised when IM channel session overrides contain invalid agent config.""" + + def _as_dict(value: Any) -> dict[str, Any]: return dict(value) if isinstance(value, Mapping) else {} @@ -45,6 +51,21 @@ def _merge_dicts(*layers: Any) -> dict[str, Any]: return merged +def _normalize_custom_agent_name(raw_value: str) -> str: + """Normalize legacy channel assistant IDs into valid custom agent names.""" + normalized = raw_value.strip().lower().replace("_", "-") + if not normalized: + raise InvalidChannelSessionConfigError( + "Channel session assistant_id is empty. Use 'lead_agent' or a valid custom agent name." + ) + if not CUSTOM_AGENT_NAME_PATTERN.fullmatch(normalized): + raise InvalidChannelSessionConfigError( + f"Invalid channel session assistant_id {raw_value!r}. " + "Use 'lead_agent' or a custom agent name containing only letters, digits, and hyphens." + ) + return normalized + + def _extract_response_text(result: dict | list) -> str: """Extract the last AI message text from a LangGraph runs.wait result. @@ -379,6 +400,13 @@ class ChannelManager: {"thread_id": thread_id}, ) + # Custom agents are implemented as lead_agent + agent_name context. + # Keep backward compatibility for channel configs that set + # assistant_id: by routing through lead_agent. + if assistant_id != DEFAULT_ASSISTANT_ID: + run_context.setdefault("agent_name", _normalize_custom_agent_name(assistant_id)) + assistant_id = DEFAULT_ASSISTANT_ID + return assistant_id, run_config, run_context # -- LangGraph SDK client (lazy) ---------------------------------------- @@ -452,6 +480,14 @@ class ChannelManager: await self._handle_command(msg) else: await self._handle_chat(msg) + except InvalidChannelSessionConfigError as exc: + logger.warning( + "Invalid channel session config for %s (chat=%s): %s", + msg.channel_name, + msg.chat_id, + exc, + ) + await self._send_error(msg, str(exc)) except Exception: logger.exception( "Error handling message from %s (chat=%s)", diff --git a/backend/tests/test_channels.py b/backend/tests/test_channels.py index faa7d36..ed5ac09 100644 --- a/backend/tests/test_channels.py +++ b/backend/tests/test_channels.py @@ -498,10 +498,11 @@ class TestChannelManager: mock_client.runs.wait.assert_called_once() call_args = mock_client.runs.wait.call_args - assert call_args[0][1] == "mobile_agent" + assert call_args[0][1] == "lead_agent" assert call_args[1]["config"]["recursion_limit"] == 55 assert call_args[1]["context"]["thinking_enabled"] is False assert call_args[1]["context"]["subagent_enabled"] is True + assert call_args[1]["context"]["agent_name"] == "mobile-agent" _run(go()) @@ -525,7 +526,7 @@ class TestChannelManager: }, "users": { "vip-user": { - "assistant_id": "vip_agent", + "assistant_id": " VIP_AGENT ", "config": {"recursion_limit": 77}, "context": { "thinking_enabled": True, @@ -556,14 +557,56 @@ class TestChannelManager: mock_client.runs.wait.assert_called_once() call_args = mock_client.runs.wait.call_args - assert call_args[0][1] == "vip_agent" + assert call_args[0][1] == "lead_agent" assert call_args[1]["config"]["recursion_limit"] == 77 assert call_args[1]["context"]["thinking_enabled"] is True assert call_args[1]["context"]["subagent_enabled"] is True + assert call_args[1]["context"]["agent_name"] == "vip-agent" assert call_args[1]["context"]["is_plan_mode"] is True _run(go()) + def test_handle_chat_rejects_invalid_custom_agent_name(self): + from app.channels.manager import ChannelManager + + async def go(): + bus = MessageBus() + store = ChannelStore(path=Path(tempfile.mkdtemp()) / "store.json") + manager = ChannelManager( + bus=bus, + store=store, + channel_sessions={ + "telegram": { + "assistant_id": "bad agent!", + } + }, + ) + + outbound_received = [] + + async def capture_outbound(msg): + outbound_received.append(msg) + + bus.subscribe_outbound(capture_outbound) + + mock_client = _make_mock_langgraph_client() + manager._client = mock_client + + await manager.start() + + inbound = InboundMessage(channel_name="telegram", chat_id="chat1", user_id="user1", text="hi") + await bus.publish_inbound(inbound) + await _wait_for(lambda: len(outbound_received) >= 1) + await manager.stop() + + mock_client.runs.wait.assert_not_called() + assert outbound_received[0].text == ( + "Invalid channel session assistant_id 'bad agent!'. " + "Use 'lead_agent' or a custom agent name containing only letters, digits, and hyphens." + ) + + _run(go()) + def test_handle_feishu_chat_streams_multiple_outbound_updates(self, monkeypatch): from app.channels.manager import ChannelManager diff --git a/config.example.yaml b/config.example.yaml index b188ecd..002ece3 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -567,7 +567,7 @@ checkpointer: # # # Optional: default mobile/session settings for all IM channels # session: -# assistant_id: lead_agent +# assistant_id: lead_agent # or a custom agent name; custom agents route via lead_agent + agent_name # config: # recursion_limit: 100 # context: @@ -593,14 +593,14 @@ checkpointer: # # # Optional: channel-level session overrides # session: -# assistant_id: mobile_agent +# assistant_id: mobile-agent # custom agent names are supported here too # context: # thinking_enabled: false # # # Optional: per-user overrides by user_id # users: # "123456789": -# assistant_id: vip_agent +# assistant_id: vip-agent # config: # recursion_limit: 150 # context: