Files
myorg-assistant/src/bot/discord_bot.py

303 lines
9.7 KiB
Python
Raw Normal View History

2026-02-03 23:50:19 +01:00
"""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)