diff --git a/astrbot/core/agent/runners/dify/dify_agent_runner.py b/astrbot/core/agent/runners/dify/dify_agent_runner.py
index 93f8d3570d..41d4cd4ec3 100644
--- a/astrbot/core/agent/runners/dify/dify_agent_runner.py
+++ b/astrbot/core/agent/runners/dify/dify_agent_runner.py
@@ -1,5 +1,6 @@
import base64
import os
+import re
import sys
import typing as T
@@ -192,12 +193,14 @@ async def _execute_dify_request(self):
# 如果是流式响应,发送增量数据
if self.streaming and chunk["answer"]:
- yield AgentResponse(
- type="streaming_delta",
- data=AgentResponseData(
- chain=MessageChain().message(chunk["answer"])
- ),
- )
+ delta = self._strip_think_tags(chunk["answer"])
+ if delta:
+ yield AgentResponse(
+ type="streaming_delta",
+ data=AgentResponseData(
+ chain=MessageChain().message(delta)
+ ),
+ )
elif chunk["event"] == "message_end":
logger.debug("Dify message end")
break
@@ -279,11 +282,23 @@ async def _execute_dify_request(self):
data=AgentResponseData(chain=chain),
)
+ @staticmethod
+ def _strip_think_tags(text: str) -> str:
+ """Remove ... blocks and orphan tags from text.
+
+ Some models (e.g. DeepSeek-R1) embed chain-of-thought inside tags
+ even when thinking mode is disabled on the Dify side. This mirrors the
+ same cleanup done in openai_source._parse_openai_completion.
+ """
+ text = re.sub(r".*?", "", text, flags=re.DOTALL)
+ text = re.sub(r"\s*$", "", text)
+ return text.strip()
+
async def parse_dify_result(self, chunk: dict | str) -> MessageChain:
"""解析 Dify 的响应结果"""
if isinstance(chunk, str):
- # Chat
- return MessageChain(chain=[Comp.Plain(chunk)])
+ # Chat — strip any tags the underlying model may have emitted
+ return MessageChain(chain=[Comp.Plain(self._strip_think_tags(chunk))])
async def parse_file(item: dict):
match item["type"]:
@@ -303,8 +318,8 @@ async def parse_file(item: dict):
output = chunk["data"]["outputs"][self.workflow_output_key]
chains = []
if isinstance(output, str):
- # 纯文本输出
- chains.append(Comp.Plain(output))
+ # 纯文本输出,过滤 标签
+ chains.append(Comp.Plain(self._strip_think_tags(output)))
elif isinstance(output, list):
# 主要适配 Dify 的 HTTP 请求结点的多模态输出
for item in output:
@@ -313,10 +328,10 @@ async def parse_file(item: dict):
not isinstance(item, dict)
or item.get("dify_model_identity", "") != "__dify__file__"
):
- chains.append(Comp.Plain(str(output)))
+ chains.append(Comp.Plain(self._strip_think_tags(str(output))))
break
else:
- chains.append(Comp.Plain(str(output)))
+ chains.append(Comp.Plain(self._strip_think_tags(str(output))))
# scan file
files = chunk["data"].get("files", [])
diff --git a/tests/test_dify_think_filter.py b/tests/test_dify_think_filter.py
new file mode 100644
index 0000000000..e72c585bb1
--- /dev/null
+++ b/tests/test_dify_think_filter.py
@@ -0,0 +1,48 @@
+"""Unit tests for DifyAgentRunner._strip_think_tags"""
+
+import pytest
+
+from astrbot.core.agent.runners.dify.dify_agent_runner import DifyAgentRunner
+
+strip = DifyAgentRunner._strip_think_tags
+
+
+class TestStripThinkTags:
+ def test_no_tags(self):
+ """Normal text without any think tags should be unchanged."""
+ assert strip("Hello, world!") == "Hello, world!"
+
+ def test_single_think_block(self):
+ """A complete ... block should be removed."""
+ result = strip("let me reasonHere is my answer.")
+ assert result == "Here is my answer."
+
+ def test_think_block_with_newlines(self):
+ """Multi-line think blocks should be removed."""
+ text = "\nStep 1: think\nStep 2: conclude\n\nFinal answer."
+ assert strip(text) == "Final answer."
+
+ def test_multiple_think_blocks(self):
+ """Multiple consecutive think blocks should all be removed."""
+ text = "block1Middleblock2End"
+ assert strip(text) == "MiddleEnd"
+
+ def test_orphan_closing_tag(self):
+ """A trailing without an opening tag should be removed."""
+ assert strip("Some text") == "Some text"
+
+ def test_empty_think_block(self):
+ """An empty block should be removed."""
+ assert strip("Answer") == "Answer"
+
+ def test_only_think_content(self):
+ """If the entire string is a think block, result should be empty string."""
+ assert strip("all reasoning") == ""
+
+ def test_whitespace_trimmed(self):
+ """Leading/trailing whitespace after stripping should be removed."""
+ assert strip(" x Answer ") == "Answer"
+
+ def test_no_modification_when_no_think(self):
+ """Strings without think tags must be returned as-is (stripped)."""
+ assert strip(" plain text ") == "plain text"