From c334dca50f61a438009e7b60fcebd74049261db0 Mon Sep 17 00:00:00 2001 From: Nikolai WInther Juhl Date: Thu, 7 May 2026 21:14:18 +0200 Subject: [PATCH] =?UTF-8?q?conversation:=20scrub=20OpenClaw=20tool=5Fcode?= =?UTF-8?q?=20fences=20before=20TTS=20=E2=80=94=20strip=20backtick-tool=5F?= =?UTF-8?q?code=20markdown=20fences=20from=20/v1/chat/completions=20respon?= =?UTF-8?q?ses,=20which=20carry=20raw=20LLM=20output=20upstream=20of=20all?= =?UTF-8?q?=20plugin=20hooks;=20fence-only=20output=20becomes=20"OK.",=20s?= =?UTF-8?q?urrounding=20text=20untouched.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude --- custom_components/openclaw/conversation.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/custom_components/openclaw/conversation.py b/custom_components/openclaw/conversation.py index 633a7d1..a2c5dc8 100644 --- a/custom_components/openclaw/conversation.py +++ b/custom_components/openclaw/conversation.py @@ -44,6 +44,23 @@ _LOGGER = logging.getLogger(__name__) +# Strip OpenClaw ```tool_code``` markdown fences from the response text before +# it reaches the Assist pipeline / TTS. The OpenClaw family-memory plugin +# bridge rewrites these fences in the persisted transcript and fires the DB +# write, but the response payload from /v1/chat/completions bypasses all +# plugin hooks (before_message_write fires on persistence only). Without this +# scrubber Voice PE TTS would speak the raw call literally. +_TOOL_CODE_FENCE_RE = re.compile(r"```tool_code\s*\n.*?\n?```", re.DOTALL) + + +def _scrub_tool_code_fences(text: str) -> str: + """Replace ```tool_code``` fences with a short confirmation for TTS.""" + if not text: + return text + scrubbed = _TOOL_CODE_FENCE_RE.sub("", text).strip() + return scrubbed if scrubbed else "OK." + + _VOICE_REQUEST_HEADERS = { "x-openclaw-source": "voice", "x-ha-voice": "true", @@ -282,7 +299,7 @@ async def _get_response( full_response += chunk if full_response: - return full_response + return _scrub_tool_code_fences(full_response) response = await client.async_send_message( message=message, @@ -292,7 +309,7 @@ async def _get_response( agent_id=agent_id, extra_headers=_VOICE_REQUEST_HEADERS, ) - return extract_text_recursive(response) or "" + return _scrub_tool_code_fences(extract_text_recursive(response) or "") @staticmethod def _should_continue(response: str) -> bool: