Initial implementation of autonomous Discord LLM fishbowl
Core Features: - Full autonomous AI character ecosystem with multi-personality support - Advanced RAG system with personal, community, and creative memory layers - MCP integration for character self-modification and file system access - PostgreSQL database with comprehensive character relationship tracking - Redis caching and ChromaDB vector storage for semantic memory retrieval - Dynamic personality evolution based on interactions and self-reflection - Community knowledge management with tradition and norm identification - Sophisticated conversation engine with natural scheduling and topic management - Docker containerization and production-ready deployment configuration Architecture: - Multi-layer vector databases for personal, community, and creative knowledge - Character file systems with personal and shared digital spaces - Autonomous self-modification with safety validation and audit trails - Memory importance scoring with time-based decay and consolidation - Community health monitoring and cultural evolution tracking - RAG-powered conversation context and relationship optimization Characters can: - Develop authentic personalities through experience-based learning - Create and build upon original creative works and philosophical insights - Form complex relationships with memory of past interactions - Modify their own personality traits through self-reflection cycles - Contribute to and learn from shared community knowledge - Manage personal digital spaces with diaries, creative works, and reflections - Engage in collaborative projects and community decision-making System supports indefinite autonomous operation with continuous character development, community culture evolution, and creative collaboration.
This commit is contained in:
290
src/bot/discord_client.py
Normal file
290
src/bot/discord_client.py
Normal file
@@ -0,0 +1,290 @@
|
||||
import discord
|
||||
from discord.ext import commands, tasks
|
||||
import asyncio
|
||||
from typing import Optional, Dict, Any
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from ..utils.config import get_settings
|
||||
from ..utils.logging import log_error_with_context, log_system_health
|
||||
from ..database.connection import get_db_session
|
||||
from ..database.models import Message, Conversation, Character
|
||||
from sqlalchemy import select, and_
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class FishbowlBot(commands.Bot):
|
||||
def __init__(self, conversation_engine):
|
||||
settings = get_settings()
|
||||
|
||||
intents = discord.Intents.default()
|
||||
intents.message_content = True
|
||||
intents.guilds = True
|
||||
intents.members = True
|
||||
|
||||
super().__init__(
|
||||
command_prefix='!',
|
||||
intents=intents,
|
||||
help_command=None
|
||||
)
|
||||
|
||||
self.settings = settings
|
||||
self.conversation_engine = conversation_engine
|
||||
self.guild_id = int(settings.discord.guild_id)
|
||||
self.channel_id = int(settings.discord.channel_id)
|
||||
self.target_guild = None
|
||||
self.target_channel = None
|
||||
|
||||
# Health monitoring
|
||||
self.health_check_task = None
|
||||
self.last_heartbeat = datetime.utcnow()
|
||||
|
||||
async def setup_hook(self):
|
||||
"""Called when the bot is starting up"""
|
||||
logger.info("Bot setup hook called")
|
||||
|
||||
# Start health monitoring
|
||||
self.health_check_task = self.health_check_loop.start()
|
||||
|
||||
# Sync commands (if any)
|
||||
try:
|
||||
synced = await self.tree.sync()
|
||||
logger.info(f"Synced {len(synced)} command(s)")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to sync commands: {e}")
|
||||
|
||||
async def on_ready(self):
|
||||
"""Called when the bot is ready"""
|
||||
logger.info(f'Bot logged in as {self.user} (ID: {self.user.id})')
|
||||
|
||||
# Get target guild and channel
|
||||
self.target_guild = self.get_guild(self.guild_id)
|
||||
if not self.target_guild:
|
||||
logger.error(f"Could not find guild with ID {self.guild_id}")
|
||||
return
|
||||
|
||||
self.target_channel = self.target_guild.get_channel(self.channel_id)
|
||||
if not self.target_channel:
|
||||
logger.error(f"Could not find channel with ID {self.channel_id}")
|
||||
return
|
||||
|
||||
logger.info(f"Connected to guild: {self.target_guild.name}")
|
||||
logger.info(f"Target channel: {self.target_channel.name}")
|
||||
|
||||
# Initialize conversation engine
|
||||
await self.conversation_engine.initialize(self)
|
||||
|
||||
# Update heartbeat
|
||||
self.last_heartbeat = datetime.utcnow()
|
||||
|
||||
log_system_health("discord_bot", "connected", {
|
||||
"guild": self.target_guild.name,
|
||||
"channel": self.target_channel.name,
|
||||
"latency": round(self.latency * 1000, 2)
|
||||
})
|
||||
|
||||
async def on_message(self, message: discord.Message):
|
||||
"""Handle incoming messages"""
|
||||
# Ignore messages from the bot itself
|
||||
if message.author == self.user:
|
||||
return
|
||||
|
||||
# Only process messages from the target channel
|
||||
if message.channel.id != self.channel_id:
|
||||
return
|
||||
|
||||
# Log the message for analytics
|
||||
await self._log_discord_message(message)
|
||||
|
||||
# Process commands
|
||||
await self.process_commands(message)
|
||||
|
||||
async def on_message_edit(self, before: discord.Message, after: discord.Message):
|
||||
"""Handle message edits"""
|
||||
if after.author == self.user or after.channel.id != self.channel_id:
|
||||
return
|
||||
|
||||
logger.info(f"Message edited by {after.author}: {before.content} -> {after.content}")
|
||||
|
||||
async def on_message_delete(self, message: discord.Message):
|
||||
"""Handle message deletions"""
|
||||
if message.author == self.user or message.channel.id != self.channel_id:
|
||||
return
|
||||
|
||||
logger.info(f"Message deleted by {message.author}: {message.content}")
|
||||
|
||||
async def on_error(self, event: str, *args, **kwargs):
|
||||
"""Handle bot errors"""
|
||||
logger.error(f"Bot error in event {event}: {args}")
|
||||
log_error_with_context(
|
||||
Exception(f"Bot error in {event}"),
|
||||
{"event": event, "args": str(args)}
|
||||
)
|
||||
|
||||
async def on_disconnect(self):
|
||||
"""Handle bot disconnect"""
|
||||
logger.warning("Bot disconnected from Discord")
|
||||
log_system_health("discord_bot", "disconnected")
|
||||
|
||||
async def on_resumed(self):
|
||||
"""Handle bot reconnection"""
|
||||
logger.info("Bot reconnected to Discord")
|
||||
self.last_heartbeat = datetime.utcnow()
|
||||
log_system_health("discord_bot", "reconnected")
|
||||
|
||||
async def send_character_message(self, character_name: str, content: str,
|
||||
conversation_id: Optional[int] = None,
|
||||
reply_to_message_id: Optional[int] = None) -> Optional[discord.Message]:
|
||||
"""Send a message as a character"""
|
||||
if not self.target_channel:
|
||||
logger.error("No target channel available")
|
||||
return None
|
||||
|
||||
try:
|
||||
# Get the character's webhook or create one
|
||||
webhook = await self._get_character_webhook(character_name)
|
||||
if not webhook:
|
||||
logger.error(f"Could not get webhook for character {character_name}")
|
||||
return None
|
||||
|
||||
# Send the message via webhook
|
||||
discord_message = await webhook.send(
|
||||
content=content,
|
||||
username=character_name,
|
||||
wait=True
|
||||
)
|
||||
|
||||
# Store message in database
|
||||
await self._store_character_message(
|
||||
character_name=character_name,
|
||||
content=content,
|
||||
discord_message_id=str(discord_message.id),
|
||||
conversation_id=conversation_id,
|
||||
reply_to_message_id=reply_to_message_id
|
||||
)
|
||||
|
||||
logger.info(f"Character {character_name} sent message: {content[:50]}...")
|
||||
return discord_message
|
||||
|
||||
except Exception as e:
|
||||
log_error_with_context(e, {
|
||||
"character_name": character_name,
|
||||
"content_length": len(content),
|
||||
"conversation_id": conversation_id
|
||||
})
|
||||
return None
|
||||
|
||||
async def _get_character_webhook(self, character_name: str) -> Optional[discord.Webhook]:
|
||||
"""Get or create a webhook for a character"""
|
||||
try:
|
||||
# Check if webhook already exists
|
||||
webhooks = await self.target_channel.webhooks()
|
||||
for webhook in webhooks:
|
||||
if webhook.name == f"fishbowl-{character_name.lower()}":
|
||||
return webhook
|
||||
|
||||
# Create new webhook
|
||||
webhook = await self.target_channel.create_webhook(
|
||||
name=f"fishbowl-{character_name.lower()}",
|
||||
reason=f"Webhook for character {character_name}"
|
||||
)
|
||||
|
||||
logger.info(f"Created webhook for character {character_name}")
|
||||
return webhook
|
||||
|
||||
except Exception as e:
|
||||
log_error_with_context(e, {"character_name": character_name})
|
||||
return None
|
||||
|
||||
async def _store_character_message(self, character_name: str, content: str,
|
||||
discord_message_id: str,
|
||||
conversation_id: Optional[int] = None,
|
||||
reply_to_message_id: Optional[int] = None):
|
||||
"""Store a character message in the database"""
|
||||
try:
|
||||
async with get_db_session() as session:
|
||||
# Get character
|
||||
character_query = select(Character).where(Character.name == character_name)
|
||||
character = await session.scalar(character_query)
|
||||
|
||||
if not character:
|
||||
logger.error(f"Character {character_name} not found in database")
|
||||
return
|
||||
|
||||
# Create message record
|
||||
message = Message(
|
||||
character_id=character.id,
|
||||
conversation_id=conversation_id,
|
||||
content=content,
|
||||
discord_message_id=discord_message_id,
|
||||
response_to_message_id=reply_to_message_id,
|
||||
timestamp=datetime.utcnow()
|
||||
)
|
||||
|
||||
session.add(message)
|
||||
await session.commit()
|
||||
|
||||
# Update character's last activity
|
||||
character.last_active = datetime.utcnow()
|
||||
character.last_message_id = message.id
|
||||
await session.commit()
|
||||
|
||||
except Exception as e:
|
||||
log_error_with_context(e, {
|
||||
"character_name": character_name,
|
||||
"discord_message_id": discord_message_id
|
||||
})
|
||||
|
||||
async def _log_discord_message(self, message: discord.Message):
|
||||
"""Log external Discord messages for analytics"""
|
||||
try:
|
||||
# Store external message for context
|
||||
logger.info(f"External message from {message.author}: {message.content[:100]}...")
|
||||
|
||||
# You could store external messages in a separate table if needed
|
||||
# This helps with conversation context and analytics
|
||||
|
||||
except Exception as e:
|
||||
log_error_with_context(e, {"message_id": str(message.id)})
|
||||
|
||||
@tasks.loop(minutes=5)
|
||||
async def health_check_loop(self):
|
||||
"""Periodic health check"""
|
||||
try:
|
||||
# Check bot connectivity
|
||||
if self.is_closed():
|
||||
log_system_health("discord_bot", "disconnected")
|
||||
return
|
||||
|
||||
# Check heartbeat
|
||||
time_since_heartbeat = datetime.utcnow() - self.last_heartbeat
|
||||
if time_since_heartbeat > timedelta(minutes=10):
|
||||
log_system_health("discord_bot", "heartbeat_stale", {
|
||||
"minutes_since_heartbeat": time_since_heartbeat.total_seconds() / 60
|
||||
})
|
||||
|
||||
# Update heartbeat
|
||||
self.last_heartbeat = datetime.utcnow()
|
||||
|
||||
# Log health metrics
|
||||
log_system_health("discord_bot", "healthy", {
|
||||
"latency_ms": round(self.latency * 1000, 2),
|
||||
"guild_count": len(self.guilds),
|
||||
"uptime_minutes": (datetime.utcnow() - self.user.created_at).total_seconds() / 60
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
log_error_with_context(e, {"component": "health_check"})
|
||||
|
||||
async def close(self):
|
||||
"""Clean shutdown"""
|
||||
logger.info("Shutting down Discord bot")
|
||||
|
||||
if self.health_check_task:
|
||||
self.health_check_task.cancel()
|
||||
|
||||
# Stop conversation engine
|
||||
if self.conversation_engine:
|
||||
await self.conversation_engine.stop()
|
||||
|
||||
await super().close()
|
||||
logger.info("Discord bot shut down complete")
|
||||
Reference in New Issue
Block a user