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:
2025-07-04 21:33:27 -07:00
commit f22a68afa6
42 changed files with 10456 additions and 0 deletions

290
src/bot/discord_client.py Normal file
View 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")