"""Discord message handler for @mentions and conversations.""" import asyncio from typing import Optional import discord from bot.claude.client import ClaudeClient class MessageHandler: """Handles Discord messages and coordinates with Claude API.""" def __init__(self, bot: discord.Client, claude_client: ClaudeClient): self.bot = bot self.claude = claude_client # Store conversation history per thread self.conversations: dict[int, list[dict]] = {} async def handle_mention(self, message: discord.Message): """Handle a message that mentions the bot.""" # Don't respond to self if message.author == self.bot.user: return # Extract the actual message content (remove bot mention) content = message.content for mention in message.mentions: if mention == self.bot.user: content = content.replace(f"<@{mention.id}>", "").strip() if not content: await message.reply("Hey! How can I help you?") return # Create a thread for this conversation if not already in one thread = None if isinstance(message.channel, discord.Thread): thread = message.channel else: # Create new thread thread_name = self._generate_thread_name(content) thread = await message.create_thread( name=thread_name, auto_archive_duration=1440 # 24 hours ) # Handle the conversation in the thread await self.handle_conversation(thread, message.author, content) def _generate_thread_name(self, message: str) -> str: """Generate a thread name from the first message.""" # Take first 50 chars, remove newlines name = message[:50].replace("\n", " ").strip() if len(message) > 50: name += "..." return name or "ClaudeTools Conversation" async def handle_conversation( self, thread: discord.Thread, user: discord.User, user_message: str ): """Handle a conversation turn in a thread.""" # Get or initialize conversation history for this thread thread_id = thread.id if thread_id not in self.conversations: self.conversations[thread_id] = [] # Add user message to history self.conversations[thread_id].append({ "role": "user", "content": user_message }) # Send initial "thinking" message thinking_msg = await thread.send("⏳ Thinking...") # Format system prompt channel_name = thread.parent.name if thread.parent else "Unknown" system_prompt = self.claude.format_system_prompt( discord_user=user, channel_name=channel_name, thread_name=thread.name, user_role="unknown" # TODO: Look up actual role from database ) # Stream response from Claude current_content = "" async def update_progress(text: str): """Update the Discord message with progress.""" nonlocal current_content if text.startswith(("🔧", "⚙️", "✅", "❌")): # Tool execution status update await self._safe_edit(thinking_msg, text) else: # Text content update current_content = text await self._safe_edit(thinking_msg, current_content) async def execute_tool(tool_name: str, tool_input: dict) -> str: """Execute a tool call (placeholder for now).""" # TODO: Implement actual tool execution return f"[Tool {tool_name} executed with {tool_input}]" try: # Stream the response final_text, tool_results = await self.claude.stream_response( messages=self.conversations[thread_id], system_prompt=system_prompt, tool_executor=execute_tool, progress_callback=update_progress ) # Format final response response_text = final_text # Add tool results if any if tool_results: response_text += "\n\n**Tools Used:**\n" for result in tool_results: if "error" in result: response_text += f"- ❌ {result['name']}: {result['error']}\n" else: response_text += f"- ✅ {result['name']}\n" # Update final message await self._safe_edit(thinking_msg, response_text) # Add assistant response to history self.conversations[thread_id].append({ "role": "assistant", "content": final_text }) # Trim conversation history if too long (keep last 20 messages) if len(self.conversations[thread_id]) > 20: self.conversations[thread_id] = self.conversations[thread_id][-20:] except Exception as e: error_msg = f"❌ Error: {str(e)}" await self._safe_edit(thinking_msg, error_msg) async def _safe_edit(self, message: discord.Message, content: str): """Safely edit a Discord message, handling 2000 char limit.""" if len(content) <= 2000: try: await message.edit(content=content) except discord.errors.NotFound: # Message was deleted, ignore pass else: # Split into multiple messages chunks = self._split_message(content) try: await message.edit(content=chunks[0]) # Send remaining chunks as new messages for chunk in chunks[1:]: await message.channel.send(chunk) except discord.errors.NotFound: pass def _split_message(self, content: str, max_length: int = 2000) -> list[str]: """Split a message into chunks under max_length.""" if len(content) <= max_length: return [content] chunks = [] current_chunk = "" # Split by lines to avoid breaking mid-sentence lines = content.split("\n") for line in lines: if len(current_chunk) + len(line) + 1 <= max_length: current_chunk += line + "\n" else: if current_chunk: chunks.append(current_chunk.rstrip()) current_chunk = line + "\n" if current_chunk: chunks.append(current_chunk.rstrip()) return chunks