303 lines
9.7 KiB
Python
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)
|