Support custom channel assistant IDs via lead_agent (#1500)

* Support custom channel assistant IDs via lead agent

* Normalize custom channel agent names
This commit is contained in:
DanielWalnut
2026-03-28 19:07:38 +08:00
committed by GitHub
parent 520c0352b5
commit 18e3487888
5 changed files with 99 additions and 12 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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: <custom-agent-name> 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)",

View File

@@ -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

View File

@@ -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: