Skip to content

Commit 533a0bd

Browse files
authored
fix: align deerflow runner with deerflow 2.0 (#7500)
* fix: align deerflow runner with deerflow 2.0 * fix: address deerflow review feedback
1 parent 35ce281 commit 533a0bd

File tree

10 files changed

+464
-33
lines changed

10 files changed

+464
-33
lines changed

astrbot/builtin_stars/builtin_commands/commands/conversation.py

Lines changed: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
from astrbot.api import sp, star
22
from astrbot.api.event import AstrMessageEvent, MessageEventResult
3+
from astrbot.core import logger
34
from astrbot.core.agent.runners.deerflow.constants import (
5+
DEERFLOW_AGENT_RUNNER_PROVIDER_ID_KEY,
46
DEERFLOW_PROVIDER_TYPE,
57
DEERFLOW_THREAD_ID_KEY,
68
)
9+
from astrbot.core.agent.runners.deerflow.deerflow_api_client import DeerFlowAPIClient
710
from astrbot.core.utils.active_event_registry import active_event_registry
811

912
from .utils.rst_scene import RstScene
@@ -17,6 +20,85 @@
1720
THIRD_PARTY_AGENT_RUNNER_STR = ", ".join(THIRD_PARTY_AGENT_RUNNER_KEY.keys())
1821

1922

23+
async def _cleanup_deerflow_thread_if_present(
24+
context: star.Context,
25+
umo: str,
26+
) -> None:
27+
try:
28+
thread_id = await sp.get_async(
29+
scope="umo",
30+
scope_id=umo,
31+
key=DEERFLOW_THREAD_ID_KEY,
32+
default="",
33+
)
34+
if not thread_id:
35+
return
36+
37+
cfg = context.get_config(umo=umo)
38+
provider_id = cfg["provider_settings"].get(
39+
DEERFLOW_AGENT_RUNNER_PROVIDER_ID_KEY,
40+
"",
41+
)
42+
if not provider_id:
43+
return
44+
45+
merged_provider_config = context.provider_manager.get_provider_config_by_id(
46+
provider_id,
47+
merged=True,
48+
)
49+
if not merged_provider_config:
50+
logger.warning(
51+
"Failed to resolve DeerFlow provider config for remote thread cleanup: provider_id=%s",
52+
provider_id,
53+
)
54+
return
55+
56+
client = DeerFlowAPIClient(
57+
api_base=merged_provider_config.get(
58+
"deerflow_api_base",
59+
"http://127.0.0.1:2026",
60+
),
61+
api_key=merged_provider_config.get("deerflow_api_key", ""),
62+
auth_header=merged_provider_config.get("deerflow_auth_header", ""),
63+
proxy=merged_provider_config.get("proxy", ""),
64+
)
65+
try:
66+
await client.delete_thread(thread_id)
67+
finally:
68+
try:
69+
await client.close()
70+
except Exception as e:
71+
logger.warning(
72+
"Failed to close DeerFlow API client after thread cleanup: %s",
73+
e,
74+
)
75+
except Exception as e:
76+
logger.warning(
77+
"Failed to clean up DeerFlow thread for session %s: %s",
78+
umo,
79+
e,
80+
)
81+
82+
83+
async def _clear_third_party_agent_runner_state(
84+
context: star.Context,
85+
umo: str,
86+
agent_runner_type: str,
87+
) -> None:
88+
session_key = THIRD_PARTY_AGENT_RUNNER_KEY.get(agent_runner_type)
89+
if not session_key:
90+
return
91+
92+
if agent_runner_type == DEERFLOW_PROVIDER_TYPE:
93+
await _cleanup_deerflow_thread_if_present(context, umo)
94+
95+
await sp.remove_async(
96+
scope="umo",
97+
scope_id=umo,
98+
key=session_key,
99+
)
100+
101+
20102
class ConversationCommands:
21103
def __init__(self, context: star.Context) -> None:
22104
self.context = context
@@ -65,10 +147,10 @@ async def reset(self, message: AstrMessageEvent) -> None:
65147
agent_runner_type = cfg["provider_settings"]["agent_runner_type"]
66148
if agent_runner_type in THIRD_PARTY_AGENT_RUNNER_KEY:
67149
active_event_registry.stop_all(umo, exclude=message)
68-
await sp.remove_async(
69-
scope="umo",
70-
scope_id=umo,
71-
key=THIRD_PARTY_AGENT_RUNNER_KEY[agent_runner_type],
150+
await _clear_third_party_agent_runner_state(
151+
self.context,
152+
umo,
153+
agent_runner_type,
72154
)
73155
message.set_result(
74156
MessageEventResult().message("✅ Conversation reset successfully.")
@@ -139,10 +221,10 @@ async def new_conv(self, message: AstrMessageEvent) -> None:
139221
agent_runner_type = cfg["provider_settings"]["agent_runner_type"]
140222
if agent_runner_type in THIRD_PARTY_AGENT_RUNNER_KEY:
141223
active_event_registry.stop_all(message.unified_msg_origin, exclude=message)
142-
await sp.remove_async(
143-
scope="umo",
144-
scope_id=message.unified_msg_origin,
145-
key=THIRD_PARTY_AGENT_RUNNER_KEY[agent_runner_type],
224+
await _clear_third_party_agent_runner_state(
225+
self.context,
226+
message.unified_msg_origin,
227+
agent_runner_type,
146228
)
147229
message.set_result(
148230
MessageEventResult().message("✅ New conversation created.")

astrbot/core/agent/runners/deerflow/deerflow_agent_runner.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -410,18 +410,20 @@ def _build_messages(
410410
)
411411
return messages
412412

413-
def _build_runtime_context(self, thread_id: str) -> dict[str, T.Any]:
414-
runtime_context: dict[str, T.Any] = {
413+
def _build_runtime_configurable(self, thread_id: str) -> dict[str, T.Any]:
414+
runtime_configurable: dict[str, T.Any] = {
415415
"thread_id": thread_id,
416416
"thinking_enabled": self.thinking_enabled,
417417
"is_plan_mode": self.plan_mode,
418418
"subagent_enabled": self.subagent_enabled,
419419
}
420420
if self.subagent_enabled:
421-
runtime_context["max_concurrent_subagents"] = self.max_concurrent_subagents
421+
runtime_configurable["max_concurrent_subagents"] = (
422+
self.max_concurrent_subagents
423+
)
422424
if self.model_name:
423-
runtime_context["model_name"] = self.model_name
424-
return runtime_context
425+
runtime_configurable["model_name"] = self.model_name
426+
return runtime_configurable
425427

426428
def _build_payload(
427429
self,
@@ -430,16 +432,19 @@ def _build_payload(
430432
image_urls: list[str],
431433
system_prompt: str | None,
432434
) -> dict[str, T.Any]:
435+
runtime_configurable = self._build_runtime_configurable(thread_id)
433436
return {
434437
"assistant_id": self.assistant_id,
435438
"input": {
436439
"messages": self._build_messages(prompt, image_urls, system_prompt),
437440
},
438441
"stream_mode": ["values", "messages-tuple", "custom"],
439-
# LangGraph 0.6+ prefers context instead of configurable.
440-
"context": self._build_runtime_context(thread_id),
442+
# DeerFlow 2.0 consumes runtime overrides from config.configurable.
443+
# Keep the legacy context mirror for older compat paths.
444+
"context": dict(runtime_configurable),
441445
"config": {
442446
"recursion_limit": self.recursion_limit,
447+
"configurable": runtime_configurable,
443448
},
444449
}
445450

astrbot/core/agent/runners/deerflow/deerflow_api_client.py

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,33 @@
1010
SSE_MAX_BUFFER_CHARS = 1_048_576
1111

1212

13+
class DeerFlowAPIError(Exception):
14+
def __init__(
15+
self,
16+
*,
17+
operation: str,
18+
status: int,
19+
body: str,
20+
url: str,
21+
thread_id: str | None = None,
22+
) -> None:
23+
self.operation = operation
24+
self.status = status
25+
self.body = body
26+
self.url = url
27+
self.thread_id = thread_id
28+
29+
message = (
30+
f"DeerFlow {operation} failed: status={status}, url={url}, body={body}"
31+
)
32+
if thread_id is not None:
33+
message = (
34+
f"DeerFlow {operation} failed: thread_id={thread_id}, "
35+
f"status={status}, url={url}, body={body}"
36+
)
37+
super().__init__(message)
38+
39+
1340
def _normalize_sse_newlines(text: str) -> str:
1441
"""Normalize CRLF/CR to LF so SSE block splitting works reliably."""
1542
return text.replace("\r\n", "\n").replace("\r", "\n")
@@ -152,11 +179,33 @@ async def create_thread(self, timeout: float = 20) -> dict[str, Any]:
152179
) as resp:
153180
if resp.status not in (200, 201):
154181
text = await resp.text()
155-
raise Exception(
156-
f"DeerFlow create thread failed: {resp.status}. {text}",
182+
raise DeerFlowAPIError(
183+
operation="create thread",
184+
status=resp.status,
185+
body=text,
186+
url=url,
157187
)
158188
return await resp.json()
159189

190+
async def delete_thread(self, thread_id: str, timeout: float = 20) -> None:
191+
session = self._get_session()
192+
url = f"{self.api_base}/api/threads/{thread_id}"
193+
async with session.delete(
194+
url,
195+
headers=self.headers,
196+
timeout=timeout,
197+
proxy=self.proxy,
198+
) as resp:
199+
if resp.status not in (200, 202, 204, 404):
200+
text = await resp.text()
201+
raise DeerFlowAPIError(
202+
operation="delete thread",
203+
status=resp.status,
204+
body=text,
205+
url=url,
206+
thread_id=thread_id,
207+
)
208+
160209
async def stream_run(
161210
self,
162211
thread_id: str,
@@ -200,8 +249,12 @@ async def stream_run(
200249
) as resp:
201250
if resp.status != 200:
202251
text = await resp.text()
203-
raise Exception(
204-
f"DeerFlow runs/stream request failed: {resp.status}. {text}",
252+
raise DeerFlowAPIError(
253+
operation="runs/stream request",
254+
status=resp.status,
255+
body=text,
256+
url=url,
257+
thread_id=thread_id,
205258
)
206259
async for event in _stream_sse(resp):
207260
yield event

astrbot/core/config/default.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2671,12 +2671,12 @@ class ChatProviderTemplate(TypedDict):
26712671
"deerflow_assistant_id": {
26722672
"description": "Assistant ID",
26732673
"type": "string",
2674-
"hint": "LangGraph assistant_id,默认为 lead_agent。",
2674+
"hint": "DeerFlow 2.0 LangGraph assistant_id,默认为 lead_agent。",
26752675
},
26762676
"deerflow_model_name": {
26772677
"description": "模型名称覆盖",
26782678
"type": "string",
2679-
"hint": "可选。覆盖 DeerFlow 默认模型(对应 runtime context 的 model_name)。",
2679+
"hint": "可选。覆盖 DeerFlow 默认模型(对应运行时 configurable 的 model_name)。",
26802680
},
26812681
"deerflow_thinking_enabled": {
26822682
"description": "启用思考模式",
@@ -2685,17 +2685,17 @@ class ChatProviderTemplate(TypedDict):
26852685
"deerflow_plan_mode": {
26862686
"description": "启用计划模式",
26872687
"type": "bool",
2688-
"hint": "对应 DeerFlow 的 is_plan_mode。",
2688+
"hint": "对应 DeerFlow 2.0 运行时 configurable 的 is_plan_mode。",
26892689
},
26902690
"deerflow_subagent_enabled": {
26912691
"description": "启用子智能体",
26922692
"type": "bool",
2693-
"hint": "对应 DeerFlow 的 subagent_enabled。",
2693+
"hint": "对应 DeerFlow 2.0 运行时 configurable 的 subagent_enabled。",
26942694
},
26952695
"deerflow_max_concurrent_subagents": {
26962696
"description": "子智能体最大并发数",
26972697
"type": "int",
2698-
"hint": "对应 DeerFlow 的 max_concurrent_subagents。仅在启用子智能体时生效,默认 3。",
2698+
"hint": "对应 DeerFlow 2.0 运行时 configurable 的 max_concurrent_subagents。仅在启用子智能体时生效,默认 3。",
26992699
},
27002700
"deerflow_recursion_limit": {
27012701
"description": "递归深度上限",

astrbot/core/provider/manager.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,26 @@ def get_merged_provider_config(self, provider_config: dict) -> dict:
505505
pc = merged_config
506506
return pc
507507

508+
def get_provider_config_by_id(
509+
self,
510+
provider_id: str,
511+
*,
512+
merged: bool = False,
513+
) -> dict | None:
514+
"""Get a provider config by id.
515+
516+
Args:
517+
provider_id: Provider id to resolve.
518+
merged: Whether to merge provider_source config into the provider config.
519+
"""
520+
for provider_config in self.providers_config:
521+
if provider_config.get("id") != provider_id:
522+
continue
523+
if merged:
524+
return self.get_merged_provider_config(provider_config)
525+
return copy.deepcopy(provider_config)
526+
return None
527+
508528
def _resolve_env_key_list(self, provider_config: dict) -> dict:
509529
keys = provider_config.get("key", [])
510530
if not isinstance(keys, list):

dashboard/src/i18n/locales/zh-CN/features/config-metadata.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1604,26 +1604,26 @@
16041604
},
16051605
"deerflow_assistant_id": {
16061606
"description": "Assistant ID",
1607-
"hint": "LangGraph assistant_id,默认为 lead_agent。"
1607+
"hint": "DeerFlow 2.0 LangGraph assistant_id,默认为 lead_agent。"
16081608
},
16091609
"deerflow_model_name": {
16101610
"description": "模型名称覆盖",
1611-
"hint": "可选。覆盖 DeerFlow 默认模型(对应 runtime context 的 model_name)。"
1611+
"hint": "可选。覆盖 DeerFlow 默认模型(对应运行时 configurable 的 model_name)。"
16121612
},
16131613
"deerflow_thinking_enabled": {
16141614
"description": "启用思考模式"
16151615
},
16161616
"deerflow_plan_mode": {
16171617
"description": "启用计划模式",
1618-
"hint": "对应 DeerFlow 的 is_plan_mode。"
1618+
"hint": "对应 DeerFlow 2.0 运行时 configurable 的 is_plan_mode。"
16191619
},
16201620
"deerflow_subagent_enabled": {
16211621
"description": "启用子智能体",
1622-
"hint": "对应 DeerFlow 的 subagent_enabled。"
1622+
"hint": "对应 DeerFlow 2.0 运行时 configurable 的 subagent_enabled。"
16231623
},
16241624
"deerflow_max_concurrent_subagents": {
16251625
"description": "子智能体最大并发数",
1626-
"hint": "对应 DeerFlow 的 max_concurrent_subagents。仅在启用子智能体时生效,默认 3。"
1626+
"hint": "对应 DeerFlow 2.0 运行时 configurable 的 max_concurrent_subagents。仅在启用子智能体时生效,默认 3。"
16271627
},
16281628
"deerflow_recursion_limit": {
16291629
"description": "递归深度上限",

docs/zh/providers/agent-runners/deerflow.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
在 v4.19.2 及之后,AstrBot 支持接入 [DeerFlow](https://github.com/bytedance/deer-flow) Agent Runner。
44

5+
当前适配面向 DeerFlow **2.0 `main` 分支**。DeerFlow 官方已将原始 Deep Research 框架迁移到 `main-1.x` 分支持续维护,因此如果你使用的是 2.0,请以 `main` 分支文档和后端 API 为准。
6+
57
## 预备工作:部署 DeerFlow
68

79
如果你还没有部署 DeerFlow,请先参考 DeerFlow 官方文档完成安装和启动:
@@ -25,19 +27,20 @@
2527
- `API Base URL`:DeerFlow API 网关地址,默认为 `http://127.0.0.1:2026`
2628
- `DeerFlow API Key`:可选。若你的 DeerFlow 网关使用 Bearer 鉴权,可在此填写
2729
- `Authorization Header`:可选。自定义 Authorization 请求头,优先级高于 `DeerFlow API Key`
28-
- `Assistant ID`:对应 LangGraph 的 `assistant_id`,默认为 `lead_agent`
30+
- `Assistant ID`:对应 DeerFlow 2.0 LangGraph 的 `assistant_id`,默认为 `lead_agent`
2931
- `模型名称覆盖`:可选。覆盖 DeerFlow 默认模型
3032
- `启用思考模式`:是否启用 DeerFlow 的思考模式
31-
- `启用计划模式`:对应 DeerFlow `is_plan_mode`
32-
- `启用子智能体`:对应 DeerFlow `subagent_enabled`
33-
- `子智能体最大并发数`:对应 `max_concurrent_subagents`,仅在启用子智能体时生效,默认 `3`
33+
- `启用计划模式`:对应 DeerFlow 2.0 运行时 `config.configurable.is_plan_mode`
34+
- `启用子智能体`:对应 DeerFlow 2.0 运行时 `config.configurable.subagent_enabled`
35+
- `子智能体最大并发数`:对应 DeerFlow 2.0 运行时 `config.configurable.max_concurrent_subagents`,仅在启用子智能体时生效,默认 `3`
3436
- `递归深度上限`:对应 LangGraph 的 `recursion_limit`,默认 `1000`
3537

3638
填写完成后点击「保存」。
3739

3840
> [!TIP]
3941
> - 如果 DeerFlow 侧已经配置了默认模型,可以将 `模型名称覆盖` 留空。
4042
> - 只有在 DeerFlow 侧已经启用了相应能力时,才建议开启 `计划模式``子智能体` 相关选项。
43+
> - AstrBot 会同时发送 DeerFlow 2.0 推荐的 `config.configurable` 运行时参数,并保留兼容字段,便于对接上游近期版本。
4144
4245
## 选择 Agent 执行器
4346

@@ -51,3 +54,4 @@
5154
- `API Base URL` 是否能从 AstrBot 所在环境访问
5255
- 鉴权配置是否填写正确
5356
- `Assistant ID` 是否与 DeerFlow 中实际可用的 assistant 一致
57+
- 如果通过 `/reset``/new``/del` 重置 DeerFlow 会话,AstrBot 会尝试同步清理 DeerFlow 远端 thread;若 DeerFlow 网关不可达,则只会清理 AstrBot 本地会话标识

0 commit comments

Comments
 (0)