mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
fix(title_generator): strip think blocks from LLM output before extracting title
Think-enabled models (MiniMax M2.7, DeepSeek, etc.) emit inline <think>...</think> reasoning even for simple prompts like title generation, and the raw XML was leaking into session titles. Route the title-model response through the canonical strip_think_blocks scrubber before cleanup so every tag variant — closed pairs, unterminated blocks, orphan closes, mixed case — is handled, not just a single literal <think> pair. - 2 regression tests: closed <think> pair stripped, unterminated block at start yields no title. Salvaged from PR #44126 by @shawchanshek.
This commit is contained in:
parent
037e389c4f
commit
3b739b990b
3 changed files with 41 additions and 1 deletions
|
|
@ -87,7 +87,15 @@ def generate_title(
|
|||
timeout=timeout,
|
||||
main_runtime=main_runtime,
|
||||
)
|
||||
title = (response.choices[0].message.content or "").strip()
|
||||
content = response.choices[0].message.content or ""
|
||||
# Strip thinking/reasoning blocks that think-enabled models
|
||||
# (MiniMax M2.7, DeepSeek, etc.) emit even for simple prompts like
|
||||
# title generation. Without this the raw <think>...</think> XML
|
||||
# leaks into session titles. Reuses the canonical scrubber so all
|
||||
# tag variants (unterminated blocks, orphan closes, mixed case)
|
||||
# are handled, not just a single literal <think> pair.
|
||||
from agent.agent_runtime_helpers import strip_think_blocks
|
||||
title = strip_think_blocks(None, content).strip()
|
||||
# Clean up: remove quotes, trailing punctuation, prefixes like "Title: "
|
||||
title = title.strip('"\'')
|
||||
if title.lower().startswith("title:"):
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ AUTHOR_MAP = {
|
|||
"gary@bitcryptic.com": "bitcryptic-gw", # PR #53997 salvage (Matrix E2EE: resolve device_id via query_keys({mxid: []}) when whoami returns none; guard verification call sites so query_keys is never sent [null]; reset _device_id_unverified at connect() start; disconnect before reconnect)
|
||||
"7698789+abchiaravalle@users.noreply.github.com": "abchiaravalle", # PR #46997 salvage (recover resume_pending sessions: dual freshness signal + empty-turn safety net so restart auto-resume never sends a blank user turn)
|
||||
"swissly@users.noreply.github.com": "swissly", # PR #47167 salvage (wrap cron delivery thread-pool fallback in its own try/except so a per-target failure can't escape the except-RuntimeError block and crash the multi-target delivery loop; #47163)
|
||||
"53571168+shawchanshek@users.noreply.github.com": "shawchanshek", # PR #44126 salvage (strip <think>...</think> reasoning blocks from title-generator LLM output via the canonical strip_think_blocks scrubber so reasoning-model output can't leak into session titles)
|
||||
"30854794+YLChen-007@users.noreply.github.com": "YLChen-007", # PR #27289 salvage (case-insensitive streaming reasoning-tag filter in cli.py _stream_delta + gateway stream_consumer so mixed-case variants like <Think>/<ThInK> are suppressed, not just the hardcoded case literals)
|
||||
"27672904+kangsoo-bit@users.noreply.github.com": "kangsoo-bit", # PR #47508 salvage (keep Telegram gateway alive on transient bootstrap network errors: best-effort deleteWebhook + resilient start_polling degrade to background recovery instead of failing startup)
|
||||
"259353979+testingbuddies24@users.noreply.github.com": "testingbuddies24", # PR #43192 salvage (strip orphan think-tag close tags in progressive gateway stream so a bare </think> whose open was dropped upstream can't leak to the user)
|
||||
|
|
|
|||
|
|
@ -99,6 +99,37 @@ class TestGenerateTitle:
|
|||
title = generate_title("how do I set up docker", "First install...")
|
||||
assert title == "Setting Up Docker Environment"
|
||||
|
||||
def test_strips_think_blocks(self):
|
||||
"""Reasoning-model output wrapped in <think>...</think> must not
|
||||
leak into the session title."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.choices = [MagicMock()]
|
||||
mock_response.choices[0].message.content = (
|
||||
"<think>The user wants a title. I'll summarize the topic "
|
||||
"concisely.</think>Debugging Python Import Errors"
|
||||
)
|
||||
|
||||
with patch("agent.title_generator.call_llm", return_value=mock_response):
|
||||
title = generate_title("help me fix this import", "Sure...")
|
||||
assert title == "Debugging Python Import Errors"
|
||||
assert "<think>" not in title
|
||||
assert "summarize" not in title
|
||||
|
||||
def test_strips_unterminated_think_block(self):
|
||||
"""An unterminated <think> block (no close tag) must still be
|
||||
stripped so the leaked reasoning doesn't become the title."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.choices = [MagicMock()]
|
||||
mock_response.choices[0].message.content = (
|
||||
"<think>Let me reason about a good title for this session"
|
||||
)
|
||||
|
||||
with patch("agent.title_generator.call_llm", return_value=mock_response):
|
||||
title = generate_title("hello", "hi there")
|
||||
# Everything from the unterminated open tag onward is stripped,
|
||||
# leaving nothing → None.
|
||||
assert title is None
|
||||
|
||||
def test_strips_title_prefix(self):
|
||||
mock_response = MagicMock()
|
||||
mock_response.choices = [MagicMock()]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue