From b568222e88e9b2b229612e0840d1fac2374c9b3c Mon Sep 17 00:00:00 2001 From: mruwnik Date: Mon, 3 Nov 2025 19:09:53 +0000 Subject: [PATCH] list discord command --- src/memory/common/db/models/discord.py | 77 ++- src/memory/common/db/models/source_items.py | 11 +- src/memory/discord/commands.py | 628 +++++++++++++------- src/memory/workers/tasks/discord.py | 5 + 4 files changed, 488 insertions(+), 233 deletions(-) diff --git a/src/memory/common/db/models/discord.py b/src/memory/common/db/models/discord.py index ff4a405..d6d54b4 100644 --- a/src/memory/common/db/models/discord.py +++ b/src/memory/common/db/models/discord.py @@ -51,21 +51,40 @@ class MessageProcessor: ), ) - def as_xml(self) -> str: - return ( - textwrap.dedent(""" - <{type}> - {name} - {summary} - - """) - .format( - type=self.__class__.__tablename__[8:], # type: ignore - name=getattr(self, "name", None) or getattr(self, "username", None), - summary=self.summary, - ) - .strip() - ) + @property + def entity_type(self) -> str: + return self.__class__.__tablename__[8:-1] # type: ignore + + def to_xml(self, fields: list[str]) -> str: + def indent(key: str, text: str) -> str: + res = textwrap.dedent(""" + <{key}> + {text} + + """).format(key=key, text=textwrap.indent(text, " ")) + return res.strip() + + vals = [] + if "name" in fields: + vals.append(indent("name", self.name)) + if "system_prompt" in fields: + vals.append(indent("system_prompt", self.system_prompt or "")) + if "summary" in fields: + vals.append(indent("summary", self.summary or "")) + if "mcp_servers" in fields: + servers = [s.as_xml() for s in self.mcp_servers] + vals.append(indent("mcp_servers", "\n".join(servers))) + + return indent(self.entity_type, "\n".join(vals)) # type: ignore + + def xml_prompt(self) -> str: + return self.to_xml(["name", "system_prompt"]) if self.system_prompt else "" + + def xml_summary(self) -> str: + return self.to_xml(["name", "summary"]) + + def xml_mcp_servers(self) -> str: + return self.to_xml(["mcp_servers"]) class DiscordServer(Base, MessageProcessor): @@ -130,6 +149,10 @@ class DiscordUser(Base, MessageProcessor): __table_args__ = (Index("discord_users_system_user_idx", "system_user_id"),) + @property + def name(self) -> str: + return self.username + class MCPServer(Base): """MCP server configuration and OAuth state.""" @@ -164,6 +187,30 @@ class MCPServer(Base): __table_args__ = (Index("mcp_state_idx", "state"),) + def as_xml(self) -> str: + tools = "\n".join(f"• {tool}" for tool in self.available_tools).strip() + return textwrap.dedent(""" + + + {name} + + + {mcp_server_url} + + + {client_id} + + + {available_tools} + + + """).format( + name=self.name, + mcp_server_url=self.mcp_server_url, + client_id=self.client_id, + available_tools=tools, + ) + class MCPServerAssignment(Base): """Assignment of MCP servers to entities (users, channels, servers, etc.).""" diff --git a/src/memory/common/db/models/source_items.py b/src/memory/common/db/models/source_items.py index 827b1be..8046ad6 100644 --- a/src/memory/common/db/models/source_items.py +++ b/src/memory/common/db/models/source_items.py @@ -345,11 +345,12 @@ class DiscordMessage(SourceItem): @property def system_prompt(self) -> str: - return ( - (self.from_user and self.from_user.system_prompt) - or (self.channel and self.channel.system_prompt) - or (self.server and self.server.system_prompt) - ) + prompts = [ + (self.from_user and self.from_user.system_prompt), + (self.channel and self.channel.system_prompt), + (self.server and self.server.system_prompt), + ] + return "\n\n".join(p for p in prompts if p) @property def chattiness_threshold(self) -> int: diff --git a/src/memory/discord/commands.py b/src/memory/discord/commands.py index fbc0300..04eec54 100644 --- a/src/memory/discord/commands.py +++ b/src/memory/discord/commands.py @@ -1,5 +1,6 @@ """Lightweight slash-command helpers for the Discord collector.""" +import io import logging from dataclasses import dataclass from typing import Callable, Literal @@ -8,12 +9,34 @@ import discord from sqlalchemy.orm import Session from memory.common.db.connection import make_session -from memory.common.db.models import DiscordChannel, DiscordServer, DiscordUser +from memory.common.db.models import ( + DiscordChannel, + DiscordServer, + DiscordUser, + MCPServer, + MCPServerAssignment, +) from memory.discord.mcp import run_mcp_server_command logger = logging.getLogger(__name__) -ScopeLiteral = Literal["bot", "server", "channel", "user"] +ScopeLiteral = Literal["bot", "me", "server", "channel", "user"] + + +@dataclass +class DiscordObjects: + bot: DiscordUser + server: DiscordServer | None + channel: DiscordChannel | None + user: DiscordUser | None + + @property + def items(self): + items = [self.bot, self.server, self.channel, self.user] + return [item for item in items if item is not None] + + +ListHandler = Callable[[DiscordObjects], str] class CommandError(Exception): @@ -43,12 +66,322 @@ class CommandContext: CommandHandler = Callable[..., CommandResponse] +async def respond( + interaction: discord.Interaction, content: str, ephemeral: bool = True +) -> None: + """Send a response to the interaction, as file if too large.""" + max_length = 1900 + if len(content) <= max_length: + await interaction.response.send_message(content, ephemeral=ephemeral) + return + + file = discord.File(io.BytesIO(content.encode("utf-8")), filename="response.txt") + await interaction.response.send_message( + "Response too large, sending as file:", file=file, ephemeral=ephemeral + ) + + +def with_object_context( + bot: discord.Client, + interaction: discord.Interaction, + handler: ListHandler, + user: discord.User | None, +) -> str: + """Execute handler with Discord objects context.""" + server = interaction.guild + channel = interaction.channel + target_user = user or interaction.user + with make_session() as session: + objects = DiscordObjects( + bot=ensure_user(session, bot.user), + server=server and ensure_server(session, server), + channel=channel and _ensure_channel(session, channel, server and server.id), + user=ensure_user(session, target_user), + ) + return handler(objects) + + +def _create_scope_group( + parent: discord.app_commands.Group, + scope: ScopeLiteral, + name: str, + description: str, +) -> discord.app_commands.Group: + """Create a command group for a scope (bot/me/server/channel). + + Args: + parent: Parent command group + scope: Scope literal (bot, me, server, channel) + name: Group name + description: Group description + """ + group = discord.app_commands.Group( + name=name, description=description, parent=parent + ) + + @group.command(name="prompt", description=f"Manage {name}'s system prompt") + @discord.app_commands.describe(prompt="The system prompt to set") + async def prompt_cmd(interaction: discord.Interaction, prompt: str | None = None): + await _run_interaction_command( + interaction, scope=scope, handler=handle_prompt, prompt=prompt + ) + + @group.command(name="chattiness", description=f"Show/set {name}'s chattiness") + @discord.app_commands.describe(value="Optional new chattiness value (0-100)") + async def chattiness_cmd( + interaction: discord.Interaction, value: int | None = None + ): + await _run_interaction_command( + interaction, scope=scope, handler=handle_chattiness, value=value + ) + + # Ignore command + @group.command(name="ignore", description=f"Toggle bot ignoring {name} messages") + @discord.app_commands.describe(enabled="Whether to ignore messages") + async def ignore_cmd(interaction: discord.Interaction, enabled: bool | None = None): + await _run_interaction_command( + interaction, scope=scope, handler=handle_ignore, ignore_enabled=enabled + ) + + # Summary command + @group.command(name="summary", description=f"Show {name}'s summary") + async def summary_cmd(interaction: discord.Interaction): + await _run_interaction_command(interaction, scope=scope, handler=handle_summary) + + # MCP command + @group.command(name="mcp", description=f"Manage {name}'s MCP servers") + @discord.app_commands.describe( + action="Action to perform", + url="MCP server URL (required for add, delete, connect, tools)", + ) + async def mcp_cmd( + interaction: discord.Interaction, + action: Literal["list", "add", "delete", "connect", "tools"] = "list", + url: str | None = None, + ): + await _run_interaction_command( + interaction, + scope=scope, + handler=handle_mcp_servers, + action=action, + url=url and url.strip(), + ) + + return group + + +def _create_user_scope_group( + parent: discord.app_commands.Group, + name: str, + description: str, +) -> discord.app_commands.Group: + """Create command group for user scope (requires user parameter). + + Args: + parent: Parent command group + name: Group name + description: Group description + """ + group = discord.app_commands.Group( + name=name, description=description, parent=parent + ) + scope = "user" + + @group.command(name="prompt", description=f"Manage {name}'s system prompt") + @discord.app_commands.describe( + user="Target user", prompt="The system prompt to set" + ) + async def prompt_cmd( + interaction: discord.Interaction, user: discord.User, prompt: str | None = None + ): + await _run_interaction_command( + interaction, + scope=scope, + handler=handle_prompt, + target_user=user, + prompt=prompt, + ) + + @group.command(name="chattiness", description=f"Show/set {name}'s chattiness") + @discord.app_commands.describe( + user="Target user", value="Optional new chattiness value (0-100)" + ) + async def chattiness_cmd( + interaction: discord.Interaction, user: discord.User, value: int | None = None + ): + await _run_interaction_command( + interaction, + scope=scope, + handler=handle_chattiness, + target_user=user, + value=value, + ) + + # Ignore command + @group.command(name="ignore", description=f"Toggle bot ignoring {name} messages") + @discord.app_commands.describe( + user="Target user", enabled="Whether to ignore messages" + ) + async def ignore_cmd( + interaction: discord.Interaction, + user: discord.User, + enabled: bool | None = None, + ): + await _run_interaction_command( + interaction, + scope=scope, + handler=handle_ignore, + target_user=user, + ignore_enabled=enabled, + ) + + # Summary command + @group.command(name="summary", description=f"Show {name}'s summary") + @discord.app_commands.describe(user="Target user") + async def summary_cmd(interaction: discord.Interaction, user: discord.User): + await _run_interaction_command( + interaction, scope=scope, handler=handle_summary, target_user=user + ) + + # MCP command + @group.command(name="mcp", description=f"Manage {name}'s MCP servers") + @discord.app_commands.describe( + user="Target user", + action="Action to perform", + url="MCP server URL (required for add, delete, connect, tools)", + ) + async def mcp_cmd( + interaction: discord.Interaction, + user: discord.User, + action: Literal["list", "add", "delete", "connect", "tools"] = "list", + url: str | None = None, + ): + await _run_interaction_command( + interaction, + scope=scope, + handler=handle_mcp_servers, + target_user=user, + action=action, + url=url and url.strip(), + ) + + return group + + +def create_list_group( + bot: discord.Client, parent: discord.app_commands.Group +) -> discord.app_commands.Group: + """Create command group for listing settings. + + Args: + parent: Parent command group + """ + group = discord.app_commands.Group( + name="list", description="List settings", parent=parent + ) + + @group.command(name="prompt", description="List full system prompt") + @discord.app_commands.describe(user="Target user") + async def prompt_cmd( + interaction: discord.Interaction, user: discord.User | None = None + ): + def handler(objects: DiscordObjects) -> str: + prompts = [o.xml_prompt() for o in objects.items if o.system_prompt] + return "\n\n".join(prompts) + + res = with_object_context(bot, interaction, handler, user) + await respond(interaction, res) + + @group.command(name="chattiness", description="Show {name}'s chattiness") + @discord.app_commands.describe(user="Target user") + async def chattiness_cmd( + interaction: discord.Interaction, user: discord.User | None = None + ): + def handler(objects: DiscordObjects) -> str: + values = [ + o.chattiness_threshold + for o in objects.items + if o.chattiness_threshold is not None + ] + val = min(values) if values else 50 + if objects.user: + return f"Total current chattiness for {objects.user.username}: {val}" + return f"Total current chattiness: {val}" + + res = with_object_context(bot, interaction, handler, user) + await respond(interaction, res) + + @group.command( + name="ignore", description="Does this bot ignore messages for this user?" + ) + @discord.app_commands.describe(user="Target user") + async def ignore_cmd( + interaction: discord.Interaction, + user: discord.User | None = None, + ): + def handler(objects: DiscordObjects) -> str: + should_ignore = any(o.ignore_messages for o in objects.items) + if should_ignore: + return f"The bot ignores messages for {objects.user}." + return f"The bot does not ignore messages for {objects.user}." + + res = with_object_context(bot, interaction, handler, user) + await respond(interaction, res) + + @group.command(name="summary", description="Show the full summary") + @discord.app_commands.describe(user="Target user") + async def summary_cmd( + interaction: discord.Interaction, user: discord.User | None = None + ): + def handler(objects: DiscordObjects) -> str: + summaries = [o.xml_summary() for o in objects.items if o.summary] + return "\n\n".join(summaries) + + res = with_object_context(bot, interaction, handler, user) + await respond(interaction, res) + + @group.command(name="mcp", description="All used MCP servers") + @discord.app_commands.describe(user="Target user") + async def mcp_cmd( + interaction: discord.Interaction, user: discord.User | None = None + ): + logger.error(f"Listing MCP servers for {user}") + ids = [ + interaction.guild_id, + interaction.channel_id, + (user or interaction.user).id, + bot.user.id, + ] + with make_session() as session: + mcp_servers = ( + session.query(MCPServer) + .filter( + MCPServerAssignment.entity_id.in_(i for i in ids if i is not None) + ) + .all() + ) + mcp_servers = [mcp_server.as_xml() for mcp_server in mcp_servers] + res = "\n\n".join(mcp_servers) + await respond(interaction, res) + # def handler(objects: DiscordObjects) -> str: + # servers = [s.as_xml() for obj in objects.items for s in obj.mcp_servers] + # return "\n\n".join(servers) if servers else "No MCP servers configured." + + # try: + # res = with_object_context(bot, interaction, handler, user) + # except Exception as exc: + # logger.error(f"Error listing MCP servers: {exc}", exc_info=True) + # return CommandResponse(content="Error listing MCP servers.") + # await respond(interaction, res) + + return group + + def register_slash_commands(bot: discord.Client) -> None: """Register the collector slash commands on the provided bot. Args: bot: Discord bot client - name: Prefix for command names (e.g., "memory" creates "memory_prompt") """ if getattr(bot, "_memory_commands_registered", False): @@ -62,139 +395,21 @@ def register_slash_commands(bot: discord.Client) -> None: tree = bot.tree name = bot.user and bot.user.name.replace("-", "_").lower() - @tree.command( - name=f"{name}_show_prompt", description="Show the current system prompt" + # Create main command group + memory_group = discord.app_commands.Group( + name=name or "memory", description=f"{name} bot configuration and management" ) - @discord.app_commands.describe( - scope="Which configuration to inspect", - user="Target user when the scope is 'user'", - ) - async def show_prompt_command( - interaction: discord.Interaction, - scope: ScopeLiteral, - user: discord.User | None = None, - ) -> None: - await _run_interaction_command( - interaction, - scope=scope, - handler=handle_prompt, - target_user=user, - ) - @tree.command( - name=f"{name}_set_prompt", - description="Set the system prompt for the target", - ) - @discord.app_commands.describe( - scope="Which configuration to modify", - prompt="The system prompt to set", - user="Target user when the scope is 'user'", - ) - async def set_prompt_command( - interaction: discord.Interaction, - scope: ScopeLiteral, - prompt: str, - user: discord.User | None = None, - ) -> None: - await _run_interaction_command( - interaction, - scope=scope, - handler=handle_set_prompt, - target_user=user, - prompt=prompt, - ) + # Create scope groups + _create_scope_group(memory_group, "bot", "bot", "Bot-wide settings") + _create_scope_group(memory_group, "me", "me", "Your personal settings") + _create_scope_group(memory_group, "server", "server", "Server-wide settings") + _create_scope_group(memory_group, "channel", "channel", "Channel-specific settings") + _create_user_scope_group(memory_group, "user", "Manage other users' settings") + create_list_group(bot, memory_group) - @tree.command( - name=f"{name}_chattiness", - description="Show or update the chattiness for the target", - ) - @discord.app_commands.describe( - scope="Which configuration to inspect", - value="Optional new chattiness value between 0 and 100", - user="Target user when the scope is 'user'", - ) - async def chattiness_command( - interaction: discord.Interaction, - scope: ScopeLiteral, - value: int | None = None, - user: discord.User | None = None, - ) -> None: - await _run_interaction_command( - interaction, - scope=scope, - handler=handle_chattiness, - target_user=user, - value=value, - ) - - @tree.command( - name=f"{name}_ignore", - description="Toggle whether the bot should ignore messages for the target", - ) - @discord.app_commands.describe( - scope="Which configuration to modify", - enabled="Optional flag. Leave empty to enable ignoring.", - user="Target user when the scope is 'user'", - ) - async def ignore_command( - interaction: discord.Interaction, - scope: ScopeLiteral, - enabled: bool | None = None, - user: discord.User | None = None, - ) -> None: - await _run_interaction_command( - interaction, - scope=scope, - handler=handle_ignore, - target_user=user, - ignore_enabled=enabled, - ) - - @tree.command( - name=f"{name}_show_summary", - description="Show the stored summary for the target", - ) - @discord.app_commands.describe( - scope="Which configuration to inspect", - user="Target user when the scope is 'user'", - ) - async def summary_command( - interaction: discord.Interaction, - scope: ScopeLiteral, - user: discord.User | None = None, - ) -> None: - await _run_interaction_command( - interaction, - scope=scope, - handler=handle_summary, - target_user=user, - ) - - @tree.command( - name=f"{name}_mcp_servers", - description="Manage MCP servers for a scope", - ) - @discord.app_commands.describe( - scope="Which configuration to modify (server, channel, or user)", - action="Action to perform", - url="MCP server URL (required for add, delete, connect, tools)", - user="Target user when the scope is 'user'", - ) - async def mcp_servers_command( - interaction: discord.Interaction, - scope: ScopeLiteral, - action: Literal["list", "add", "delete", "connect", "tools"] = "list", - url: str | None = None, - user: discord.User | None = None, - ) -> None: - await _run_interaction_command( - interaction, - scope=scope, - handler=handle_mcp_servers, - target_user=user, - action=action, - url=url and url.strip(), - ) + # Register main group + tree.add_command(memory_group) async def _run_interaction_command( @@ -208,17 +423,16 @@ async def _run_interaction_command( """Shared coroutine used by the registered slash commands.""" try: with make_session() as session: - context = _build_context(session, interaction, scope, target_user) + # Get bot from interaction client if needed for bot scope + bot = getattr(interaction, "client", None) + context = _build_context(session, interaction, scope, target_user, bot) response = await handler(context, **handler_kwargs) session.commit() except CommandError as exc: # pragma: no cover - passthrough - await interaction.response.send_message(str(exc), ephemeral=True) + await respond(interaction, str(exc)) return - await interaction.response.send_message( - response.content, - ephemeral=response.ephemeral, - ) + await respond(interaction, response.content, response.ephemeral) def _build_context( @@ -226,60 +440,55 @@ def _build_context( interaction: discord.Interaction, scope: ScopeLiteral, target_user: discord.User | None, + bot: discord.Client | None = None, ) -> CommandContext: - actor = _ensure_user(session, interaction.user) + actor = ensure_user(session, interaction.user) - if scope == "server": + # Determine target and display name based on scope + if scope == "bot": + if not bot or not bot.user: + raise CommandError("Bot user is not available.") + target = ensure_user(session, bot.user) + display_name = f"bot **{bot.user.name}**" + + elif scope == "me": + target = ensure_user(session, interaction.user) + name = target.display_name or target.username + display_name = f"you (**{name}**)" + + elif scope == "server": if interaction.guild is None: raise CommandError("This command can only be used inside a server.") - - target = _ensure_server(session, interaction.guild) + target = ensure_server(session, interaction.guild) display_name = f"server **{target.name}**" - return CommandContext( - session=session, - interaction=interaction, - actor=actor, - scope=scope, - target=target, - display_name=display_name, - ) - if scope == "channel": - channel_obj = interaction.channel - if channel_obj is None or not hasattr(channel_obj, "id"): + elif scope == "channel": + if interaction.channel is None or not hasattr(interaction.channel, "id"): raise CommandError("Unable to determine channel for this interaction.") - - target = _ensure_channel(session, channel_obj, interaction.guild_id) + target = _ensure_channel(session, interaction.channel, interaction.guild_id) display_name = f"channel **#{target.name}**" - return CommandContext( - session=session, - interaction=interaction, - actor=actor, - scope=scope, - target=target, - display_name=display_name, - ) - if scope == "user": - discord_user = target_user or interaction.user - if discord_user is None: + elif scope == "user": + if target_user is None: raise CommandError("A target user is required for this command.") + target = ensure_user(session, target_user) + name = target.display_name or target.username + display_name = f"user **{name}**" - target = _ensure_user(session, discord_user) - display_name = target.display_name or target.username - return CommandContext( - session=session, - interaction=interaction, - actor=actor, - scope=scope, - target=target, - display_name=f"user **{display_name}**", - ) + else: + raise CommandError(f"Unsupported scope '{scope}'.") - raise CommandError(f"Unsupported scope '{scope}'.") + return CommandContext( + session=session, + interaction=interaction, + actor=actor, + scope=scope, + target=target, + display_name=display_name, + ) -def _ensure_server(session: Session, guild: discord.Guild) -> DiscordServer: +def ensure_server(session: Session, guild: discord.Guild) -> DiscordServer: server = session.get(DiscordServer, guild.id) if server is None: server = DiscordServer( @@ -330,7 +539,7 @@ def _ensure_channel( return channel_model -def _ensure_user(session: Session, discord_user: discord.abc.User) -> DiscordUser: +def ensure_user(session: Session, discord_user: discord.abc.User) -> DiscordUser: user = session.get(DiscordUser, discord_user.id) display_name = getattr(discord_user, "display_name", discord_user.name) if user is None: @@ -364,32 +573,23 @@ def _resolve_channel_type(channel: discord.abc.Messageable) -> str: return getattr(getattr(channel, "type", None), "name", "unknown") -def handle_prompt(context: CommandContext) -> CommandResponse: - prompt = getattr(context.target, "system_prompt", None) +async def handle_prompt( + context: CommandContext, *, prompt: str | None = None +) -> CommandResponse: + if prompt is not None: + prompt = prompt or None + setattr(context.target, "system_prompt", prompt) + else: + prompt = getattr(context.target, "system_prompt", None) if prompt: - return CommandResponse( - content=f"Current prompt for {context.display_name}:\n\n{prompt}", - ) - - return CommandResponse( - content=f"No prompt configured for {context.display_name}.", - ) + content = f"Current prompt for {context.display_name}:\n\n{prompt}" + else: + content = f"No prompt configured for {context.display_name}." + return CommandResponse(content=content) -def handle_set_prompt( - context: CommandContext, - *, - prompt: str, -) -> CommandResponse: - setattr(context.target, "system_prompt", prompt) - - return CommandResponse( - content=f"Updated system prompt for {context.display_name}.", - ) - - -def handle_chattiness( +async def handle_chattiness( context: CommandContext, *, value: int | None, @@ -419,7 +619,7 @@ def handle_chattiness( ) -def handle_ignore( +async def handle_ignore( context: CommandContext, *, ignore_enabled: bool | None, @@ -434,7 +634,7 @@ def handle_ignore( ) -def handle_summary(context: CommandContext) -> CommandResponse: +async def handle_summary(context: CommandContext) -> CommandResponse: summary = getattr(context.target, "summary", None) if summary: @@ -454,20 +654,22 @@ async def handle_mcp_servers( url: str | None, ) -> CommandResponse: """Handle MCP server commands for a specific scope.""" - entity_type_map = { + # Map scope to database entity type + entity_type = { + "bot": "DiscordUser", + "me": "DiscordUser", + "user": "DiscordUser", "server": "DiscordServer", "channel": "DiscordChannel", - "user": "DiscordUser", - } - entity_type = entity_type_map[context.scope] - entity_id = context.target.id + }[context.scope] + + bot_user = getattr(getattr(context.interaction, "client", None), "user", None) + try: res = await run_mcp_server_command( - context.interaction.user, action, url, entity_type, entity_id + bot_user, action, url, entity_type, context.target.id ) return CommandResponse(content=res) except Exception as exc: - import traceback - - logger.error(f"Error running MCP server command: {traceback.format_exc()}") + logger.error(f"Error running MCP server command: {exc}", exc_info=True) raise CommandError(f"Error: {exc}") from exc diff --git a/src/memory/workers/tasks/discord.py b/src/memory/workers/tasks/discord.py index 3662926..9c70973 100644 --- a/src/memory/workers/tasks/discord.py +++ b/src/memory/workers/tasks/discord.py @@ -210,6 +210,11 @@ def process_discord_message(message_id: int) -> dict[str, Any]: for server in discord_message.recipient_user.mcp_servers ] + system_prompt = discord_message.system_prompt or "" + system_prompt += comm_channel_prompt( + session, discord_message.recipient_user, discord_message.channel + ) + try: response = call_llm( session,