Refactor: remove unused cogs and services, simplify architecture

- Remove admin.py and search.py cogs
- Remove searxng.py service and rate_limiter.py utility
- Update bot.py, ai_chat.py, config.py, and ai_service.py
- Update documentation and docker-compose.yml
This commit is contained in:
2026-01-11 18:41:23 +01:00
parent 0607b05e3b
commit b05fa034a6
14 changed files with 182 additions and 801 deletions

View File

@@ -1,137 +0,0 @@
"""Admin cog - administrative commands for the bot."""
import logging
import platform
import sys
from datetime import datetime
import discord
from discord import app_commands
from discord.ext import commands
from daemon_boyfriend.config import settings
from daemon_boyfriend.services import SearXNGService
logger = logging.getLogger(__name__)
class AdminCog(commands.Cog):
"""Administrative commands for bot management."""
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
self.start_time = datetime.now()
@app_commands.command(name="ping", description="Check bot latency")
async def ping(self, interaction: discord.Interaction) -> None:
"""Check the bot's latency."""
latency = round(self.bot.latency * 1000)
await interaction.response.send_message(f"Pong! Latency: {latency}ms")
@app_commands.command(name="status", description="Show bot status and info")
async def status(self, interaction: discord.Interaction) -> None:
"""Show bot status and information."""
# Calculate uptime
uptime = datetime.now() - self.start_time
hours, remainder = divmod(int(uptime.total_seconds()), 3600)
minutes, seconds = divmod(remainder, 60)
uptime_str = f"{hours}h {minutes}m {seconds}s"
# Check SearXNG status
searxng = SearXNGService()
searxng_status = "Online" if await searxng.health_check() else "Offline"
embed = discord.Embed(
title=f"{settings.bot_name} Status",
color=discord.Color.green(),
)
embed.add_field(name="Uptime", value=uptime_str, inline=True)
embed.add_field(name="Latency", value=f"{round(self.bot.latency * 1000)}ms", inline=True)
embed.add_field(name="Guilds", value=str(len(self.bot.guilds)), inline=True)
embed.add_field(name="AI Provider", value=settings.ai_provider, inline=True)
embed.add_field(name="AI Model", value=settings.ai_model, inline=True)
embed.add_field(name="SearXNG", value=searxng_status, inline=True)
embed.add_field(
name="Python", value=f"{sys.version_info.major}.{sys.version_info.minor}", inline=True
)
embed.add_field(name="Discord.py", value=discord.__version__, inline=True)
embed.add_field(name="Platform", value=platform.system(), inline=True)
await interaction.response.send_message(embed=embed)
@app_commands.command(name="provider", description="Show or change AI provider")
@app_commands.describe(provider="The AI provider to switch to (admin only)")
@app_commands.choices(
provider=[
app_commands.Choice(name="OpenAI", value="openai"),
app_commands.Choice(name="OpenRouter", value="openrouter"),
app_commands.Choice(name="Anthropic (Claude)", value="anthropic"),
]
)
async def provider(
self,
interaction: discord.Interaction,
provider: str | None = None,
) -> None:
"""Show current AI provider or change it (info only, actual change requires restart)."""
if provider is None:
# Just show current provider
await interaction.response.send_message(
f"Current AI provider: **{settings.ai_provider}**\nModel: **{settings.ai_model}**",
ephemeral=True,
)
else:
# Inform that changing requires restart
await interaction.response.send_message(
f"To change the AI provider to **{provider}**, update the `AI_PROVIDER` "
f"and corresponding API key in your `.env` file, then restart the bot.",
ephemeral=True,
)
@app_commands.command(name="help", description="Show available commands")
async def help_command(self, interaction: discord.Interaction) -> None:
"""Show help information."""
embed = discord.Embed(
title=f"{settings.bot_name} - Help",
description="Here are the available commands:",
color=discord.Color.blue(),
)
embed.add_field(
name="Chat",
value=(
f"**@{settings.bot_name} <message>** - Chat with me by mentioning me\n"
"**/chat <message>** - Chat using slash command\n"
"**/clear** - Clear your conversation history"
),
inline=False,
)
embed.add_field(
name="Search",
value=("**/search <query>** - Search the web\n**/image <query>** - Search for images"),
inline=False,
)
embed.add_field(
name="Info",
value=(
"**/ping** - Check bot latency\n"
"**/status** - Show bot status\n"
"**/provider** - Show current AI provider\n"
"**/help** - Show this help message"
),
inline=False,
)
embed.set_footer(text=f"Made with love for the MSC group")
await interaction.response.send_message(embed=embed)
async def setup(bot: commands.Bot) -> None:
"""Load the Admin cog."""
await bot.add_cog(AdminCog(bot))

View File

@@ -1,10 +1,9 @@
"""AI Chat cog - handles chat commands and mention responses."""
"""AI Chat cog - handles mention responses."""
import logging
import re
import discord
from discord import app_commands
from discord.ext import commands
from daemon_boyfriend.config import settings
@@ -66,42 +65,13 @@ def split_message(content: str, max_length: int = MAX_MESSAGE_LENGTH) -> list[st
class AIChatCog(commands.Cog):
"""AI conversation commands and mention handling."""
"""AI conversation via mentions."""
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
self.ai_service = AIService()
self.conversations = ConversationManager()
@app_commands.command(name="chat", description="Chat with Daemon Boyfriend")
@app_commands.describe(message="Your message to the bot")
async def chat(self, interaction: discord.Interaction, message: str) -> None:
"""Slash command to chat with the AI."""
await interaction.response.defer(thinking=True)
try:
response_text = await self._generate_response(interaction.user.id, message)
# Split long responses
chunks = split_message(response_text)
await interaction.followup.send(chunks[0])
# Send additional chunks as follow-up messages
for chunk in chunks[1:]:
await interaction.followup.send(chunk)
except Exception as e:
logger.error(f"Chat error: {e}", exc_info=True)
await interaction.followup.send("Sorry, I encountered an error. Please try again.")
@app_commands.command(name="clear", description="Clear your conversation history")
async def clear_history(self, interaction: discord.Interaction) -> None:
"""Clear the user's conversation history."""
self.conversations.clear_history(interaction.user.id)
await interaction.response.send_message(
"Your conversation history has been cleared!", ephemeral=True
)
@commands.Cog.listener()
async def on_message(self, message: discord.Message) -> None:
"""Respond when the bot is mentioned."""
@@ -117,8 +87,8 @@ class AIChatCog(commands.Cog):
content = self._extract_message_content(message)
if not content:
# Just a mention with no message
await message.reply(f"Hey {message.author.display_name}! How can I help you?")
# Just a mention with no message - use configured description
await message.reply(f"Hey {message.author.display_name}! {settings.bot_description}")
return
# Show typing indicator while generating response

View File

@@ -1,144 +0,0 @@
"""Search cog - web search using SearXNG."""
import logging
import discord
from discord import app_commands
from discord.ext import commands
from daemon_boyfriend.services import SearchResponse, SearXNGService
logger = logging.getLogger(__name__)
def create_search_embed(response: SearchResponse) -> discord.Embed:
"""Create a Discord embed for search results.
Args:
response: The search response
Returns:
A formatted Discord embed
"""
embed = discord.Embed(
title=f"Search: {response.query}",
color=discord.Color.blue(),
)
if not response.results:
embed.description = "No results found."
return embed
# Add results as fields
for i, result in enumerate(response.results, 1):
# Truncate content if too long
content = result.content
if len(content) > 200:
content = content[:197] + "..."
embed.add_field(
name=f"{i}. {result.title[:100]}",
value=f"{content}\n[Link]({result.url})",
inline=False,
)
# Add footer with result count
embed.set_footer(text=f"Found {response.number_of_results} results")
return embed
class SearchCog(commands.Cog):
"""Web search commands using SearXNG."""
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
self.search_service = SearXNGService()
@app_commands.command(name="search", description="Search the web")
@app_commands.describe(
query="What to search for",
category="Search category",
)
@app_commands.choices(
category=[
app_commands.Choice(name="General", value="general"),
app_commands.Choice(name="Images", value="images"),
app_commands.Choice(name="News", value="news"),
app_commands.Choice(name="Science", value="science"),
app_commands.Choice(name="IT", value="it"),
app_commands.Choice(name="Videos", value="videos"),
]
)
async def search(
self,
interaction: discord.Interaction,
query: str,
category: str = "general",
) -> None:
"""Search the web using SearXNG."""
await interaction.response.defer(thinking=True)
try:
response = await self.search_service.search(
query=query,
categories=[category],
num_results=5,
)
embed = create_search_embed(response)
await interaction.followup.send(embed=embed)
except Exception as e:
logger.error(f"Search error: {e}", exc_info=True)
await interaction.followup.send(
f"Search failed. Make sure SearXNG is running and accessible."
)
@app_commands.command(name="image", description="Search for images")
@app_commands.describe(query="What to search for")
async def image_search(
self,
interaction: discord.Interaction,
query: str,
) -> None:
"""Search for images using SearXNG."""
await interaction.response.defer(thinking=True)
try:
response = await self.search_service.search(
query=query,
categories=["images"],
num_results=5,
)
if not response.results:
await interaction.followup.send(f"No images found for: {query}")
return
# Create embed with first image
embed = discord.Embed(
title=f"Image search: {query}",
color=discord.Color.purple(),
)
# Add image results
for i, result in enumerate(response.results[:5], 1):
embed.add_field(
name=f"{i}. {result.title[:50]}",
value=f"[View]({result.url})",
inline=True,
)
await interaction.followup.send(embed=embed)
except Exception as e:
logger.error(f"Image search error: {e}", exc_info=True)
await interaction.followup.send(
"Image search failed. Make sure SearXNG is running and accessible."
)
async def setup(bot: commands.Bot) -> None:
"""Load the Search cog."""
await bot.add_cog(SearchCog(bot))