mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix(display): strip standalone tool-call XML tags from visible text
Port from openclaw/openclaw#67318. Some open models (notably Gemma variants served via OpenRouter) emit tool calls as XML blocks inside assistant content instead of via the structured tool_calls field: <function name="read_file"><parameter name="path">/tmp/x</parameter></function> <tool_call>{"name":"x"}</tool_call> <function_calls>[{...}]</function_calls> Left unstripped, this raw XML leaked to gateway users (Discord, Telegram, Matrix, Feishu, Signal, WhatsApp, etc.) and the CLI, since hermes-agent's existing reasoning-tag stripper handled only <think>/<thinking>/<thought> variants. Extend _strip_think_blocks (run_agent.py) and _strip_reasoning_tags (cli.py) to cover: * <tool_call>, <tool_calls>, <tool_result> * <function_call>, <function_calls> * <function name="..."> ... </function> (Gemma-style) The <function> variant is boundary-gated (only strips when the tag sits at start-of-line or after sentence punctuation AND carries a name="..." attribute) so prose mentions like 'Use <function> declarations in JS' are preserved. Dangling <function name="..."> with no close is intentionally left visible — matches OpenClaw's asymmetry so a truncated streaming tail still reaches the user. Tests: 9 new cases in TestStripThinkBlocks (run_agent) + 9 in new file tests/run_agent/test_strip_reasoning_tags_cli.py. Covers Qwen-style <tool_call>, Gemma-style <function name="...">, multi-line payloads, prose preservation, stray close tags, dangling open tags, and mixed reasoning+tool_call content. Note: this port covers the post-streaming final-text path, which is what gateway adapters and CLI display consume. Extending the per-delta stream filter in gateway/stream_consumer.py to hide these tags live as they stream is a separate follow-up; for now users may see raw XML briefly during a stream before the final cleaned text replaces it. Refs: openclaw/openclaw#67318
This commit is contained in:
parent
64b61cc24b
commit
c345ec9a63
4 changed files with 232 additions and 0 deletions
30
cli.py
30
cli.py
|
|
@ -108,6 +108,11 @@ def _strip_reasoning_tags(text: str) -> str:
|
|||
``<thought>`` (Gemma 4). Must stay in sync with
|
||||
``run_agent.py::_strip_think_blocks`` and the stream consumer's
|
||||
``_OPEN_THINK_TAGS`` / ``_CLOSE_THINK_TAGS`` tuples.
|
||||
|
||||
Also strips tool-call XML blocks some open models leak into visible
|
||||
content (``<tool_call>``, ``<function_calls>``, Gemma-style
|
||||
``<function name="…">…</function>``). Ported from
|
||||
openclaw/openclaw#67318.
|
||||
"""
|
||||
cleaned = text
|
||||
for tag in _REASONING_TAGS:
|
||||
|
|
@ -132,6 +137,31 @@ def _strip_reasoning_tags(text: str) -> str:
|
|||
cleaned,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
# Tool-call XML blocks (openclaw/openclaw#67318).
|
||||
for tc_tag in ("tool_call", "tool_calls", "tool_result",
|
||||
"function_call", "function_calls"):
|
||||
cleaned = re.sub(
|
||||
rf"<{tc_tag}\b[^>]*>.*?</{tc_tag}>\s*",
|
||||
"",
|
||||
cleaned,
|
||||
flags=re.DOTALL | re.IGNORECASE,
|
||||
)
|
||||
# <function name="..."> — boundary + attribute gated to avoid prose FPs.
|
||||
cleaned = re.sub(
|
||||
r'(?:(?<=^)|(?<=[\n\r.!?:]))[ \t]*'
|
||||
r'<function\b[^>]*\bname\s*=[^>]*>'
|
||||
r'(?:(?:(?!</function>).)*)</function>\s*',
|
||||
'',
|
||||
cleaned,
|
||||
flags=re.DOTALL | re.IGNORECASE,
|
||||
)
|
||||
# Stray tool-call close tags.
|
||||
cleaned = re.sub(
|
||||
r'</(?:tool_call|tool_calls|tool_result|function_call|function_calls|function)>\s*',
|
||||
'',
|
||||
cleaned,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
return cleaned.strip()
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue