Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 27 additions & 12 deletions astrbot/core/agent/runners/dify/dify_agent_runner.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import base64
import os
import re
import sys
import typing as T

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -279,11 +282,23 @@ async def _execute_dify_request(self):
data=AgentResponseData(chain=chain),
)

@staticmethod
def _strip_think_tags(text: str) -> str:
"""Remove <think>...</think> blocks and orphan </think> tags from text.

Some models (e.g. DeepSeek-R1) embed chain-of-thought inside <think> 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"<think>.*?</think>", "", text, flags=re.DOTALL)
text = re.sub(r"</think>\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 <think> 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"]:
Expand All @@ -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))
# 纯文本输出,过滤 <think> 标签
chains.append(Comp.Plain(self._strip_think_tags(output)))
elif isinstance(output, list):
# 主要适配 Dify 的 HTTP 请求结点的多模态输出
for item in output:
Expand All @@ -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", [])
Expand Down
48 changes: 48 additions & 0 deletions tests/test_dify_think_filter.py
Original file line number Diff line number Diff line change
@@ -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 <think>...</think> block should be removed."""
result = strip("<think>let me reason</think>Here is my answer.")
assert result == "Here is my answer."

def test_think_block_with_newlines(self):
"""Multi-line think blocks should be removed."""
text = "<think>\nStep 1: think\nStep 2: conclude\n</think>\nFinal answer."
assert strip(text) == "Final answer."

def test_multiple_think_blocks(self):
"""Multiple consecutive think blocks should all be removed."""
text = "<think>block1</think>Middle<think>block2</think>End"
assert strip(text) == "MiddleEnd"

def test_orphan_closing_tag(self):
"""A trailing </think> without an opening tag should be removed."""
assert strip("Some text</think>") == "Some text"

def test_empty_think_block(self):
"""An empty <think></think> block should be removed."""
assert strip("<think></think>Answer") == "Answer"

def test_only_think_content(self):
"""If the entire string is a think block, result should be empty string."""
assert strip("<think>all reasoning</think>") == ""

def test_whitespace_trimmed(self):
"""Leading/trailing whitespace after stripping should be removed."""
assert strip(" <think>x</think> 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"
Loading