first commit
This commit is contained in:
190
src/daemon_boyfriend/cogs/ai_chat.py
Normal file
190
src/daemon_boyfriend/cogs/ai_chat.py
Normal file
@@ -0,0 +1,190 @@
|
||||
"""AI Chat cog - handles chat commands and mention responses."""
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
import discord
|
||||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
|
||||
from daemon_boyfriend.config import settings
|
||||
from daemon_boyfriend.services import AIService, ConversationManager, Message
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Discord message character limit
|
||||
MAX_MESSAGE_LENGTH = 2000
|
||||
|
||||
|
||||
def split_message(content: str, max_length: int = MAX_MESSAGE_LENGTH) -> list[str]:
|
||||
"""Split a long message into chunks that fit Discord's limit.
|
||||
|
||||
Tries to split on paragraph breaks, then sentence breaks, then word breaks.
|
||||
"""
|
||||
if len(content) <= max_length:
|
||||
return [content]
|
||||
|
||||
chunks: list[str] = []
|
||||
remaining = content
|
||||
|
||||
while remaining:
|
||||
if len(remaining) <= max_length:
|
||||
chunks.append(remaining)
|
||||
break
|
||||
|
||||
# Find a good split point
|
||||
split_point = max_length
|
||||
|
||||
# Try to split on paragraph break
|
||||
para_break = remaining.rfind("\n\n", 0, max_length)
|
||||
if para_break > max_length // 2:
|
||||
split_point = para_break + 2
|
||||
else:
|
||||
# Try to split on line break
|
||||
line_break = remaining.rfind("\n", 0, max_length)
|
||||
if line_break > max_length // 2:
|
||||
split_point = line_break + 1
|
||||
else:
|
||||
# Try to split on sentence
|
||||
sentence_end = max(
|
||||
remaining.rfind(". ", 0, max_length),
|
||||
remaining.rfind("! ", 0, max_length),
|
||||
remaining.rfind("? ", 0, max_length),
|
||||
)
|
||||
if sentence_end > max_length // 2:
|
||||
split_point = sentence_end + 2
|
||||
else:
|
||||
# Fall back to word break
|
||||
word_break = remaining.rfind(" ", 0, max_length)
|
||||
if word_break > 0:
|
||||
split_point = word_break + 1
|
||||
|
||||
chunks.append(remaining[:split_point].rstrip())
|
||||
remaining = remaining[split_point:].lstrip()
|
||||
|
||||
return chunks
|
||||
|
||||
|
||||
class AIChatCog(commands.Cog):
|
||||
"""AI conversation commands and mention handling."""
|
||||
|
||||
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."""
|
||||
# Ignore messages from bots
|
||||
if message.author.bot:
|
||||
return
|
||||
|
||||
# Check if bot is mentioned
|
||||
if self.bot.user is None or self.bot.user not in message.mentions:
|
||||
return
|
||||
|
||||
# Extract message content without the mention
|
||||
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?")
|
||||
return
|
||||
|
||||
# Show typing indicator while generating response
|
||||
async with message.channel.typing():
|
||||
try:
|
||||
response_text = await self._generate_response(message.author.id, content)
|
||||
|
||||
# Split and send response
|
||||
chunks = split_message(response_text)
|
||||
await message.reply(chunks[0])
|
||||
|
||||
for chunk in chunks[1:]:
|
||||
await message.channel.send(chunk)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Mention response error: {e}", exc_info=True)
|
||||
await message.reply("Sorry, I encountered an error. Please try again.")
|
||||
|
||||
def _extract_message_content(self, message: discord.Message) -> str:
|
||||
"""Extract the actual message content, removing bot mentions."""
|
||||
content = message.content
|
||||
|
||||
# Remove all mentions of the bot
|
||||
if self.bot.user:
|
||||
# Remove <@BOT_ID> and <@!BOT_ID> patterns
|
||||
content = re.sub(
|
||||
rf"<@!?{self.bot.user.id}>",
|
||||
"",
|
||||
content,
|
||||
)
|
||||
|
||||
return content.strip()
|
||||
|
||||
async def _generate_response(self, user_id: int, user_message: str) -> str:
|
||||
"""Generate an AI response for a user message.
|
||||
|
||||
Args:
|
||||
user_id: Discord user ID
|
||||
user_message: The user's message
|
||||
|
||||
Returns:
|
||||
The AI's response text
|
||||
"""
|
||||
# Get conversation history
|
||||
history = self.conversations.get_history(user_id)
|
||||
|
||||
# Add current message to history for the API call
|
||||
messages = history + [Message(role="user", content=user_message)]
|
||||
|
||||
# Generate response
|
||||
response = await self.ai_service.chat(
|
||||
messages=messages,
|
||||
system_prompt=self.ai_service.get_system_prompt(),
|
||||
)
|
||||
|
||||
# Save the exchange to history
|
||||
self.conversations.add_exchange(user_id, user_message, response.content)
|
||||
|
||||
logger.debug(
|
||||
f"Generated response for user {user_id}: "
|
||||
f"{len(response.content)} chars, {response.usage}"
|
||||
)
|
||||
|
||||
return response.content
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
"""Load the AI Chat cog."""
|
||||
await bot.add_cog(AIChatCog(bot))
|
||||
Reference in New Issue
Block a user