refactor(feishu): extract tool hint card sending into dedicated method
- Extract card creation logic into _send_tool_hint_card() helper - Improves code organization and testability - Update tests to use pytest.mark.asyncio for cleaner async testing - Remove redundant asyncio.run() calls in favor of native async test functions
This commit is contained in:
@@ -825,28 +825,7 @@ class FeishuChannel(BaseChannel):
|
|||||||
# Handle tool hint messages as code blocks in interactive cards
|
# Handle tool hint messages as code blocks in interactive cards
|
||||||
if msg.metadata.get("_tool_hint"):
|
if msg.metadata.get("_tool_hint"):
|
||||||
if msg.content and msg.content.strip():
|
if msg.content and msg.content.strip():
|
||||||
# Create a simple card with a code block
|
await self._send_tool_hint_card(receive_id_type, msg.chat_id, msg.content.strip())
|
||||||
code_text = msg.content.strip()
|
|
||||||
# Format tool calls: put each tool on its own line for better readability
|
|
||||||
# _tool_hint uses ", " to join multiple tool calls
|
|
||||||
if ", " in code_text:
|
|
||||||
formatted_code = code_text.replace(", ", ",\n")
|
|
||||||
else:
|
|
||||||
formatted_code = code_text
|
|
||||||
card = {
|
|
||||||
"config": {"wide_screen_mode": True},
|
|
||||||
"elements": [
|
|
||||||
{
|
|
||||||
"tag": "markdown",
|
|
||||||
"content": f"**Tool Calls**\n\n```text\n{formatted_code}\n```"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
await loop.run_in_executor(
|
|
||||||
None, self._send_message_sync,
|
|
||||||
receive_id_type, msg.chat_id, "interactive",
|
|
||||||
json.dumps(card, ensure_ascii=False),
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
for file_path in msg.media:
|
for file_path in msg.media:
|
||||||
@@ -1030,3 +1009,33 @@ class FeishuChannel(BaseChannel):
|
|||||||
"""Ignore p2p-enter events when a user opens a bot chat."""
|
"""Ignore p2p-enter events when a user opens a bot chat."""
|
||||||
logger.debug("Bot entered p2p chat (user opened chat window)")
|
logger.debug("Bot entered p2p chat (user opened chat window)")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
receive_id_type: "chat_id" or "open_id"
|
||||||
|
receive_id: The target chat or user ID
|
||||||
|
tool_hint: Formatted tool hint string (e.g., 'web_search("q"), read_file("path")')
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
card = {
|
||||||
|
"config": {"wide_screen_mode": True},
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"tag": "markdown",
|
||||||
|
"content": f"**Tool Calls**\n\n```text\n{formatted_code}\n```"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
await loop.run_in_executor(
|
||||||
|
None, self._send_message_sync,
|
||||||
|
receive_id_type, receive_id, "interactive",
|
||||||
|
json.dumps(card, ensure_ascii=False),
|
||||||
|
)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import json
|
|||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from pytest import mark
|
||||||
|
|
||||||
from nanobot.bus.events import OutboundMessage
|
from nanobot.bus.events import OutboundMessage
|
||||||
from nanobot.channels.feishu import FeishuChannel
|
from nanobot.channels.feishu import FeishuChannel
|
||||||
@@ -23,7 +24,8 @@ def mock_feishu_channel():
|
|||||||
return channel
|
return channel
|
||||||
|
|
||||||
|
|
||||||
def test_tool_hint_sends_code_message(mock_feishu_channel):
|
@mark.asyncio
|
||||||
|
async def test_tool_hint_sends_code_message(mock_feishu_channel):
|
||||||
"""Tool hint messages should be sent as interactive cards with code blocks."""
|
"""Tool hint messages should be sent as interactive cards with code blocks."""
|
||||||
msg = OutboundMessage(
|
msg = OutboundMessage(
|
||||||
channel="feishu",
|
channel="feishu",
|
||||||
@@ -33,9 +35,7 @@ def test_tool_hint_sends_code_message(mock_feishu_channel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
with patch.object(mock_feishu_channel, '_send_message_sync') as mock_send:
|
with patch.object(mock_feishu_channel, '_send_message_sync') as mock_send:
|
||||||
# Run send in async context
|
await mock_feishu_channel.send(msg)
|
||||||
import asyncio
|
|
||||||
asyncio.run(mock_feishu_channel.send(msg))
|
|
||||||
|
|
||||||
# Verify interactive message with card was sent
|
# Verify interactive message with card was sent
|
||||||
assert mock_send.call_count == 1
|
assert mock_send.call_count == 1
|
||||||
@@ -56,7 +56,8 @@ def test_tool_hint_sends_code_message(mock_feishu_channel):
|
|||||||
assert card["elements"][0]["content"] == expected_md
|
assert card["elements"][0]["content"] == expected_md
|
||||||
|
|
||||||
|
|
||||||
def test_tool_hint_empty_content_does_not_send(mock_feishu_channel):
|
@mark.asyncio
|
||||||
|
async def test_tool_hint_empty_content_does_not_send(mock_feishu_channel):
|
||||||
"""Empty tool hint messages should not be sent."""
|
"""Empty tool hint messages should not be sent."""
|
||||||
msg = OutboundMessage(
|
msg = OutboundMessage(
|
||||||
channel="feishu",
|
channel="feishu",
|
||||||
@@ -66,14 +67,14 @@ def test_tool_hint_empty_content_does_not_send(mock_feishu_channel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
with patch.object(mock_feishu_channel, '_send_message_sync') as mock_send:
|
with patch.object(mock_feishu_channel, '_send_message_sync') as mock_send:
|
||||||
import asyncio
|
await mock_feishu_channel.send(msg)
|
||||||
asyncio.run(mock_feishu_channel.send(msg))
|
|
||||||
|
|
||||||
# Should not send any message
|
# Should not send any message
|
||||||
mock_send.assert_not_called()
|
mock_send.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_tool_hint_without_metadata_sends_as_normal(mock_feishu_channel):
|
@mark.asyncio
|
||||||
|
async def test_tool_hint_without_metadata_sends_as_normal(mock_feishu_channel):
|
||||||
"""Regular messages without _tool_hint should use normal formatting."""
|
"""Regular messages without _tool_hint should use normal formatting."""
|
||||||
msg = OutboundMessage(
|
msg = OutboundMessage(
|
||||||
channel="feishu",
|
channel="feishu",
|
||||||
@@ -83,8 +84,7 @@ def test_tool_hint_without_metadata_sends_as_normal(mock_feishu_channel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
with patch.object(mock_feishu_channel, '_send_message_sync') as mock_send:
|
with patch.object(mock_feishu_channel, '_send_message_sync') as mock_send:
|
||||||
import asyncio
|
await mock_feishu_channel.send(msg)
|
||||||
asyncio.run(mock_feishu_channel.send(msg))
|
|
||||||
|
|
||||||
# Should send as text message (detected format)
|
# Should send as text message (detected format)
|
||||||
assert mock_send.call_count == 1
|
assert mock_send.call_count == 1
|
||||||
@@ -94,7 +94,8 @@ def test_tool_hint_without_metadata_sends_as_normal(mock_feishu_channel):
|
|||||||
assert json.loads(content) == {"text": "Hello, world!"}
|
assert json.loads(content) == {"text": "Hello, world!"}
|
||||||
|
|
||||||
|
|
||||||
def test_tool_hint_multiple_tools_in_one_message(mock_feishu_channel):
|
@mark.asyncio
|
||||||
|
async def test_tool_hint_multiple_tools_in_one_message(mock_feishu_channel):
|
||||||
"""Multiple tool calls should be displayed each on its own line in a code block."""
|
"""Multiple tool calls should be displayed each on its own line in a code block."""
|
||||||
msg = OutboundMessage(
|
msg = OutboundMessage(
|
||||||
channel="feishu",
|
channel="feishu",
|
||||||
@@ -104,8 +105,7 @@ def test_tool_hint_multiple_tools_in_one_message(mock_feishu_channel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
with patch.object(mock_feishu_channel, '_send_message_sync') as mock_send:
|
with patch.object(mock_feishu_channel, '_send_message_sync') as mock_send:
|
||||||
import asyncio
|
await mock_feishu_channel.send(msg)
|
||||||
asyncio.run(mock_feishu_channel.send(msg))
|
|
||||||
|
|
||||||
call_args = mock_send.call_args[0]
|
call_args = mock_send.call_args[0]
|
||||||
msg_type = call_args[2]
|
msg_type = call_args[2]
|
||||||
|
|||||||
Reference in New Issue
Block a user