From 19ae7a167e6818eb7e661e1e979d35d4eddddac0 Mon Sep 17 00:00:00 2001 From: Xubin Ren Date: Sat, 14 Mar 2026 15:40:53 +0000 Subject: [PATCH] fix(feishu): avoid breaking tool hint formatting and think stripping --- nanobot/agent/loop.py | 8 +--- nanobot/channels/feishu.py | 51 +++++++++++++++++++++-- tests/test_feishu_tool_hint_code_block.py | 22 ++++++++++ 3 files changed, 71 insertions(+), 10 deletions(-) diff --git a/nanobot/agent/loop.py b/nanobot/agent/loop.py index 6ebebcd..d644845 100644 --- a/nanobot/agent/loop.py +++ b/nanobot/agent/loop.py @@ -163,13 +163,7 @@ class AgentLoop: """Remove blocks that some models embed in content.""" if not text: return None - # Remove complete think blocks (non-greedy) - cleaned = re.sub(r"[\s\S]*?", "", text) - # Remove any stray closing tags left without opening - cleaned = re.sub(r"", "", cleaned) - # Remove any stray opening tag and everything after it (incomplete block) - cleaned = re.sub(r"[\s\S]*$", "", cleaned) - return cleaned.strip() or None + return re.sub(r"[\s\S]*?", "", text).strip() or None @staticmethod def _tool_hint(tool_calls: list) -> str: diff --git a/nanobot/channels/feishu.py b/nanobot/channels/feishu.py index e3ab8a0..f657359 100644 --- a/nanobot/channels/feishu.py +++ b/nanobot/channels/feishu.py @@ -1137,6 +1137,52 @@ class FeishuChannel(BaseChannel): logger.debug("Bot entered p2p chat (user opened chat window)") pass + @staticmethod + def _format_tool_hint_lines(tool_hint: str) -> str: + """Split tool hints across lines on top-level call separators only.""" + parts: list[str] = [] + buf: list[str] = [] + depth = 0 + in_string = False + quote_char = "" + escaped = False + + for i, ch in enumerate(tool_hint): + buf.append(ch) + + if in_string: + if escaped: + escaped = False + elif ch == "\\": + escaped = True + elif ch == quote_char: + in_string = False + continue + + if ch in {'"', "'"}: + in_string = True + quote_char = ch + continue + + if ch == "(": + depth += 1 + continue + + if ch == ")" and depth > 0: + depth -= 1 + continue + + if ch == "," and depth == 0: + next_char = tool_hint[i + 1] if i + 1 < len(tool_hint) else "" + if next_char == " ": + parts.append("".join(buf).rstrip()) + buf = [] + + if buf: + parts.append("".join(buf).strip()) + + return "\n".join(part for part in parts if part) + async def _send_tool_hint_card(self, receive_id_type: str, receive_id: str, tool_hint: str) -> None: """Send tool hint as an interactive card with formatted code block. @@ -1147,9 +1193,8 @@ class FeishuChannel(BaseChannel): """ loop = asyncio.get_running_loop() - # Format: put each tool call on its own line for readability - # _tool_hint joins multiple calls with ", " - formatted_code = tool_hint.replace(", ", ",\n") if ", " in tool_hint else tool_hint + # Put each top-level tool call on its own line without altering commas inside arguments. + formatted_code = self._format_tool_hint_lines(tool_hint) card = { "config": {"wide_screen_mode": True}, diff --git a/tests/test_feishu_tool_hint_code_block.py b/tests/test_feishu_tool_hint_code_block.py index a3fc024..2a1b812 100644 --- a/tests/test_feishu_tool_hint_code_block.py +++ b/tests/test_feishu_tool_hint_code_block.py @@ -114,3 +114,25 @@ async def test_tool_hint_multiple_tools_in_one_message(mock_feishu_channel): # Each tool call should be on its own line expected_md = "**Tool Calls**\n\n```text\nweb_search(\"query\"),\nread_file(\"/path/to/file\")\n```" assert content["elements"][0]["content"] == expected_md + + +@mark.asyncio +async def test_tool_hint_keeps_commas_inside_arguments(mock_feishu_channel): + """Commas inside a single tool argument must not be split onto a new line.""" + msg = OutboundMessage( + channel="feishu", + chat_id="oc_123456", + content='web_search("foo, bar"), read_file("/path/to/file")', + metadata={"_tool_hint": True} + ) + + with patch.object(mock_feishu_channel, '_send_message_sync') as mock_send: + await mock_feishu_channel.send(msg) + + content = json.loads(mock_send.call_args[0][3]) + expected_md = ( + "**Tool Calls**\n\n```text\n" + "web_search(\"foo, bar\"),\n" + "read_file(\"/path/to/file\")\n```" + ) + assert content["elements"][0]["content"] == expected_md