"""Discord bot implementation for MyOrg Assistant.""" import discord from discord.ext import commands from typing import Optional from src.config import settings from src.agent.core import MyOrgAgent from src.agent.prompts import get_system_prompt from src.tools.file_ops import get_file_operation_tools from src.tools.task_ops import get_task_management_tools from src.tools.calendar_ops import get_calendar_tools from src.tools.git_ops import get_git_tools from src.bot.formatters import format_response_for_discord class MyOrgBot(commands.Bot): """Discord bot for MyOrg Assistant.""" def __init__(self) -> None: """Initialize the bot.""" # Set up intents intents = discord.Intents.default() intents.message_content = True intents.members = True # Initialize bot super().__init__( command_prefix='/', intents=intents, help_command=None, # We'll create our own ) # Initialize agent self.agent = MyOrgAgent( system_prompt=get_system_prompt(), tools=( get_file_operation_tools() + get_task_management_tools() + get_calendar_tools() + get_git_tools() ), ) # Track user sessions (one agent per user) self.user_agents: dict[int, MyOrgAgent] = {} def get_user_agent(self, user_id: int) -> MyOrgAgent: """Get or create an agent for a specific user. Args: user_id: Discord user ID Returns: MyOrgAgent instance for this user """ if user_id not in self.user_agents: self.user_agents[user_id] = MyOrgAgent( system_prompt=get_system_prompt(), tools=( get_file_operation_tools() + get_task_management_tools() + get_calendar_tools() + get_git_tools() ), ) return self.user_agents[user_id] async def on_ready(self) -> None: """Called when bot is ready.""" print(f'✅ Logged in as {self.user}') print(f'📊 Connected to {len(self.guilds)} server(s)') print(f'🤖 Agent initialized with {len(self.agent.tools)} tools') print('🎉 MyOrg Assistant is ready!') async def on_message(self, message: discord.Message) -> None: """Handle incoming messages. Args: message: Discord message object """ # Ignore messages from the bot itself if message.author == self.user: return # Ignore messages not mentioning the bot (unless in DM) if not isinstance(message.channel, discord.DMChannel): if not self.user.mentioned_in(message): return # Process commands first await self.process_commands(message) # If message wasn't a command, treat as natural conversation if not message.content.startswith('/'): await self.handle_conversation(message) async def handle_conversation(self, message: discord.Message) -> None: """Handle natural language conversation. Args: message: Discord message object """ # Remove bot mention from message content = message.content if self.user.mentioned_in(message): content = content.replace(f'<@{self.user.id}>', '').strip() if not content: return # Show typing indicator async with message.channel.typing(): try: # Get user's agent agent = self.get_user_agent(message.author.id) # Process message response = agent.run(content) # Format and send response formatted = format_response_for_discord(response) # Split if too long (Discord limit is 2000 chars) if len(formatted) <= 2000: await message.reply(formatted) else: # Split into chunks chunks = [formatted[i:i+1900] for i in range(0, len(formatted), 1900)] for chunk in chunks: await message.channel.send(chunk) except Exception as e: await message.reply(f'❌ Error: {str(e)}') async def setup_hook(self) -> None: """Set up bot commands.""" # Commands will be added via decorators below pass # Create bot instance bot = MyOrgBot() @bot.command(name='help') async def help_command(ctx: commands.Context) -> None: """Show help information.""" help_text = """ **🤖 MyOrg Assistant - Help** **Natural Conversation:** Just mention me or DM me to interact naturally! - "Add task: Buy milk tomorrow" - "What should I work on now?" - "Show my tasks for project myorg-assistant" **Commands:** `/help` - Show this help message `/briefing` - Get daily briefing (calendar + priority tasks) `/add [task]` - Quick task addition `/tasks [filter]` - Show tasks (optionally filtered) `/today` - Today's calendar and priority tasks `/context [context]` - Set your current context `/reset` - Clear conversation history **Examples:** `/add Buy groceries @recados due:2026-02-01` `/tasks project:myorg-assistant` `/context computer-deep` **Contexts:** `@computer-deep` - Deep focus work `@computer-light` - Light computer work `@telefon` - Calls/meetings `@recados` - Errands `@bcn` - Barcelona location `@personal` - Personal activities """ await ctx.send(help_text) @bot.command(name='briefing') async def briefing_command(ctx: commands.Context) -> None: """Get daily briefing.""" async with ctx.typing(): try: agent = bot.get_user_agent(ctx.author.id) prompt = """Generate a morning briefing for today: 1. Read calendar.txt and show today's events 2. Read todo.txt and show priority A and B tasks 3. Show tasks with due dates in the next 3 days 4. Format as a nice daily briefing""" response = agent.run(prompt) formatted = format_response_for_discord(response) await ctx.send(formatted) except Exception as e: await ctx.send(f'❌ Error generating briefing: {str(e)}') @bot.command(name='add') async def add_command(ctx: commands.Context, *, task: str) -> None: """Quick task addition. Args: task: Task description with optional metadata """ async with ctx.typing(): try: agent = bot.get_user_agent(ctx.author.id) prompt = f"""Add this task to todo.txt: {task} Parse the task description and extract: - Description (main text) - Project tags (words starting with +) - Context tags (words starting with @) - Due date (due:YYYY-MM-DD format) - Priority if implied Then add the task using the add_task tool and commit the change.""" response = agent.run(prompt) formatted = format_response_for_discord(response) await ctx.send(formatted) except Exception as e: await ctx.send(f'❌ Error adding task: {str(e)}') @bot.command(name='tasks') async def tasks_command(ctx: commands.Context, *, filters: Optional[str] = None) -> None: """Show tasks with optional filters. Args: filters: Optional filter string (e.g., "project:myorg-assistant context:computer-deep") """ async with ctx.typing(): try: agent = bot.get_user_agent(ctx.author.id) if filters: prompt = f"""Show tasks from todo.txt with these filters: {filters} Parse the filter string and use the search_tasks tool appropriately.""" else: prompt = "Show all active tasks from todo.txt grouped by priority using get_tasks_by_priority tool." response = agent.run(prompt) formatted = format_response_for_discord(response) await ctx.send(formatted) except Exception as e: await ctx.send(f'❌ Error fetching tasks: {str(e)}') @bot.command(name='today') async def today_command(ctx: commands.Context) -> None: """Show today's calendar and priority tasks.""" async with ctx.typing(): try: agent = bot.get_user_agent(ctx.author.id) prompt = """Show me what's on for today: 1. Read calendar.txt and show today's events 2. Read todo.txt and show priority A tasks 3. Show any tasks due today 4. Format as a concise daily overview""" response = agent.run(prompt) formatted = format_response_for_discord(response) await ctx.send(formatted) except Exception as e: await ctx.send(f'❌ Error: {str(e)}') @bot.command(name='context') async def context_command(ctx: commands.Context, context: str) -> None: """Set current context. Args: context: Context name (without @ prefix) """ # For now, just acknowledge. In Phase 3, we'll track this await ctx.send(f'✅ Context set to `@{context}`') @bot.command(name='reset') async def reset_command(ctx: commands.Context) -> None: """Reset conversation history.""" agent = bot.get_user_agent(ctx.author.id) agent.reset_conversation() await ctx.send('🔄 Conversation history cleared!') def run_bot() -> None: """Run the Discord bot.""" if not settings.discord_bot_token: raise ValueError("DISCORD_BOT_TOKEN not set in environment") print("🚀 Starting MyOrg Discord Bot...") bot.run(settings.discord_bot_token)