Files
myorg-assistant/src/bot/discord_bot.py
Roger Oriol 87fb32b559 first commit
2026-02-03 23:50:19 +01:00

303 lines
9.7 KiB
Python

"""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)