mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-02 22:02:13 +08:00
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:
10
README.md
10
README.md
@@ -304,7 +304,7 @@ channels:
|
|||||||
|
|
||||||
# Optional: global session defaults for all mobile channels
|
# Optional: global session defaults for all mobile channels
|
||||||
session:
|
session:
|
||||||
assistant_id: lead_agent
|
assistant_id: lead_agent # or a custom agent name; custom agents are routed via lead_agent + agent_name
|
||||||
config:
|
config:
|
||||||
recursion_limit: 100
|
recursion_limit: 100
|
||||||
context:
|
context:
|
||||||
@@ -330,12 +330,12 @@ channels:
|
|||||||
|
|
||||||
# Optional: per-channel / per-user session settings
|
# Optional: per-channel / per-user session settings
|
||||||
session:
|
session:
|
||||||
assistant_id: mobile_agent
|
assistant_id: mobile-agent # custom agent names are also supported here
|
||||||
context:
|
context:
|
||||||
thinking_enabled: false
|
thinking_enabled: false
|
||||||
users:
|
users:
|
||||||
"123456789":
|
"123456789":
|
||||||
assistant_id: vip_agent
|
assistant_id: vip-agent
|
||||||
config:
|
config:
|
||||||
recursion_limit: 150
|
recursion_limit: 150
|
||||||
context:
|
context:
|
||||||
@@ -343,6 +343,10 @@ channels:
|
|||||||
subagent_enabled: true
|
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:
|
Set the corresponding API keys in your `.env` file:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
10
README_zh.md
10
README_zh.md
@@ -243,7 +243,7 @@ channels:
|
|||||||
|
|
||||||
# 可选:所有移动端渠道共用的全局 session 默认值
|
# 可选:所有移动端渠道共用的全局 session 默认值
|
||||||
session:
|
session:
|
||||||
assistant_id: lead_agent
|
assistant_id: lead_agent # 也可以填自定义 agent 名;渠道层会自动转换为 lead_agent + agent_name
|
||||||
config:
|
config:
|
||||||
recursion_limit: 100
|
recursion_limit: 100
|
||||||
context:
|
context:
|
||||||
@@ -269,12 +269,12 @@ channels:
|
|||||||
|
|
||||||
# 可选:按渠道 / 按用户单独覆盖 session 配置
|
# 可选:按渠道 / 按用户单独覆盖 session 配置
|
||||||
session:
|
session:
|
||||||
assistant_id: mobile_agent
|
assistant_id: mobile-agent # 这里同样支持自定义 agent 名
|
||||||
context:
|
context:
|
||||||
thinking_enabled: false
|
thinking_enabled: false
|
||||||
users:
|
users:
|
||||||
"123456789":
|
"123456789":
|
||||||
assistant_id: vip_agent
|
assistant_id: vip-agent
|
||||||
config:
|
config:
|
||||||
recursion_limit: 150
|
recursion_limit: 150
|
||||||
context:
|
context:
|
||||||
@@ -282,6 +282,10 @@ channels:
|
|||||||
subagent_enabled: true
|
subagent_enabled: true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
- `assistant_id: lead_agent` 会直接调用默认的 LangGraph assistant。
|
||||||
|
- 如果 `assistant_id` 填的是自定义 agent 名,DeerFlow 仍然会走 `lead_agent`,同时把该值注入为 `agent_name`,这样 IM 渠道也会生效对应 agent 的 SOUL 和配置。
|
||||||
|
|
||||||
在 `.env` 里设置对应的 API key:
|
在 `.env` 里设置对应的 API key:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import mimetypes
|
import mimetypes
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -17,6 +18,7 @@ logger = logging.getLogger(__name__)
|
|||||||
DEFAULT_LANGGRAPH_URL = "http://localhost:2024"
|
DEFAULT_LANGGRAPH_URL = "http://localhost:2024"
|
||||||
DEFAULT_GATEWAY_URL = "http://localhost:8001"
|
DEFAULT_GATEWAY_URL = "http://localhost:8001"
|
||||||
DEFAULT_ASSISTANT_ID = "lead_agent"
|
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_CONFIG: dict[str, Any] = {"recursion_limit": 100}
|
||||||
DEFAULT_RUN_CONTEXT: dict[str, Any] = {
|
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]:
|
def _as_dict(value: Any) -> dict[str, Any]:
|
||||||
return dict(value) if isinstance(value, Mapping) else {}
|
return dict(value) if isinstance(value, Mapping) else {}
|
||||||
|
|
||||||
@@ -45,6 +51,21 @@ def _merge_dicts(*layers: Any) -> dict[str, Any]:
|
|||||||
return merged
|
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:
|
def _extract_response_text(result: dict | list) -> str:
|
||||||
"""Extract the last AI message text from a LangGraph runs.wait result.
|
"""Extract the last AI message text from a LangGraph runs.wait result.
|
||||||
|
|
||||||
@@ -379,6 +400,13 @@ class ChannelManager:
|
|||||||
{"thread_id": thread_id},
|
{"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
|
return assistant_id, run_config, run_context
|
||||||
|
|
||||||
# -- LangGraph SDK client (lazy) ----------------------------------------
|
# -- LangGraph SDK client (lazy) ----------------------------------------
|
||||||
@@ -452,6 +480,14 @@ class ChannelManager:
|
|||||||
await self._handle_command(msg)
|
await self._handle_command(msg)
|
||||||
else:
|
else:
|
||||||
await self._handle_chat(msg)
|
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:
|
except Exception:
|
||||||
logger.exception(
|
logger.exception(
|
||||||
"Error handling message from %s (chat=%s)",
|
"Error handling message from %s (chat=%s)",
|
||||||
|
|||||||
@@ -498,10 +498,11 @@ class TestChannelManager:
|
|||||||
|
|
||||||
mock_client.runs.wait.assert_called_once()
|
mock_client.runs.wait.assert_called_once()
|
||||||
call_args = mock_client.runs.wait.call_args
|
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]["config"]["recursion_limit"] == 55
|
||||||
assert call_args[1]["context"]["thinking_enabled"] is False
|
assert call_args[1]["context"]["thinking_enabled"] is False
|
||||||
assert call_args[1]["context"]["subagent_enabled"] is True
|
assert call_args[1]["context"]["subagent_enabled"] is True
|
||||||
|
assert call_args[1]["context"]["agent_name"] == "mobile-agent"
|
||||||
|
|
||||||
_run(go())
|
_run(go())
|
||||||
|
|
||||||
@@ -525,7 +526,7 @@ class TestChannelManager:
|
|||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
"vip-user": {
|
"vip-user": {
|
||||||
"assistant_id": "vip_agent",
|
"assistant_id": " VIP_AGENT ",
|
||||||
"config": {"recursion_limit": 77},
|
"config": {"recursion_limit": 77},
|
||||||
"context": {
|
"context": {
|
||||||
"thinking_enabled": True,
|
"thinking_enabled": True,
|
||||||
@@ -556,14 +557,56 @@ class TestChannelManager:
|
|||||||
|
|
||||||
mock_client.runs.wait.assert_called_once()
|
mock_client.runs.wait.assert_called_once()
|
||||||
call_args = mock_client.runs.wait.call_args
|
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]["config"]["recursion_limit"] == 77
|
||||||
assert call_args[1]["context"]["thinking_enabled"] is True
|
assert call_args[1]["context"]["thinking_enabled"] is True
|
||||||
assert call_args[1]["context"]["subagent_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
|
assert call_args[1]["context"]["is_plan_mode"] is True
|
||||||
|
|
||||||
_run(go())
|
_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):
|
def test_handle_feishu_chat_streams_multiple_outbound_updates(self, monkeypatch):
|
||||||
from app.channels.manager import ChannelManager
|
from app.channels.manager import ChannelManager
|
||||||
|
|
||||||
|
|||||||
@@ -567,7 +567,7 @@ checkpointer:
|
|||||||
#
|
#
|
||||||
# # Optional: default mobile/session settings for all IM channels
|
# # Optional: default mobile/session settings for all IM channels
|
||||||
# session:
|
# session:
|
||||||
# assistant_id: lead_agent
|
# assistant_id: lead_agent # or a custom agent name; custom agents route via lead_agent + agent_name
|
||||||
# config:
|
# config:
|
||||||
# recursion_limit: 100
|
# recursion_limit: 100
|
||||||
# context:
|
# context:
|
||||||
@@ -593,14 +593,14 @@ checkpointer:
|
|||||||
#
|
#
|
||||||
# # Optional: channel-level session overrides
|
# # Optional: channel-level session overrides
|
||||||
# session:
|
# session:
|
||||||
# assistant_id: mobile_agent
|
# assistant_id: mobile-agent # custom agent names are supported here too
|
||||||
# context:
|
# context:
|
||||||
# thinking_enabled: false
|
# thinking_enabled: false
|
||||||
#
|
#
|
||||||
# # Optional: per-user overrides by user_id
|
# # Optional: per-user overrides by user_id
|
||||||
# users:
|
# users:
|
||||||
# "123456789":
|
# "123456789":
|
||||||
# assistant_id: vip_agent
|
# assistant_id: vip-agent
|
||||||
# config:
|
# config:
|
||||||
# recursion_limit: 150
|
# recursion_limit: 150
|
||||||
# context:
|
# context:
|
||||||
|
|||||||
Reference in New Issue
Block a user