fix(feishu): avoid breaking tool hint formatting and think stripping
This commit is contained in:
@@ -163,13 +163,7 @@ class AgentLoop:
|
|||||||
"""Remove <think>…</think> blocks that some models embed in content."""
|
"""Remove <think>…</think> blocks that some models embed in content."""
|
||||||
if not text:
|
if not text:
|
||||||
return None
|
return None
|
||||||
# Remove complete think blocks (non-greedy)
|
return re.sub(r"<think>[\s\S]*?</think>", "", text).strip() or None
|
||||||
cleaned = re.sub(r"<think>[\s\S]*?</think>", "", text)
|
|
||||||
# Remove any stray closing tags left without opening
|
|
||||||
cleaned = re.sub(r"</think>", "", cleaned)
|
|
||||||
# Remove any stray opening tag and everything after it (incomplete block)
|
|
||||||
cleaned = re.sub(r"<think>[\s\S]*$", "", cleaned)
|
|
||||||
return cleaned.strip() or None
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _tool_hint(tool_calls: list) -> str:
|
def _tool_hint(tool_calls: list) -> str:
|
||||||
|
|||||||
@@ -1137,6 +1137,52 @@ class FeishuChannel(BaseChannel):
|
|||||||
logger.debug("Bot entered p2p chat (user opened chat window)")
|
logger.debug("Bot entered p2p chat (user opened chat window)")
|
||||||
pass
|
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:
|
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.
|
"""Send tool hint as an interactive card with formatted code block.
|
||||||
|
|
||||||
@@ -1147,9 +1193,8 @@ class FeishuChannel(BaseChannel):
|
|||||||
"""
|
"""
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
# Format: put each tool call on its own line for readability
|
# Put each top-level tool call on its own line without altering commas inside arguments.
|
||||||
# _tool_hint joins multiple calls with ", "
|
formatted_code = self._format_tool_hint_lines(tool_hint)
|
||||||
formatted_code = tool_hint.replace(", ", ",\n") if ", " in tool_hint else tool_hint
|
|
||||||
|
|
||||||
card = {
|
card = {
|
||||||
"config": {"wide_screen_mode": True},
|
"config": {"wide_screen_mode": True},
|
||||||
|
|||||||
@@ -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
|
# 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```"
|
expected_md = "**Tool Calls**\n\n```text\nweb_search(\"query\"),\nread_file(\"/path/to/file\")\n```"
|
||||||
assert content["elements"][0]["content"] == expected_md
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user