diff --git a/backend/integrations/discord/bot.py b/backend/integrations/discord/bot.py index a83651cb..26ad8dbc 100644 --- a/backend/integrations/discord/bot.py +++ b/backend/integrations/discord/bot.py @@ -18,16 +18,14 @@ def __init__(self, queue_manager: AsyncQueueManager, **kwargs): intents.dm_messages = True super().__init__( - command_prefix="!", + command_prefix=None, intents=intents, **kwargs ) self.queue_manager = queue_manager self.classifier = ClassificationRouter() - self.active_threads: Dict[str, str] = {} # user_id -> thread_id mapping - - # Register queue handlers + self.active_threads: Dict[str, str] = {} self._register_queue_handlers() def _register_queue_handlers(self): @@ -38,16 +36,18 @@ async def on_ready(self): """Bot ready event""" logger.info(f'Enhanced Discord bot logged in as {self.user}') print(f'Bot is ready! Logged in as {self.user}') + try: + synced = await self.tree.sync() + print(f"Synced {len(synced)} slash command(s)") + except Exception as e: + print(f"Failed to sync slash commands: {e}") async def on_message(self, message): - """Enhanced message handling with classification""" + """Handles regular chat messages, but ignores slash commands.""" if message.author == self.user: return - # if message is a command (starts with !) - ctx = await self.get_context(message) - if ctx.command is not None: - await self.invoke(ctx) + if message.interaction_metadata is not None: return try: @@ -60,23 +60,18 @@ async def on_message(self, message): } ) - logger.info(f"Message triage result: {triage_result}") - if triage_result.get("needs_devrel", False): await self._handle_devrel_message(message, triage_result) except Exception as e: logger.error(f"Error processing message: {str(e)}") - + async def _handle_devrel_message(self, message, triage_result: Dict[str, Any]): - """Handle messages that need DevRel intervention""" + """This now handles both new requests and follow-ups in threads.""" try: user_id = str(message.author.id) - - # Get or create thread for this user thread_id = await self._get_or_create_thread(message, user_id) - - # Prepare message for agent processing + agent_message = { "type": "devrel_request", "id": f"discord_{message.id}", @@ -93,93 +88,55 @@ async def _handle_devrel_message(self, message, triage_result: Dict[str, Any]): "display_name": message.author.display_name } } - - # Determine priority based on triage - priority_map = { - "high": QueuePriority.HIGH, - "medium": QueuePriority.MEDIUM, - "low": QueuePriority.LOW + priority_map = {"high": QueuePriority.HIGH, + "medium": QueuePriority.MEDIUM, + "low": QueuePriority.LOW } priority = priority_map.get(triage_result.get("priority"), QueuePriority.MEDIUM) - - # Enqueue for agent processing await self.queue_manager.enqueue(agent_message, priority) - # Send acknowledgment in thread + # --- "PROCESSING" MESSAGE RESTORED --- if thread_id: thread = self.get_channel(int(thread_id)) if thread: await thread.send("I'm processing your request, please hold on...") - + # ------------------------------------ + except Exception as e: logger.error(f"Error handling DevRel message: {str(e)}") async def _get_or_create_thread(self, message, user_id: str) -> Optional[str]: - """Get existing thread or create new one for user""" try: - # Check if user already has an active thread if user_id in self.active_threads: thread_id = self.active_threads[user_id] thread = self.get_channel(int(thread_id)) - - # Verify thread still exists and is active if thread and not thread.archived: return thread_id else: del self.active_threads[user_id] - logger.info(f"Cleaned up archived thread for user {user_id}") - - # Create new thread - thread_name = f"DevRel Chat - {message.author.display_name}" - + + # This part only runs if it's not a follow-up message in an active thread. if isinstance(message.channel, discord.TextChannel): - thread = await message.create_thread( - name=thread_name, - auto_archive_duration=60 # 1 hour - ) - - # Store thread mapping + thread_name = f"DevRel Chat - {message.author.display_name}" + thread = await message.create_thread(name=thread_name, auto_archive_duration=60) self.active_threads[user_id] = str(thread.id) - - # Send welcome message - await thread.send( - f"Hello {message.author.mention}! 👋\n" - f"I'm your DevRel assistant. I can help you with:\n" - f"• Technical questions about Devr.AI\n" - f"• Getting started and onboarding\n" - f"• Searching for information on the web\n" - f"• General developer support\n\n" - f"This thread keeps our conversation organized!" - ) - + await thread.send(f"Hello {message.author.mention}! I've created this thread to help you. How can I assist?") return str(thread.id) - except Exception as e: - logger.error(f"Failed to create thread: {str(e)}") - - return str(message.channel.id) # Fallback to original channel + logger.error(f"Failed to create thread: {e}") + return str(message.channel.id) async def _handle_agent_response(self, response_data: Dict[str, Any]): - """Handle response from DevRel agent""" try: thread_id = response_data.get("thread_id") response_text = response_data.get("response", "") - if not thread_id or not response_text: - logger.warning("Invalid agent response data") return - thread = self.get_channel(int(thread_id)) if thread: - # Split long responses into multiple messages - if len(response_text) > 2000: - chunks = [response_text[i:i+2000] for i in range(0, len(response_text), 2000)] - for chunk in chunks: - await thread.send(chunk) - else: - await thread.send(response_text) + for i in range(0, len(response_text), 2000): + await thread.send(response_text[i:i+2000]) else: logger.error(f"Thread {thread_id} not found for agent response") - except Exception as e: - logger.error(f"Error handling agent response: {str(e)}") + logger.error(f"Error handling agent response: {str(e)}") \ No newline at end of file diff --git a/backend/integrations/discord/cogs.py b/backend/integrations/discord/cogs.py index 1f892953..a92765f9 100644 --- a/backend/integrations/discord/cogs.py +++ b/backend/integrations/discord/cogs.py @@ -1,4 +1,5 @@ import discord +from discord import app_commands from discord.ext import commands, tasks import logging from app.core.orchestration.queue_manager import AsyncQueueManager, QueuePriority @@ -15,17 +16,23 @@ class DevRelCommands(commands.Cog): def __init__(self, bot: DiscordBot, queue_manager: AsyncQueueManager): self.bot = bot self.queue = queue_manager - self.cleanup_expired_tokens.start() + + @commands.Cog.listener() + async def on_ready(self): + if not self.cleanup_expired_tokens.is_running(): + print("--> Starting the token cleanup task...") + self.cleanup_expired_tokens.start() def cog_unload(self): - """Clean up when cog is unloaded""" self.cleanup_expired_tokens.cancel() @tasks.loop(minutes=5) async def cleanup_expired_tokens(self): """Periodic cleanup of expired verification tokens""" try: + print("--> Running token cleanup task...") await cleanup_expired_tokens() + print("--> Token cleanup task finished.") except Exception as e: logger.error(f"Error during token cleanup: {e}") @@ -34,10 +41,9 @@ async def before_cleanup(self): """Wait until the bot is ready before starting cleanup""" await self.bot.wait_until_ready() - @commands.command(name="reset") - async def reset_thread(self, ctx: commands.Context): - """Reset your DevRel thread and memory.""" - user_id = str(ctx.author.id) + @app_commands.command(name="reset", description="Reset your DevRel thread and memory.") + async def reset_thread(self, interaction: discord.Interaction): + user_id = str(interaction.user.id) cleanup = { "type": "clear_thread_memory", "memory_thread_id": user_id, @@ -46,11 +52,10 @@ async def reset_thread(self, ctx: commands.Context): } await self.queue.enqueue(cleanup, QueuePriority.HIGH) self.bot.active_threads.pop(user_id, None) - await ctx.send("Your DevRel thread & memory have been reset! Send another message to start fresh.") + await interaction.response.send_message("Your DevRel thread & memory have been reset!", ephemeral=True) - @commands.command(name="help_devrel") - async def help_devrel(self, ctx: commands.Context): - """Show DevRel assistant help.""" + @app_commands.command(name="help", description="Show DevRel assistant help.") + async def help_devrel(self, interaction: discord.Interaction): embed = discord.Embed( title="DevRel Assistant Help", description="I can help you with Devr.AI related questions!", @@ -59,101 +64,62 @@ async def help_devrel(self, ctx: commands.Context): embed.add_field( name="Commands", value=( - "• `!reset` – Reset your DevRel thread and memory\n" - "• `!help_devrel` – Show this help message\n" - "• `!verify_github` – Link your GitHub account\n" - "• `!verification_status` – Check your verification status\n" + "• `/reset` - Reset your DevRel thread and memory\n" + "• `/help` - Show this help message\n" + "• `/verify_github` - Link your GitHub account\n" + "• `/verification_status` - Check your verification status\n" ), inline=False ) - embed.add_field( - name="Features", - value=( - "• Technical support and troubleshooting\n" - "• Onboarding assistance\n" - "• Web search capabilities\n" - "• Community FAQ answers\n" - ), - inline=False - ) - await ctx.send(embed=embed) + await interaction.response.send_message(embed=embed) - @commands.command(name="verification_status") - async def verification_status(self, ctx: commands.Context): - """Check your GitHub verification status.""" + @app_commands.command(name="verification_status", + description="Check your GitHub verification status.") + async def verification_status(self, interaction: discord.Interaction): try: user_profile = await get_or_create_user_by_discord( - discord_id=str(ctx.author.id), - display_name=ctx.author.display_name, - discord_username=ctx.author.name, - avatar_url=str(ctx.author.avatar.url) if ctx.author.avatar else None, + discord_id=str(interaction.user.id), + display_name=interaction.user.display_name, + discord_username=interaction.user.name, + avatar_url=str(interaction.user.avatar.url) if interaction.user.avatar else None, ) - if user_profile.is_verified and user_profile.github_id: - embed = discord.Embed( - title="✅ Verification Status", - color=discord.Color.green() - ) - embed.add_field( - name="GitHub Account", - value=f"`{user_profile.github_username}`", - inline=True - ) - embed.add_field( - name="Verified At", - value=f"" if user_profile.verified_at else "Unknown", - inline=True - ) - embed.add_field( - name="Status", - value="✅ Verified", - inline=True - ) + embed = discord.Embed(title="✅ Verification Status", + color=discord.Color.green()) + embed.add_field(name="GitHub Account", value=f"`{user_profile.github_username}`", inline=True) + embed.add_field(name="Status", value="✅ Verified", inline=True) else: - embed = discord.Embed( - title="❌ Verification Status", - description="Your GitHub account is not linked.", - color=discord.Color.red() - ) - embed.add_field( - name="Next Steps", - value="Use `!verify_github` to link your GitHub account.", - inline=False - ) - - await ctx.reply(embed=embed) - + embed = discord.Embed(title="❌ Verification Status", + description="Your GitHub account is not linked.", + color=discord.Color.red()) + embed.add_field(name="Next Steps", + value="Use `/verify_github` to link your GitHub account.", + inline=False) + await interaction.response.send_message(embed=embed, ephemeral=True) except Exception as e: - logger.error(f"Error checking verification status for user {ctx.author.id}: {e}") - await ctx.reply("❌ Error checking verification status. Please try again.") + logger.error(f"Error checking verification status: {e}") + await interaction.response.send_message("❌ Error checking verification status.", ephemeral=True) - @commands.command(name="verify_github") - async def verify_github(self, ctx: commands.Context): - """Initiates the GitHub account linking and verification process.""" + @app_commands.command(name="verify_github", description="Link your GitHub account.") + async def verify_github(self, interaction: discord.Interaction): try: - logger.info(f"User {ctx.author.name}({ctx.author.id}) has requested for GitHub verification") - + await interaction.response.defer(ephemeral=True) + user_profile = await get_or_create_user_by_discord( - discord_id=str(ctx.author.id), - display_name=ctx.author.display_name, - discord_username=ctx.author.name, - avatar_url=str(ctx.author.avatar.url) if ctx.author.avatar else None, + discord_id=str(interaction.user.id), + display_name=interaction.user.display_name, + discord_username=interaction.user.name, + avatar_url=str(interaction.user.avatar.url) if interaction.user.avatar else None, ) - if user_profile.is_verified and user_profile.github_id: embed = discord.Embed( title="✅ Already Verified", description=f"Your GitHub account `{user_profile.github_username}` is already linked!", color=discord.Color.green() ) - embed.add_field( - name="Verified At", - value=f"" if user_profile.verified_at else "Unknown", - inline=True - ) - await ctx.reply(embed=embed) + await interaction.followup.send(embed=embed, ephemeral=True) return - + if user_profile.verification_token: embed = discord.Embed( title="⏳ Verification Pending", @@ -165,29 +131,19 @@ async def verify_github(self, ctx: commands.Context): value="Please complete the existing verification or wait for it to expire (5 minutes).", inline=False ) - await ctx.reply(embed=embed) + await interaction.followup.send(embed=embed, ephemeral=True) return - session_id = await create_verification_session(str(ctx.author.id)) + session_id = await create_verification_session(str(interaction.user.id)) if not session_id: raise Exception("Failed to create verification session.") - logger.info(f"Created verification session for user {ctx.author.id}: {session_id[:8]}...") - - if not settings.backend_url: - raise Exception("Backend URL not configured. Please set BACKEND_URL environment variable.") - callback_url = f"{settings.backend_url}/v1/auth/callback?session={session_id}" - logger.info(f"Using callback URL: {callback_url}") - auth_url_data = await login_with_github(redirect_to=callback_url) auth_url = auth_url_data.get("url") - if not auth_url: raise Exception("Failed to generate OAuth URL.") - logger.info(f"Generated OAuth URL for user {ctx.author.id}") - embed = discord.Embed( title="🔗 Link Your GitHub Account", description=( @@ -212,54 +168,16 @@ async def verify_github(self, ctx: commands.Context): ) view = OAuthView(auth_url, "GitHub") - - try: - await ctx.author.send(embed=embed, view=view) - - confirmation_embed = discord.Embed( - title="📨 Check Your DMs", - description="I've sent you a private message with the GitHub verification link.", - color=discord.Color.green() - ) - confirmation_embed.add_field( - name="⏰ Time Limit", - value="Please complete the verification within 5 minutes.", - inline=False - ) - await ctx.reply(embed=confirmation_embed) - - except discord.Forbidden: - embed.add_field( - name="🔒 Privacy Notice", - value="**Please enable DMs for a more secure experience.**", - inline=False - ) - await ctx.reply(embed=embed, view=view) - - except discord.Forbidden: - error_embed = discord.Embed( - title="❌ DM Error", - description=( - "I couldn't send you a DM. Please:\n" - "1. Enable DMs from server members in your privacy settings\n" - "2. Try the command again" - ), - color=discord.Color.red() - ) - await ctx.reply(embed=error_embed) - + await interaction.followup.send(embed=embed, view=view, ephemeral=True) except Exception as e: - logger.error(f"Error in !verify_github for user {ctx.author.id}: {e}", exc_info=True) - + logger.error(f"Error in /verify_github: {e}") error_embed = discord.Embed( title="❌ Verification Error", - description="An error occurred during verification setup. Please try again or contact support.", + description="An error occurred. Please contact an administrator.", color=discord.Color.red() ) - if "Backend URL not configured" in str(e): - error_embed.add_field( - name="Configuration Issue", - value="The bot is not properly configured. Please contact an administrator.", - inline=False - ) - await ctx.reply(embed=error_embed) + await interaction.followup.send(embed=error_embed, ephemeral=True) + +async def setup(bot: commands.Bot): + """This function is called by the bot to load the cog.""" + await bot.add_cog(DevRelCommands(bot, bot.queue_manager)) \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index 94840427..ed59e6af 100644 --- a/backend/main.py +++ b/backend/main.py @@ -12,7 +12,9 @@ from app.core.orchestration.queue_manager import AsyncQueueManager from app.database.weaviate.client import get_weaviate_client from integrations.discord.bot import DiscordBot -from integrations.discord.cogs import DevRelCommands +from discord.ext import commands +# DevRel commands are now loaded dynamically (commented out below) +# from integrations.discord.cogs import DevRelCommands logging.basicConfig( level=logging.INFO, @@ -32,7 +34,6 @@ def __init__(self): self.queue_manager = AsyncQueueManager() self.agent_coordinator = AgentCoordinator(self.queue_manager) self.discord_bot = DiscordBot(self.queue_manager) - self.discord_bot.add_cog(DevRelCommands(self.discord_bot, self.queue_manager)) async def start_background_tasks(self): """Starts the Discord bot and queue workers in the background.""" @@ -42,6 +43,14 @@ async def start_background_tasks(self): await self.test_weaviate_connection() await self.queue_manager.start(num_workers=3) + + # --- Load commands inside the async startup function --- + try: + await self.discord_bot.load_extension("integrations.discord.cogs") + except (ImportError, commands.ExtensionError) as e: + logger.error("Failed to load Discord cog extension: %s", e) + + # Start the bot as a background task. asyncio.create_task( self.discord_bot.start(settings.discord_bot_token) ) @@ -49,13 +58,13 @@ async def start_background_tasks(self): except Exception as e: logger.error(f"Error during background task startup: {e}", exc_info=True) await self.stop_background_tasks() + raise async def test_weaviate_connection(self): """Test Weaviate connection during startup.""" try: async with get_weaviate_client() as client: - is_ready = await client.is_ready() - if is_ready: + if await client.is_ready(): logger.info("Weaviate connection successful and ready") except Exception as e: logger.error(f"Failed to connect to Weaviate: {e}") @@ -64,28 +73,23 @@ async def test_weaviate_connection(self): async def stop_background_tasks(self): """Stops all background tasks and connections gracefully.""" logger.info("Stopping background tasks and closing connections...") - try: if not self.discord_bot.is_closed(): await self.discord_bot.close() logger.info("Discord bot has been closed.") except Exception as e: logger.error(f"Error closing Discord bot: {e}", exc_info=True) - try: await self.queue_manager.stop() logger.info("Queue manager has been stopped.") except Exception as e: logger.error(f"Error stopping queue manager: {e}", exc_info=True) - logger.info("All background tasks and connections stopped.") # --- FASTAPI LIFESPAN AND APP INITIALIZATION --- -# Global application instance app_instance = DevRAIApplication() - @asynccontextmanager async def lifespan(app: FastAPI): """ @@ -104,7 +108,6 @@ async def favicon(): """Return empty favicon to prevent 404 logs""" return Response(status_code=204) - api.include_router(api_router) diff --git a/pyproject.toml b/pyproject.toml index 67971f93..b653f853 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ dependencies = [ "aio-pika (>=9.5.5,<10.0.0)", "uvicorn (>=0.35.0,<0.36.0)", "ddgs (>=9.0.2,<10.0.0)", + "discord-py (>=2.5.2,<3.0.0)", ] [tool.poetry]