fix channel members tool, add debug commands
This commit is contained in:
+99
-2
@@ -49,6 +49,8 @@ logger = logging.getLogger(__name__)
|
||||
# Initialize the bot
|
||||
intents = discord.Intents.default()
|
||||
intents.message_content = True
|
||||
intents.members = True
|
||||
intents.presences = True
|
||||
bot = commands.Bot(command_prefix="!", intents=intents)
|
||||
|
||||
|
||||
@@ -370,6 +372,99 @@ async def lobotomize(ctx: CommandsContext[Bot]) -> None:
|
||||
await ctx.send("All conversation history and memory has been cleared. 🧠✨")
|
||||
|
||||
|
||||
@bot.command(name="debug")
|
||||
async def debug(
|
||||
ctx: CommandsContext[Bot],
|
||||
*,
|
||||
subcommand: str | None = None,
|
||||
) -> None:
|
||||
"""Debug menu for various debugging sub-commands.
|
||||
|
||||
Usage: !debug <subcommand>
|
||||
Available sub-commands:
|
||||
- members: List all members in the current channel
|
||||
- whoami: Show all information the bot has about you
|
||||
- tools: Show the LLM's available tools
|
||||
"""
|
||||
logger.info(
|
||||
"Debug command triggered by %s with subcommand %r", ctx.author.name, subcommand
|
||||
)
|
||||
|
||||
if not subcommand:
|
||||
menu = "Debug Menu:\n\n"
|
||||
menu += "Available sub-commands:\n"
|
||||
menu += "- `members` - List all members in the current channel\n"
|
||||
menu += "- `whoami` - Show all information the bot has about you\n"
|
||||
menu += "- `tools` - Show the LLM's available tools"
|
||||
await ctx.send(menu)
|
||||
return
|
||||
|
||||
if subcommand == "members":
|
||||
result = get_channel_members_impl(ctx.channel)
|
||||
chunk_size = 1900
|
||||
chunks: list[str] = []
|
||||
current_chunk = result
|
||||
while current_chunk:
|
||||
if len(current_chunk) <= chunk_size:
|
||||
chunks.append(current_chunk)
|
||||
break
|
||||
split_pos = current_chunk.rfind("\n", 0, chunk_size)
|
||||
if split_pos == -1:
|
||||
split_pos = chunk_size
|
||||
chunks.append(current_chunk[:split_pos])
|
||||
current_chunk = current_chunk[split_pos:].lstrip("\n")
|
||||
|
||||
for chunk in chunks:
|
||||
await ctx.send(chunk)
|
||||
return
|
||||
|
||||
if subcommand == "whoami":
|
||||
user_info = get_user_info(ctx.author)
|
||||
chunk_size = 1900
|
||||
whoami_chunks: list[str] = []
|
||||
current_chunk = user_info
|
||||
while current_chunk:
|
||||
if len(current_chunk) <= chunk_size:
|
||||
whoami_chunks.append(current_chunk)
|
||||
break
|
||||
split_pos = current_chunk.rfind("\n", 0, chunk_size)
|
||||
if split_pos == -1:
|
||||
split_pos = chunk_size
|
||||
whoami_chunks.append(current_chunk[:split_pos])
|
||||
current_chunk = current_chunk[split_pos:].lstrip("\n")
|
||||
|
||||
for chunk in whoami_chunks:
|
||||
await ctx.send(chunk)
|
||||
return
|
||||
|
||||
if subcommand == "tools":
|
||||
tool_list = "LLM Tools:\n\n"
|
||||
tool_list += f"- `{get_channel_members.name}`\n"
|
||||
tool_list += f" Description: {get_channel_members.description}\n"
|
||||
tool_list += f" Parameters: {get_channel_members.args_schema.model_json_schema()}" # type: ignore
|
||||
chunk_size = 1900
|
||||
tool_chunks: list[str] = []
|
||||
current_chunk = tool_list
|
||||
while current_chunk:
|
||||
if len(current_chunk) <= chunk_size:
|
||||
tool_chunks.append(current_chunk)
|
||||
break
|
||||
split_pos = current_chunk.rfind("\n", 0, chunk_size)
|
||||
if split_pos == -1:
|
||||
split_pos = chunk_size
|
||||
tool_chunks.append(current_chunk[:split_pos])
|
||||
current_chunk = current_chunk[split_pos:].lstrip("\n")
|
||||
|
||||
for chunk in tool_chunks:
|
||||
await ctx.send(chunk)
|
||||
return
|
||||
|
||||
await ctx.send(
|
||||
f"Unknown debug sub-command: `{subcommand}`\n\n"
|
||||
f"Use `!debug` to see available sub-commands.",
|
||||
)
|
||||
|
||||
|
||||
@bot.command(name="voices")
|
||||
async def voices(ctx: CommandsContext[Bot]) -> None:
|
||||
"""List all available TTS voices organized by category."""
|
||||
@@ -377,7 +472,7 @@ async def voices(ctx: CommandsContext[Bot]) -> None:
|
||||
for category, info in VOICES_LIST.items():
|
||||
voice_list += f"{category} ({info['language']}):\n"
|
||||
for v in info["voices"]:
|
||||
voice_list += f" - {v}\n"
|
||||
voice_list += f"- {v}\n"
|
||||
voice_list += "\n"
|
||||
voice_list += "Use `!speak <text> --voice <voice_name>` to choose a voice."
|
||||
|
||||
@@ -514,7 +609,9 @@ async def _speak_with_bot(
|
||||
return get_channel_members_impl(ctx.channel)
|
||||
return f"Unknown tool: {tool_name}"
|
||||
|
||||
async def speak_tool_call_notifier(tool_name: str, tool_args: dict[str, str]) -> None:
|
||||
async def speak_tool_call_notifier(
|
||||
tool_name: str, tool_args: dict[str, str]
|
||||
) -> None:
|
||||
"""Send a notification message when a tool is called."""
|
||||
if tool_name == "get_channel_members":
|
||||
await ctx.send(f"**{bot_name}** is looking at the channel members...")
|
||||
|
||||
+158
-1
@@ -65,7 +65,7 @@ def test_bot_initialized(mock_discord: dict[str, MagicMock]) -> None:
|
||||
|
||||
|
||||
def test_bot_intents_set(mock_discord: dict[str, MagicMock]) -> None:
|
||||
"""Test that message_content intent is enabled."""
|
||||
"""Test that required intents are enabled."""
|
||||
import vibe_bot.main as main_module
|
||||
|
||||
main_module.bot = mock_discord["bot_instance"]
|
||||
@@ -74,6 +74,17 @@ def test_bot_intents_set(mock_discord: dict[str, MagicMock]) -> None:
|
||||
assert main_module.MIN_PERSONALITY_LENGTH == 10
|
||||
|
||||
|
||||
def test_bot_intents_members_and_presence(mock_discord: dict[str, MagicMock]) -> None:
|
||||
"""Test that members and presence intents are enabled."""
|
||||
intents = mock_discord["Intents"].default.return_value
|
||||
intents.message_content = True
|
||||
intents.members = True
|
||||
intents.presences = True
|
||||
assert intents.message_content is True
|
||||
assert intents.members is True
|
||||
assert intents.presences is True
|
||||
|
||||
|
||||
@patch("vibe_bot.main.tts_engine", None)
|
||||
def test_speak_tts_not_initialized(mock_ctx: MagicMock) -> None:
|
||||
"""Test speak command when TTS engine is not initialized."""
|
||||
@@ -804,3 +815,149 @@ def test_history_long_response_chunked(
|
||||
asyncio.run(main_module.history(mock_ctx, bot_name="alfred"))
|
||||
|
||||
assert mock_ctx.send.call_count >= 1
|
||||
|
||||
|
||||
def test_debug_no_subcommand(mock_ctx: MagicMock) -> None:
|
||||
"""Test debug command without subcommand shows menu."""
|
||||
import asyncio
|
||||
|
||||
import vibe_bot.main as main_module
|
||||
|
||||
asyncio.run(main_module.debug(mock_ctx, subcommand=None))
|
||||
|
||||
mock_ctx.send.assert_called_once()
|
||||
call_args = mock_ctx.send.call_args[0][0]
|
||||
assert "Debug Menu" in call_args
|
||||
assert "members" in call_args
|
||||
|
||||
|
||||
def test_debug_members_no_guild(mock_ctx: MagicMock) -> None:
|
||||
"""Test debug members command when channel has no guild."""
|
||||
import asyncio
|
||||
|
||||
import vibe_bot.main as main_module
|
||||
|
||||
mock_ctx.channel.guild = None
|
||||
|
||||
asyncio.run(main_module.debug(mock_ctx, subcommand="members"))
|
||||
|
||||
mock_ctx.send.assert_called_once()
|
||||
call_args = mock_ctx.send.call_args[0][0]
|
||||
assert "No members found in this channel." in call_args
|
||||
|
||||
|
||||
def test_debug_members_with_members(mock_ctx: MagicMock) -> None:
|
||||
"""Test debug members command with channel members."""
|
||||
import asyncio
|
||||
|
||||
import vibe_bot.main as main_module
|
||||
|
||||
mock_member = MagicMock()
|
||||
mock_member.display_name = "Alice"
|
||||
mock_member.name = "alice"
|
||||
mock_member.nick = None
|
||||
mock_member.global_name = None
|
||||
mock_member.status = MagicMock(value="online")
|
||||
|
||||
mock_ctx.channel.guild.members = [mock_member]
|
||||
|
||||
asyncio.run(main_module.debug(mock_ctx, subcommand="members"))
|
||||
|
||||
assert mock_ctx.send.called
|
||||
call_args = mock_ctx.send.call_args[0][0]
|
||||
assert "Alice" in call_args
|
||||
assert "1 total" in call_args
|
||||
|
||||
|
||||
def test_debug_unknown_subcommand(mock_ctx: MagicMock) -> None:
|
||||
"""Test debug command with unknown subcommand."""
|
||||
import asyncio
|
||||
|
||||
import vibe_bot.main as main_module
|
||||
|
||||
asyncio.run(main_module.debug(mock_ctx, subcommand="unknown"))
|
||||
|
||||
mock_ctx.send.assert_called_once()
|
||||
call_args = mock_ctx.send.call_args[0][0]
|
||||
assert "Unknown debug sub-command" in call_args
|
||||
|
||||
|
||||
def test_debug_members_many_chunks(mock_ctx: MagicMock) -> None:
|
||||
"""Test debug members command with many members that exceed 1900 chars."""
|
||||
import asyncio
|
||||
|
||||
import vibe_bot.main as main_module
|
||||
|
||||
mock_members = []
|
||||
for i in range(50):
|
||||
mock_member = MagicMock()
|
||||
mock_member.display_name = f"User{i}_with_a_very_long_display_name"
|
||||
mock_member.name = f"user{i}"
|
||||
mock_member.nick = None
|
||||
mock_member.global_name = None
|
||||
mock_member.status = MagicMock(value="online")
|
||||
mock_members.append(mock_member)
|
||||
|
||||
mock_ctx.channel.guild.members = mock_members
|
||||
|
||||
asyncio.run(main_module.debug(mock_ctx, subcommand="members"))
|
||||
|
||||
assert mock_ctx.send.call_count >= 2
|
||||
first_chunk = mock_ctx.send.call_args_list[0][0][0]
|
||||
assert "Members in this channel (50 total)" in first_chunk
|
||||
|
||||
|
||||
def test_debug_whoami(mock_ctx: MagicMock) -> None:
|
||||
"""Test debug whoami command shows user info."""
|
||||
import asyncio
|
||||
|
||||
import vibe_bot.main as main_module
|
||||
|
||||
asyncio.run(main_module.debug(mock_ctx, subcommand="whoami"))
|
||||
|
||||
mock_ctx.send.assert_called_once()
|
||||
call_args = mock_ctx.send.call_args[0][0]
|
||||
assert "Username: testuser" in call_args
|
||||
assert "User ID: 12345" in call_args
|
||||
assert "Global Name: Test User" in call_args
|
||||
assert "Nickname: tester" in call_args
|
||||
|
||||
|
||||
def test_debug_whoami_minimal(mock_ctx: MagicMock) -> None:
|
||||
"""Test debug whoami command with minimal user data."""
|
||||
import asyncio
|
||||
|
||||
import vibe_bot.main as main_module
|
||||
|
||||
mock_ctx.author.global_name = None
|
||||
mock_ctx.author.nick = None
|
||||
mock_ctx.author.top_role.name = "@everyone"
|
||||
mock_ctx.author.activities = []
|
||||
mock_ctx.author.joined_at = None
|
||||
mock_ctx.author.created_at = None
|
||||
|
||||
asyncio.run(main_module.debug(mock_ctx, subcommand="whoami"))
|
||||
|
||||
mock_ctx.send.assert_called_once()
|
||||
call_args = mock_ctx.send.call_args[0][0]
|
||||
assert "Username: testuser" in call_args
|
||||
assert "User ID: 12345" in call_args
|
||||
assert "Global Name" not in call_args
|
||||
assert "Nickname" not in call_args
|
||||
assert "Activities" not in call_args
|
||||
assert "Joined" not in call_args
|
||||
|
||||
|
||||
def test_debug_tools(mock_ctx: MagicMock) -> None:
|
||||
"""Test debug tools command shows LLM tools."""
|
||||
import asyncio
|
||||
|
||||
import vibe_bot.main as main_module
|
||||
|
||||
asyncio.run(main_module.debug(mock_ctx, subcommand="tools"))
|
||||
|
||||
mock_ctx.send.assert_called_once()
|
||||
call_args = mock_ctx.send.call_args[0][0]
|
||||
assert "LLM Tools" in call_args
|
||||
assert "get_channel_members" in call_args
|
||||
assert "members" in call_args.lower()
|
||||
|
||||
@@ -24,7 +24,7 @@ def test_get_channel_members_impl_returns_formatted_list() -> None:
|
||||
mock_member2.status = MagicMock(value="idle")
|
||||
|
||||
mock_channel = MagicMock()
|
||||
mock_channel.members = [mock_member1, mock_member2]
|
||||
mock_channel.guild.members = [mock_member1, mock_member2]
|
||||
|
||||
result = get_channel_members_impl(mock_channel)
|
||||
|
||||
@@ -38,7 +38,7 @@ def test_get_channel_members_impl_returns_formatted_list() -> None:
|
||||
def test_get_channel_members_impl_empty() -> None:
|
||||
"""Test get_channel_members_impl with no members."""
|
||||
mock_channel = MagicMock()
|
||||
mock_channel.members = []
|
||||
mock_channel.guild.members = []
|
||||
|
||||
result = get_channel_members_impl(mock_channel)
|
||||
|
||||
@@ -55,7 +55,7 @@ def test_get_channel_members_impl_with_global_name() -> None:
|
||||
mock_member.status = MagicMock(value="dnd")
|
||||
|
||||
mock_channel = MagicMock()
|
||||
mock_channel.members = [mock_member]
|
||||
mock_channel.guild.members = [mock_member]
|
||||
|
||||
result = get_channel_members_impl(mock_channel)
|
||||
|
||||
@@ -72,7 +72,7 @@ def test_get_channel_members_impl_no_status() -> None:
|
||||
mock_member.status = None
|
||||
|
||||
mock_channel = MagicMock()
|
||||
mock_channel.members = [mock_member]
|
||||
mock_channel.guild.members = [mock_member]
|
||||
|
||||
result = get_channel_members_impl(mock_channel)
|
||||
|
||||
@@ -83,8 +83,7 @@ def test_get_channel_members_impl_no_status() -> None:
|
||||
def test_get_channel_members_impl_exception() -> None:
|
||||
"""Test get_channel_members_impl handles exceptions gracefully."""
|
||||
mock_channel = MagicMock()
|
||||
mock_channel.members = None
|
||||
type(mock_channel).members = property(
|
||||
type(mock_channel.guild).members = property(
|
||||
lambda self: (_ for _ in ()).throw(Exception("test"))
|
||||
)
|
||||
|
||||
@@ -119,7 +118,7 @@ def test_get_channel_members_impl_sorted_by_display_name() -> None:
|
||||
mock_member_a.status = MagicMock(value="online")
|
||||
|
||||
mock_channel = MagicMock()
|
||||
mock_channel.members = [mock_member_z, mock_member_a]
|
||||
mock_channel.guild.members = [mock_member_z, mock_member_a]
|
||||
|
||||
result = get_channel_members_impl(mock_channel)
|
||||
|
||||
@@ -138,7 +137,7 @@ def test_get_channel_members_impl_no_nick_when_same_as_display() -> None:
|
||||
mock_member.status = MagicMock(value="online")
|
||||
|
||||
mock_channel = MagicMock()
|
||||
mock_channel.members = [mock_member]
|
||||
mock_channel.guild.members = [mock_member]
|
||||
|
||||
result = get_channel_members_impl(mock_channel)
|
||||
|
||||
@@ -179,3 +178,23 @@ def test_format_member_with_all_fields() -> None:
|
||||
assert "(nickname: Al)" in result
|
||||
assert "(global name: Alice Global)" in result
|
||||
assert "[online]" in result
|
||||
|
||||
|
||||
def test_get_channel_members_impl_no_guild() -> None:
|
||||
"""Test get_channel_members_impl when channel has no guild (e.g. DM)."""
|
||||
mock_channel = MagicMock()
|
||||
mock_channel.guild = None
|
||||
|
||||
result = get_channel_members_impl(mock_channel)
|
||||
|
||||
assert "No members found in this channel." in result
|
||||
|
||||
|
||||
def test_get_channel_members_impl_guild_members_none() -> None:
|
||||
"""Test get_channel_members_impl when guild.members is None."""
|
||||
mock_channel = MagicMock()
|
||||
mock_channel.guild.members = None
|
||||
|
||||
result = get_channel_members_impl(mock_channel)
|
||||
|
||||
assert "No members found in this channel." in result
|
||||
|
||||
+2
-2
@@ -44,7 +44,7 @@ def get_channel_members_impl(channel: Any) -> str:
|
||||
|
||||
"""
|
||||
try:
|
||||
members = channel.members
|
||||
members = getattr(channel.guild, "members", None)
|
||||
if not members:
|
||||
return "No members found in this channel."
|
||||
|
||||
@@ -55,7 +55,7 @@ def get_channel_members_impl(channel: Any) -> str:
|
||||
getattr(m, "display_name", "") or getattr(m, "name", "")
|
||||
).lower(),
|
||||
):
|
||||
lines.append(f" - {_format_member(member)}")
|
||||
lines.append(f"- {_format_member(member)}")
|
||||
|
||||
return "\n".join(lines)
|
||||
except Exception:
|
||||
|
||||
Reference in New Issue
Block a user