fix: normalize thinking tags in reasoning output

This commit is contained in:
Zhou
2026-06-24 15:45:34 +08:00
committed by Xubin Ren
parent f9afc9389b
commit 523bb928bf
4 changed files with 131 additions and 12 deletions
+36
View File
@@ -369,3 +369,39 @@ async def test_runner_streams_native_thinking_deltas_without_post_hoc_dup():
assert result.final_content == "done"
assert hook.emitted == ["part1", "part2"]
@pytest.mark.asyncio
async def test_runner_strips_thinking_tags_from_native_thinking_deltas():
from nanobot.agent.runner import AgentRunner, AgentRunSpec
provider = MagicMock()
async def chat_stream_with_retry(
*, on_content_delta=None, on_thinking_delta=None, **kwargs
):
if on_thinking_delta:
await on_thinking_delta("<thinking>")
await on_thinking_delta("Preparing final response")
await on_thinking_delta("</thinking>")
if on_content_delta:
await on_content_delta("done")
return LLMResponse(content="done", tool_calls=[], usage={})
provider.chat_stream_with_retry = chat_stream_with_retry
tools = MagicMock()
tools.get_definitions.return_value = []
hook = _StreamRecordingHook()
runner = AgentRunner(provider)
result = await runner.run(AgentRunSpec(
initial_messages=[{"role": "user", "content": "q"}],
tools=tools,
model="test-model",
max_iterations=3,
max_tool_result_chars=_MAX_TOOL_RESULT_CHARS,
hook=hook,
))
assert result.final_content == "done"
assert hook.emitted == ["Preparing final response"]
+57 -1
View File
@@ -1,4 +1,9 @@
from nanobot.utils.helpers import extract_reasoning, extract_think, strip_think
from nanobot.utils.helpers import (
extract_reasoning,
extract_think,
strip_reasoning_tags,
strip_think,
)
class TestStripThinkTag:
@@ -27,6 +32,15 @@ class TestStripThinkTag:
def test_self_closing_tag_not_matched(self):
assert strip_think("<thought/>some text") == "<thought/>some text"
def test_thinking_alias_closed_tag(self):
assert strip_think("Hello <thinking>reasoning</thinking> World") == "Hello World"
def test_thinking_alias_unclosed_trailing_tag(self):
assert strip_think("<thinking>ongoing...") == ""
def test_self_closing_thinking_marker_at_start_stripped(self):
assert strip_think("<thinking/>some text") == "some text"
def test_normal_text_unchanged(self):
assert strip_think("Just normal text") == "Just normal text"
@@ -165,6 +179,12 @@ class TestExtractThink:
assert thinking == "reasoning content"
assert clean == "Hello World"
def test_single_thinking_block(self):
text = "Hello <thinking>reasoning content</thinking> World"
thinking, clean = extract_think(text)
assert thinking == "reasoning content"
assert clean == "Hello World"
def test_multiple_think_blocks(self):
text = "A<think>first</think>B<thought>second</thought>C"
thinking, clean = extract_think(text)
@@ -230,6 +250,24 @@ squares = [x**2 for x in range(10)]
class TestExtractReasoning:
"""Single source of truth for reasoning extraction across all providers."""
def test_strips_tags_from_dedicated_reasoning_content(self):
reasoning, content = extract_reasoning(
"<thinking>Preparing final response",
None,
"visible answer",
)
assert reasoning == "Preparing final response"
assert content == "visible answer"
def test_self_closing_thinking_marker_in_reasoning_content(self):
reasoning, content = extract_reasoning(
"<thinking/>Preparing final response",
None,
"visible answer",
)
assert reasoning == "Preparing final response"
assert content == "visible answer"
def test_prefers_reasoning_content_and_strips_inline_think(self):
# Dedicated field wins; inline tags are still scrubbed from content.
reasoning, content = extract_reasoning(
@@ -271,3 +309,21 @@ class TestExtractReasoning:
)
assert reasoning == "plan"
assert content == "answer"
class TestStripReasoningTags:
def test_unclosed_thinking_wrapper_keeps_reasoning_body(self):
assert strip_reasoning_tags("<thinking>Preparing final response") == (
"Preparing final response"
)
def test_self_closing_thinking_marker_keeps_reasoning_body(self):
assert strip_reasoning_tags("<thinking/>Preparing final response") == (
"Preparing final response"
)
def test_closing_thinking_wrapper_removed(self):
assert strip_reasoning_tags("Preparing final response</thinking>") == (
"Preparing final response"
)