fix(titles): reject trivial-echo new session titles (gate-rebase of #6529)
Co-authored-by: Hermes Agent <hermes@nousresearch.com>
This commit is contained in:
1 parent
8e3bb02e0e
commit
b145f59312
2 files changed
+86
-5
No files matched your search
+35
-5
@@ -2839,13 +2839,19 @@ def _sanitize_generated_title(text: str) -> str:
|
|||||||
s = re.sub(r'^\s*title\s*:\s*', '', s, flags=re.IGNORECASE)
|
s = re.sub(r'^\s*title\s*:\s*', '', s, flags=re.IGNORECASE)
|
||||||
s = s.strip(" \t\r\n\"'`*_~")
|
s = s.strip(" \t\r\n\"'`*_~")
|
||||||
s = re.sub(r'\s+', ' ', s).strip()
|
s = re.sub(r'\s+', ' ', s).strip()
|
||||||
# Guard against chain-of-thought leakage and meta-reasoning patterns.
|
# Guard against chain-of-thought leakage, meta-reasoning, and trivial echo.
|
||||||
if _looks_invalid_generated_title(s):
|
if _is_bad_new_title(s):
|
||||||
return ''
|
return ''
|
||||||
return s[:80]
|
return s[:80]
|
||||||
|
|
||||||
|
|
||||||
def _looks_invalid_generated_title(text: str) -> bool:
|
def _looks_invalid_generated_title(text: str) -> bool:
|
||||||
|
"""True when an existing/persisted title is structurally invalid (CoT leak).
|
||||||
|
|
||||||
|
Intentionally does NOT reject short conversational words like "Done" or
|
||||||
|
"Cool" — those can be legitimate stored titles. Use ``_is_bad_new_title``
|
||||||
|
when validating a freshly generated candidate.
|
||||||
|
"""
|
||||||
s = str(text or '')
|
s = str(text or '')
|
||||||
if not s.strip():
|
if not s.strip():
|
||||||
return True
|
return True
|
||||||
@@ -2857,7 +2863,31 @@ def _looks_invalid_generated_title(text: str) -> bool:
|
|||||||
or re.search(r'^\s*(i|we)\s+(should|need to|will|can)\b', s, flags=re.IGNORECASE)
|
or re.search(r'^\s*(i|we)\s+(should|need to|will|can)\b', s, flags=re.IGNORECASE)
|
||||||
or re.search(r'^\s*let me\b', s, flags=re.IGNORECASE)
|
or re.search(r'^\s*let me\b', s, flags=re.IGNORECASE)
|
||||||
or re.search(r"^\s*here(?:'s| is) (?:a |my )?(?:thinking|thought)", s, flags=re.IGNORECASE)
|
or re.search(r"^\s*here(?:'s| is) (?:a |my )?(?:thinking|thought)", s, flags=re.IGNORECASE)
|
||||||
or re.search(r'^\s*(ok|okay|done|all set|complete|completed|finished)\b[\s.!?]*$', s, flags=re.IGNORECASE)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_bad_new_title(text: str) -> bool:
|
||||||
|
"""True when a freshly generated title candidate should be discarded.
|
||||||
|
|
||||||
|
Includes structural CoT rejection plus trivial single-token echo replies
|
||||||
|
(pong, yes, done, cool, …) that are useless as first-generation titles.
|
||||||
|
"""
|
||||||
|
if _looks_invalid_generated_title(text):
|
||||||
|
return True
|
||||||
|
s = str(text or '').strip()
|
||||||
|
_token = re.sub(r'[\s.!?]+$', '', s, flags=re.IGNORECASE)
|
||||||
|
if re.fullmatch(
|
||||||
|
r'(?:pong|ping|yes|no|yep|nope|hi|hello|hey|thanks|thank you|sure|k|kk|cool|nice|lol|ok|okay|done)',
|
||||||
|
_token,
|
||||||
|
flags=re.IGNORECASE,
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
return bool(
|
||||||
|
re.search(
|
||||||
|
r'^\s*(ok|okay|done|all set|complete|completed|finished)\b[\s.!?]*$',
|
||||||
|
s,
|
||||||
|
flags=re.IGNORECASE,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -2992,7 +3022,7 @@ def _first_exchange_snippets(messages):
|
|||||||
# is asking...", etc.). Assistant rows that carry tool_calls but
|
# is asking...", etc.). Assistant rows that carry tool_calls but
|
||||||
# also contain a substantive answer text are kept — those are
|
# also contain a substantive answer text are kept — those are
|
||||||
# agentic first-turn plans that are legitimate title candidates.
|
# agentic first-turn plans that are legitimate title candidates.
|
||||||
if m.get('tool_calls') and (not candidate or _looks_invalid_generated_title(candidate)):
|
if m.get('tool_calls') and (not candidate or _is_bad_new_title(candidate)):
|
||||||
continue
|
continue
|
||||||
if candidate:
|
if candidate:
|
||||||
asst_text = candidate
|
asst_text = candidate
|
||||||
@@ -3016,7 +3046,7 @@ def _latest_exchange_snippets(messages):
|
|||||||
if role == 'assistant' and not asst_text:
|
if role == 'assistant' and not asst_text:
|
||||||
candidate = _message_text(m.get('content'))
|
candidate = _message_text(m.get('content'))
|
||||||
# Skip tool-call-only preambles
|
# Skip tool-call-only preambles
|
||||||
if m.get('tool_calls') and (not candidate or _looks_invalid_generated_title(candidate)):
|
if m.get('tool_calls') and (not candidate or _is_bad_new_title(candidate)):
|
||||||
continue
|
continue
|
||||||
if candidate:
|
if candidate:
|
||||||
asst_text = candidate
|
asst_text = candidate
|
||||||
|
|||||||
@@ -98,3 +98,54 @@ class TestThinkingPreambleStripping:
|
|||||||
assert not _looks_invalid_generated_title("Python import debugging"), (
|
assert not _looks_invalid_generated_title("Python import debugging"), (
|
||||||
"Real titles must still pass the invalid-title guard"
|
"Real titles must still pass the invalid-title guard"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_looks_invalid_generated_title_rejects_trivial_echo(self):
|
||||||
|
"""Trivial echo rejection applies to NEW titles only, not existing titles."""
|
||||||
|
from api.streaming import _is_bad_new_title, _looks_invalid_generated_title
|
||||||
|
assert _is_bad_new_title("pong")
|
||||||
|
assert _is_bad_new_title("Yes!")
|
||||||
|
assert _is_bad_new_title("Done")
|
||||||
|
assert _is_bad_new_title("Cool")
|
||||||
|
# Existing-title validity must accept short conversational titles so
|
||||||
|
# every subsequent turn does not re-fire the title LLM.
|
||||||
|
assert not _looks_invalid_generated_title("pong")
|
||||||
|
assert not _looks_invalid_generated_title("Done")
|
||||||
|
assert not _looks_invalid_generated_title("Cool")
|
||||||
|
assert not _looks_invalid_generated_title("Repo Metrics Overview")
|
||||||
|
|
||||||
|
def test_snippet_extraction_skips_tool_preamble_echo(self):
|
||||||
|
"""First/latest exchange helpers skip tool rows whose content is echo/CoT."""
|
||||||
|
from api.streaming import _first_exchange_snippets, _latest_exchange_snippets
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
{"role": "user", "content": "ping"},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "Let me check my memory first.",
|
||||||
|
"tool_calls": [{"id": "1", "function": {"name": "memory"}}],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "pong",
|
||||||
|
"tool_calls": [{"id": "2", "function": {"name": "echo"}}],
|
||||||
|
},
|
||||||
|
{"role": "assistant", "content": "Repo metrics look healthy overall."},
|
||||||
|
]
|
||||||
|
u, a = _first_exchange_snippets(messages)
|
||||||
|
assert u == "ping"
|
||||||
|
assert "healthy" in a.lower()
|
||||||
|
assert "pong" not in a.lower()
|
||||||
|
assert "let me" not in a.lower()
|
||||||
|
|
||||||
|
latest = [
|
||||||
|
{"role": "user", "content": "status?"},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "Done",
|
||||||
|
"tool_calls": [{"id": "9", "function": {"name": "status"}}],
|
||||||
|
},
|
||||||
|
{"role": "assistant", "content": "All services green."},
|
||||||
|
]
|
||||||
|
u2, a2 = _latest_exchange_snippets(latest)
|
||||||
|
assert u2 == "status?"
|
||||||
|
assert "green" in a2.lower()
|
||||||
Reference in new issue
Block a user