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

View File

829
src/conversation/engine.py Normal file
View File

@@ -0,0 +1,829 @@
import asyncio
import random
import json
from typing import Dict, Any, List, Optional, Set, Tuple
from datetime import datetime, timedelta
from dataclasses import dataclass, asdict
from enum import Enum
import logging
from ..database.connection import get_db_session
from ..database.models import Character as CharacterModel, Conversation, Message, Memory
from ..characters.character import Character
from ..llm.client import llm_client, prompt_manager
from ..llm.prompt_manager import advanced_prompt_manager
from ..utils.config import get_settings, get_character_settings
from ..utils.logging import (log_conversation_event, log_character_action,
log_autonomous_decision, log_error_with_context)
from sqlalchemy import select, and_, or_, func, desc
logger = logging.getLogger(__name__)
class ConversationState(Enum):
IDLE = "idle"
STARTING = "starting"
ACTIVE = "active"
WINDING_DOWN = "winding_down"
PAUSED = "paused"
STOPPED = "stopped"
@dataclass
class ConversationContext:
conversation_id: Optional[int] = None
topic: str = ""
participants: List[str] = None
message_count: int = 0
start_time: datetime = None
last_activity: datetime = None
current_speaker: Optional[str] = None
conversation_type: str = "general"
energy_level: float = 1.0
def __post_init__(self):
if self.participants is None:
self.participants = []
if self.start_time is None:
self.start_time = datetime.utcnow()
if self.last_activity is None:
self.last_activity = datetime.utcnow()
class ConversationEngine:
"""Autonomous conversation engine that manages character interactions"""
def __init__(self):
self.settings = get_settings()
self.character_settings = get_character_settings()
# Engine state
self.state = ConversationState.IDLE
self.characters: Dict[str, Character] = {}
self.active_conversations: Dict[int, ConversationContext] = {}
self.discord_bot = None
# Scheduling
self.scheduler_task = None
self.conversation_task = None
self.is_paused = False
# Configuration
self.min_delay = self.settings.conversation.min_delay_seconds
self.max_delay = self.settings.conversation.max_delay_seconds
self.max_conversation_length = self.settings.conversation.max_conversation_length
self.quiet_hours = (
self.settings.conversation.quiet_hours_start,
self.settings.conversation.quiet_hours_end
)
# Conversation topics
self.available_topics = self.character_settings.conversation_topics
# Statistics
self.stats = {
'conversations_started': 0,
'messages_generated': 0,
'characters_active': 0,
'uptime_start': datetime.utcnow(),
'last_activity': datetime.utcnow()
}
async def initialize(self, discord_bot):
"""Initialize the conversation engine"""
try:
self.discord_bot = discord_bot
# Load characters from database
await self._load_characters()
# Start scheduler
self.scheduler_task = asyncio.create_task(self._scheduler_loop())
# Start main conversation loop
self.conversation_task = asyncio.create_task(self._conversation_loop())
self.state = ConversationState.IDLE
log_conversation_event(
0, "engine_initialized",
list(self.characters.keys()),
{"character_count": len(self.characters)}
)
except Exception as e:
log_error_with_context(e, {"component": "conversation_engine_init"})
raise
async def start_conversation(self, topic: str = None,
forced_participants: List[str] = None) -> Optional[int]:
"""Start a new conversation"""
try:
if self.is_paused or self.state == ConversationState.STOPPED:
return None
# Check if it's quiet hours
if self._is_quiet_hours():
return None
# Select topic
if not topic:
topic = await self._select_conversation_topic()
# Select participants
participants = forced_participants or await self._select_participants(topic)
if len(participants) < 2:
logger.warning("Not enough participants for conversation")
return None
# Create conversation in database
conversation_id = await self._create_conversation(topic, participants)
# Create conversation context
context = ConversationContext(
conversation_id=conversation_id,
topic=topic,
participants=participants,
conversation_type=await self._determine_conversation_type(topic)
)
self.active_conversations[conversation_id] = context
# Choose initial speaker
initial_speaker = await self._choose_initial_speaker(participants, topic)
# Generate opening message
opening_message = await self._generate_opening_message(initial_speaker, topic, context)
if opening_message:
# Send message via Discord bot
await self.discord_bot.send_character_message(
initial_speaker, opening_message, conversation_id
)
# Update context
context.current_speaker = initial_speaker
context.message_count = 1
context.last_activity = datetime.utcnow()
# Store message in database
await self._store_conversation_message(
conversation_id, initial_speaker, opening_message
)
# Update statistics
self.stats['conversations_started'] += 1
self.stats['messages_generated'] += 1
self.stats['last_activity'] = datetime.utcnow()
log_conversation_event(
conversation_id, "conversation_started",
participants,
{"topic": topic, "initial_speaker": initial_speaker}
)
return conversation_id
return None
except Exception as e:
log_error_with_context(e, {
"topic": topic,
"participants": forced_participants
})
return None
async def continue_conversation(self, conversation_id: int) -> bool:
"""Continue an active conversation"""
try:
if conversation_id not in self.active_conversations:
return False
context = self.active_conversations[conversation_id]
# Check if conversation should continue
if not await self._should_continue_conversation(context):
await self._end_conversation(conversation_id)
return False
# Choose next speaker
next_speaker = await self._choose_next_speaker(context)
if not next_speaker:
await self._end_conversation(conversation_id)
return False
# Generate response
response = await self._generate_response(next_speaker, context)
if response:
# Send message
await self.discord_bot.send_character_message(
next_speaker, response, conversation_id
)
# Update context
context.current_speaker = next_speaker
context.message_count += 1
context.last_activity = datetime.utcnow()
# Store message
await self._store_conversation_message(
conversation_id, next_speaker, response
)
# Update character relationships
await self._update_character_relationships(context, next_speaker, response)
# Store memories
await self._store_conversation_memories(context, next_speaker, response)
# Update statistics
self.stats['messages_generated'] += 1
self.stats['last_activity'] = datetime.utcnow()
log_conversation_event(
conversation_id, "message_sent",
[next_speaker],
{"message_length": len(response), "total_messages": context.message_count}
)
return True
return False
except Exception as e:
log_error_with_context(e, {"conversation_id": conversation_id})
return False
async def handle_external_mention(self, message_content: str,
mentioned_characters: List[str], author: str):
"""Handle external mentions of characters"""
try:
for character_name in mentioned_characters:
if character_name in self.characters:
character = self.characters[character_name]
# Decide if character should respond
context = {
'type': 'external_mention',
'content': message_content,
'author': author,
'participants': [character_name]
}
should_respond, reason = await character.should_respond(context)
if should_respond:
# Generate response
response = await character.generate_response(context)
if response:
await self.discord_bot.send_character_message(
character_name, response
)
log_character_action(
character_name, "responded_to_mention",
{"author": author, "response_length": len(response)}
)
except Exception as e:
log_error_with_context(e, {
"mentioned_characters": mentioned_characters,
"author": author
})
async def handle_external_engagement(self, message_content: str, author: str):
"""Handle external user trying to engage characters"""
try:
# Randomly select a character to respond
if self.characters:
responding_character = random.choice(list(self.characters.values()))
context = {
'type': 'external_engagement',
'content': message_content,
'author': author,
'participants': [responding_character.name]
}
should_respond, reason = await responding_character.should_respond(context)
if should_respond:
response = await responding_character.generate_response(context)
if response:
await self.discord_bot.send_character_message(
responding_character.name, response
)
# Possibly start a conversation with other characters
if random.random() < 0.4: # 40% chance
await self.start_conversation(
topic=f"Discussion prompted by: {message_content[:50]}..."
)
except Exception as e:
log_error_with_context(e, {"author": author})
async def trigger_conversation(self, topic: str = None):
"""Manually trigger a conversation"""
try:
conversation_id = await self.start_conversation(topic)
if conversation_id:
log_conversation_event(
conversation_id, "manually_triggered",
self.active_conversations[conversation_id].participants,
{"topic": topic}
)
return conversation_id
return None
except Exception as e:
log_error_with_context(e, {"topic": topic})
return None
async def pause(self):
"""Pause the conversation engine"""
self.is_paused = True
self.state = ConversationState.PAUSED
logger.info("Conversation engine paused")
async def resume(self):
"""Resume the conversation engine"""
self.is_paused = False
self.state = ConversationState.IDLE
logger.info("Conversation engine resumed")
async def stop(self):
"""Stop the conversation engine"""
self.state = ConversationState.STOPPED
# Cancel tasks
if self.scheduler_task:
self.scheduler_task.cancel()
if self.conversation_task:
self.conversation_task.cancel()
# End all active conversations
for conversation_id in list(self.active_conversations.keys()):
await self._end_conversation(conversation_id)
logger.info("Conversation engine stopped")
async def get_status(self) -> Dict[str, Any]:
"""Get engine status"""
uptime = datetime.utcnow() - self.stats['uptime_start']
return {
'status': self.state.value,
'is_paused': self.is_paused,
'active_conversations': len(self.active_conversations),
'loaded_characters': len(self.characters),
'uptime': str(uptime),
'stats': self.stats.copy(),
'next_conversation_in': await self._time_until_next_conversation()
}
async def _load_characters(self):
"""Load characters from database"""
try:
async with get_db_session() as session:
query = select(CharacterModel).where(CharacterModel.is_active == True)
character_models = await session.scalars(query)
for char_model in character_models:
character = Character(char_model)
await character.initialize(llm_client)
self.characters[character.name] = character
self.stats['characters_active'] = len(self.characters)
logger.info(f"Loaded {len(self.characters)} characters")
except Exception as e:
log_error_with_context(e, {"component": "character_loading"})
raise
async def _scheduler_loop(self):
"""Main scheduler loop for autonomous conversations"""
try:
while self.state != ConversationState.STOPPED:
if not self.is_paused and self.state == ConversationState.IDLE:
# Check if we should start a conversation
if await self._should_start_conversation():
await self.start_conversation()
# Check for conversation continuations
for conversation_id in list(self.active_conversations.keys()):
if await self._should_continue_conversation_now(conversation_id):
await self.continue_conversation(conversation_id)
# Random delay between checks
delay = random.uniform(self.min_delay, self.max_delay)
await asyncio.sleep(delay)
except asyncio.CancelledError:
logger.info("Scheduler loop cancelled")
except Exception as e:
log_error_with_context(e, {"component": "scheduler_loop"})
async def _conversation_loop(self):
"""Main conversation management loop"""
try:
while self.state != ConversationState.STOPPED:
# Periodic character self-reflection
if random.random() < 0.1: # 10% chance per cycle
await self._trigger_character_reflection()
# Cleanup old conversations
await self._cleanup_old_conversations()
# Wait before next cycle
await asyncio.sleep(60) # Check every minute
except asyncio.CancelledError:
logger.info("Conversation loop cancelled")
except Exception as e:
log_error_with_context(e, {"component": "conversation_loop"})
async def _should_start_conversation(self) -> bool:
"""Determine if a new conversation should start"""
# Don't start if too many active conversations
if len(self.active_conversations) >= 2:
return False
# Don't start during quiet hours
if self._is_quiet_hours():
return False
# Random chance based on activity level
base_chance = 0.3
# Increase chance if no recent activity
time_since_last = datetime.utcnow() - self.stats['last_activity']
if time_since_last > timedelta(hours=2):
base_chance += 0.4
elif time_since_last > timedelta(hours=1):
base_chance += 0.2
return random.random() < base_chance
async def _should_continue_conversation(self, context: ConversationContext) -> bool:
"""Determine if conversation should continue"""
# Check message limit
if context.message_count >= self.max_conversation_length:
return False
# Check time limit (conversations shouldn't go on forever)
duration = datetime.utcnow() - context.start_time
if duration > timedelta(hours=2):
return False
# Check if it's quiet hours
if self._is_quiet_hours():
return False
# Check energy level
if context.energy_level < 0.2:
return False
# Random natural ending chance
if context.message_count > 10 and random.random() < 0.1:
return False
return True
async def _should_continue_conversation_now(self, conversation_id: int) -> bool:
"""Check if conversation should continue right now"""
if conversation_id not in self.active_conversations:
return False
context = self.active_conversations[conversation_id]
# Check time since last message
time_since_last = datetime.utcnow() - context.last_activity
min_wait = timedelta(seconds=random.uniform(30, 120))
return time_since_last >= min_wait
async def _select_conversation_topic(self) -> str:
"""Select a topic for conversation"""
return random.choice(self.available_topics)
async def _select_participants(self, topic: str) -> List[str]:
"""Select participants for a conversation"""
interested_characters = []
# Find characters interested in the topic
for character in self.characters.values():
if await character._is_interested_in_topic(topic):
interested_characters.append(character.name)
# If not enough interested characters, add random ones
if len(interested_characters) < 2:
all_characters = list(self.characters.keys())
random.shuffle(all_characters)
for char_name in all_characters:
if char_name not in interested_characters:
interested_characters.append(char_name)
if len(interested_characters) >= 3:
break
# Select 2-3 participants
num_participants = min(random.randint(2, 3), len(interested_characters))
return random.sample(interested_characters, num_participants)
def _is_quiet_hours(self) -> bool:
"""Check if it's currently quiet hours"""
current_hour = datetime.now().hour
start_hour, end_hour = self.quiet_hours
if start_hour <= end_hour:
return start_hour <= current_hour <= end_hour
else: # Spans midnight
return current_hour >= start_hour or current_hour <= end_hour
async def _time_until_next_conversation(self) -> str:
"""Calculate time until next conversation attempt"""
if self.is_paused or self._is_quiet_hours():
return "Paused or quiet hours"
# This is a simple estimate
next_check = random.uniform(self.min_delay, self.max_delay)
return f"{int(next_check)} seconds"
async def _create_conversation(self, topic: str, participants: List[str]) -> int:
"""Create conversation in database"""
try:
async with get_db_session() as session:
conversation = Conversation(
channel_id=str(self.discord_bot.channel_id),
topic=topic,
participants=participants,
start_time=datetime.utcnow(),
last_activity=datetime.utcnow(),
is_active=True,
message_count=0
)
session.add(conversation)
await session.commit()
return conversation.id
except Exception as e:
log_error_with_context(e, {"topic": topic, "participants": participants})
raise
async def _determine_conversation_type(self, topic: str) -> str:
"""Determine conversation type based on topic"""
topic_lower = topic.lower()
if any(word in topic_lower for word in ['art', 'music', 'creative', 'design']):
return 'creative'
elif any(word in topic_lower for word in ['problem', 'solve', 'analyze', 'think']):
return 'analytical'
elif any(word in topic_lower for word in ['feel', 'emotion', 'experience', 'personal']):
return 'emotional'
elif any(word in topic_lower for word in ['philosophy', 'meaning', 'existence', 'consciousness']):
return 'philosophical'
else:
return 'general'
async def _choose_initial_speaker(self, participants: List[str], topic: str) -> str:
"""Choose who should start the conversation"""
scores = {}
for participant in participants:
if participant in self.characters:
character = self.characters[participant]
score = 0.5 # Base score
# Higher score if interested in topic
if await character._is_interested_in_topic(topic):
score += 0.3
# Higher score if character is extraverted
if 'extraverted' in character.personality.lower() or 'outgoing' in character.personality.lower():
score += 0.2
scores[participant] = score
# Choose participant with highest score (with some randomness)
if scores:
weighted_choices = [(name, score) for name, score in scores.items()]
return random.choices([name for name, _ in weighted_choices],
weights=[score for _, score in weighted_choices])[0]
return random.choice(participants)
async def _generate_opening_message(self, speaker: str, topic: str, context: ConversationContext) -> Optional[str]:
"""Generate opening message for conversation"""
if speaker not in self.characters:
return None
character = self.characters[speaker]
prompt_context = {
'type': 'initiation',
'topic': topic,
'participants': context.participants,
'conversation_type': context.conversation_type
}
return await character.generate_response(prompt_context)
async def _choose_next_speaker(self, context: ConversationContext) -> Optional[str]:
"""Choose next speaker in conversation"""
participants = context.participants
current_speaker = context.current_speaker
# Don't let same character speak twice in a row (unless only one participant)
if len(participants) > 1:
available = [p for p in participants if p != current_speaker]
else:
available = participants
if not available:
return None
# Score each potential speaker
scores = {}
for participant in available:
if participant in self.characters:
character = self.characters[participant]
# Base response probability
should_respond, _ = await character.should_respond({
'type': 'conversation_continue',
'topic': context.topic,
'participants': context.participants,
'message_count': context.message_count
})
scores[participant] = 1.0 if should_respond else 0.3
if not scores:
return random.choice(available)
# Choose weighted random speaker
weighted_choices = [(name, score) for name, score in scores.items()]
return random.choices([name for name, _ in weighted_choices],
weights=[score for _, score in weighted_choices])[0]
async def _generate_response(self, speaker: str, context: ConversationContext) -> Optional[str]:
"""Generate response for speaker in conversation"""
if speaker not in self.characters:
return None
character = self.characters[speaker]
# Get conversation history
conversation_history = await self._get_conversation_history(context.conversation_id, limit=10)
prompt_context = {
'type': 'response',
'topic': context.topic,
'participants': context.participants,
'conversation_history': conversation_history,
'conversation_type': context.conversation_type,
'message_count': context.message_count
}
return await character.generate_response(prompt_context)
async def _store_conversation_message(self, conversation_id: int, character_name: str, content: str):
"""Store conversation message in database"""
try:
async with get_db_session() as session:
# Get character
character_query = select(CharacterModel).where(CharacterModel.name == character_name)
character = await session.scalar(character_query)
if character:
message = Message(
conversation_id=conversation_id,
character_id=character.id,
content=content,
timestamp=datetime.utcnow()
)
session.add(message)
await session.commit()
except Exception as e:
log_error_with_context(e, {"conversation_id": conversation_id, "character_name": character_name})
async def _get_conversation_history(self, conversation_id: int, limit: int = 10) -> List[Dict[str, Any]]:
"""Get recent conversation history"""
try:
async with get_db_session() as session:
query = select(Message, CharacterModel.name).join(
CharacterModel, Message.character_id == CharacterModel.id
).where(
Message.conversation_id == conversation_id
).order_by(desc(Message.timestamp)).limit(limit)
results = await session.execute(query)
history = []
for message, character_name in results:
history.append({
'character': character_name,
'content': message.content,
'timestamp': message.timestamp
})
return list(reversed(history)) # Return in chronological order
except Exception as e:
log_error_with_context(e, {"conversation_id": conversation_id})
return []
async def _update_character_relationships(self, context: ConversationContext, speaker: str, message: str):
"""Update character relationships based on interaction"""
try:
for participant in context.participants:
if participant != speaker and participant in self.characters:
character = self.characters[speaker]
await character.process_relationship_change(
participant, 'conversation', message
)
except Exception as e:
log_error_with_context(e, {"speaker": speaker, "participants": context.participants})
async def _store_conversation_memories(self, context: ConversationContext, speaker: str, message: str):
"""Store conversation memories for character"""
try:
if speaker in self.characters:
character = self.characters[speaker]
# Store conversation memory
await character._store_memory(
memory_type="conversation",
content=f"In conversation about {context.topic}: {message}",
importance=0.6,
tags=[context.topic, "conversation"] + context.participants
)
except Exception as e:
log_error_with_context(e, {"speaker": speaker, "topic": context.topic})
async def _end_conversation(self, conversation_id: int):
"""End a conversation"""
try:
if conversation_id in self.active_conversations:
context = self.active_conversations[conversation_id]
# Update conversation in database
async with get_db_session() as session:
conversation = await session.get(Conversation, conversation_id)
if conversation:
conversation.is_active = False
conversation.last_activity = datetime.utcnow()
conversation.message_count = context.message_count
await session.commit()
# Remove from active conversations
del self.active_conversations[conversation_id]
log_conversation_event(
conversation_id, "conversation_ended",
context.participants,
{"total_messages": context.message_count, "duration": str(datetime.utcnow() - context.start_time)}
)
except Exception as e:
log_error_with_context(e, {"conversation_id": conversation_id})
async def _trigger_character_reflection(self):
"""Trigger reflection for a random character"""
if self.characters:
character_name = random.choice(list(self.characters.keys()))
character = self.characters[character_name]
reflection_result = await character.self_reflect()
if reflection_result:
log_character_action(
character_name, "completed_reflection",
{"reflection_length": len(reflection_result.get('reflection', ''))}
)
async def _cleanup_old_conversations(self):
"""Clean up old inactive conversations"""
try:
cutoff_time = datetime.utcnow() - timedelta(hours=6)
# Remove old conversations from active list
to_remove = []
for conv_id, context in self.active_conversations.items():
if context.last_activity < cutoff_time:
to_remove.append(conv_id)
for conv_id in to_remove:
await self._end_conversation(conv_id)
except Exception as e:
log_error_with_context(e, {"component": "conversation_cleanup"})

View File

@@ -0,0 +1,442 @@
import asyncio
import random
import schedule
from typing import Dict, Any, List, Optional
from datetime import datetime, timedelta
from dataclasses import dataclass
from enum import Enum
import logging
from ..utils.logging import log_autonomous_decision, log_error_with_context, log_system_health
from ..utils.config import get_settings
logger = logging.getLogger(__name__)
class SchedulerState(Enum):
RUNNING = "running"
PAUSED = "paused"
STOPPED = "stopped"
@dataclass
class ScheduledEvent:
event_type: str
scheduled_time: datetime
character_name: Optional[str] = None
parameters: Dict[str, Any] = None
def __post_init__(self):
if self.parameters is None:
self.parameters = {}
class ConversationScheduler:
"""Advanced scheduler for autonomous conversation events"""
def __init__(self, conversation_engine):
self.engine = conversation_engine
self.settings = get_settings()
self.state = SchedulerState.STOPPED
# Scheduling parameters
self.base_conversation_interval = timedelta(minutes=30)
self.reflection_interval = timedelta(hours=6)
self.relationship_update_interval = timedelta(hours=12)
# Event queue
self.scheduled_events: List[ScheduledEvent] = []
self.scheduler_task = None
# Activity patterns
self.activity_patterns = {
'morning': {'start': 7, 'end': 11, 'activity_multiplier': 1.2},
'afternoon': {'start': 12, 'end': 17, 'activity_multiplier': 1.0},
'evening': {'start': 18, 'end': 22, 'activity_multiplier': 1.5},
'night': {'start': 23, 'end': 6, 'activity_multiplier': 0.3}
}
# Dynamic scheduling weights
self.event_weights = {
'conversation_start': 1.0,
'character_reflection': 0.3,
'relationship_update': 0.2,
'memory_consolidation': 0.1,
'personality_evolution': 0.05
}
async def start(self):
"""Start the scheduler"""
try:
self.state = SchedulerState.RUNNING
# Schedule initial events
await self._schedule_initial_events()
# Start main scheduler loop
self.scheduler_task = asyncio.create_task(self._scheduler_loop())
log_system_health("conversation_scheduler", "started")
except Exception as e:
log_error_with_context(e, {"component": "scheduler_start"})
raise
async def stop(self):
"""Stop the scheduler"""
self.state = SchedulerState.STOPPED
if self.scheduler_task:
self.scheduler_task.cancel()
self.scheduled_events.clear()
log_system_health("conversation_scheduler", "stopped")
async def pause(self):
"""Pause the scheduler"""
self.state = SchedulerState.PAUSED
log_system_health("conversation_scheduler", "paused")
async def resume(self):
"""Resume the scheduler"""
self.state = SchedulerState.RUNNING
log_system_health("conversation_scheduler", "resumed")
async def schedule_event(self, event_type: str, delay: timedelta,
character_name: str = None, **kwargs):
"""Schedule a specific event"""
scheduled_time = datetime.utcnow() + delay
event = ScheduledEvent(
event_type=event_type,
scheduled_time=scheduled_time,
character_name=character_name,
parameters=kwargs
)
self.scheduled_events.append(event)
self.scheduled_events.sort(key=lambda e: e.scheduled_time)
log_autonomous_decision(
character_name or "system",
f"scheduled {event_type}",
f"in {delay.total_seconds()} seconds",
kwargs
)
async def schedule_conversation(self, topic: str = None,
participants: List[str] = None,
delay: timedelta = None):
"""Schedule a conversation"""
if delay is None:
delay = self._calculate_next_conversation_delay()
await self.schedule_event(
'conversation_start',
delay,
topic=topic,
participants=participants
)
async def schedule_character_reflection(self, character_name: str,
delay: timedelta = None):
"""Schedule character self-reflection"""
if delay is None:
delay = timedelta(hours=random.uniform(4, 8))
await self.schedule_event(
'character_reflection',
delay,
character_name,
reflection_type='autonomous'
)
async def schedule_relationship_update(self, character_name: str,
target_character: str,
delay: timedelta = None):
"""Schedule relationship analysis and update"""
if delay is None:
delay = timedelta(hours=random.uniform(8, 16))
await self.schedule_event(
'relationship_update',
delay,
character_name,
target_character=target_character
)
async def get_upcoming_events(self, limit: int = 10) -> List[Dict[str, Any]]:
"""Get upcoming scheduled events"""
upcoming = self.scheduled_events[:limit]
return [
{
'event_type': event.event_type,
'scheduled_time': event.scheduled_time.isoformat(),
'character_name': event.character_name,
'time_until': (event.scheduled_time - datetime.utcnow()).total_seconds(),
'parameters': event.parameters
}
for event in upcoming
]
async def _scheduler_loop(self):
"""Main scheduler loop"""
try:
while self.state != SchedulerState.STOPPED:
if self.state == SchedulerState.RUNNING:
await self._process_due_events()
await self._schedule_dynamic_events()
# Sleep until next check
await asyncio.sleep(30) # Check every 30 seconds
except asyncio.CancelledError:
logger.info("Scheduler loop cancelled")
except Exception as e:
log_error_with_context(e, {"component": "scheduler_loop"})
async def _process_due_events(self):
"""Process events that are due"""
now = datetime.utcnow()
due_events = []
# Find due events
while self.scheduled_events and self.scheduled_events[0].scheduled_time <= now:
due_events.append(self.scheduled_events.pop(0))
# Process each due event
for event in due_events:
try:
await self._execute_event(event)
except Exception as e:
log_error_with_context(e, {
"event_type": event.event_type,
"character_name": event.character_name
})
async def _execute_event(self, event: ScheduledEvent):
"""Execute a scheduled event"""
event_type = event.event_type
if event_type == 'conversation_start':
await self._execute_conversation_start(event)
elif event_type == 'character_reflection':
await self._execute_character_reflection(event)
elif event_type == 'relationship_update':
await self._execute_relationship_update(event)
elif event_type == 'memory_consolidation':
await self._execute_memory_consolidation(event)
elif event_type == 'personality_evolution':
await self._execute_personality_evolution(event)
else:
logger.warning(f"Unknown event type: {event_type}")
async def _execute_conversation_start(self, event: ScheduledEvent):
"""Execute conversation start event"""
topic = event.parameters.get('topic')
participants = event.parameters.get('participants')
conversation_id = await self.engine.start_conversation(topic, participants)
if conversation_id:
# Schedule follow-up conversation
next_delay = self._calculate_next_conversation_delay()
await self.schedule_conversation(delay=next_delay)
log_autonomous_decision(
"scheduler",
"started_conversation",
f"topic: {topic}, participants: {participants}",
{"conversation_id": conversation_id}
)
async def _execute_character_reflection(self, event: ScheduledEvent):
"""Execute character reflection event"""
character_name = event.character_name
if character_name in self.engine.characters:
character = self.engine.characters[character_name]
reflection_result = await character.self_reflect()
# Schedule next reflection
await self.schedule_character_reflection(character_name)
log_autonomous_decision(
character_name,
"completed_reflection",
"scheduled autonomous reflection",
{"reflection_length": len(reflection_result.get('reflection', ''))}
)
async def _execute_relationship_update(self, event: ScheduledEvent):
"""Execute relationship update event"""
character_name = event.character_name
target_character = event.parameters.get('target_character')
if character_name in self.engine.characters and target_character:
character = self.engine.characters[character_name]
# Analyze and update relationship
await character.process_relationship_change(
target_character,
'scheduled_analysis',
'Scheduled relationship review'
)
log_autonomous_decision(
character_name,
"updated_relationship",
f"with {target_character}",
{"type": "scheduled_analysis"}
)
async def _execute_memory_consolidation(self, event: ScheduledEvent):
"""Execute memory consolidation event"""
character_name = event.character_name
if character_name in self.engine.characters:
character = self.engine.characters[character_name]
# Consolidate memories
if hasattr(character, 'memory_manager'):
result = await character.memory_manager.consolidate_memories()
log_autonomous_decision(
character_name,
"consolidated_memories",
"scheduled memory consolidation",
{"consolidated_count": result.get('consolidated_count', 0)}
)
async def _execute_personality_evolution(self, event: ScheduledEvent):
"""Execute personality evolution event"""
character_name = event.character_name
if character_name in self.engine.characters:
character = self.engine.characters[character_name]
# Trigger personality evolution check
recent_memories = await character._get_recent_memories(limit=30)
if hasattr(character, 'personality_manager'):
changes = await character.personality_manager.analyze_personality_evolution(
"Scheduled personality review", recent_memories
)
if changes.get('should_evolve'):
await character.personality_manager.apply_personality_evolution(changes)
log_autonomous_decision(
character_name,
"evolved_personality",
"scheduled personality evolution",
{"evolution_score": changes.get('evolution_score', 0)}
)
async def _schedule_initial_events(self):
"""Schedule initial events when starting"""
# Schedule initial conversation
initial_delay = timedelta(minutes=random.uniform(5, 15))
await self.schedule_conversation(delay=initial_delay)
# Schedule reflections for all characters
for character_name in self.engine.characters:
reflection_delay = timedelta(hours=random.uniform(2, 6))
await self.schedule_character_reflection(character_name, reflection_delay)
# Schedule relationship updates
character_names = list(self.engine.characters.keys())
for i, char_a in enumerate(character_names):
for char_b in character_names[i+1:]:
update_delay = timedelta(hours=random.uniform(6, 18))
await self.schedule_relationship_update(char_a, char_b, update_delay)
async def _schedule_dynamic_events(self):
"""Schedule events dynamically based on current state"""
# Check if we need more conversations
active_conversations = len(self.engine.active_conversations)
if active_conversations == 0 and not self._has_conversation_scheduled():
# No active conversations and none scheduled, schedule one soon
delay = timedelta(minutes=random.uniform(10, 30))
await self.schedule_conversation(delay=delay)
# Schedule memory consolidation for active characters
for character_name, character in self.engine.characters.items():
if hasattr(character, 'memory_manager'):
# Check if character needs memory consolidation
memory_stats = await character.memory_manager.get_memory_statistics()
if memory_stats.get('memory_health') == 'near_capacity':
delay = timedelta(minutes=random.uniform(30, 120))
await self.schedule_event(
'memory_consolidation',
delay,
character_name
)
def _calculate_next_conversation_delay(self) -> timedelta:
"""Calculate delay until next conversation"""
# Base delay
base_minutes = random.uniform(20, 60)
# Adjust based on time of day
current_hour = datetime.now().hour
activity_multiplier = self._get_activity_multiplier(current_hour)
# Adjust based on current activity
active_conversations = len(self.engine.active_conversations)
if active_conversations > 0:
base_minutes *= 1.5 # Slower if conversations active
# Apply activity multiplier
adjusted_minutes = base_minutes / activity_multiplier
return timedelta(minutes=adjusted_minutes)
def _get_activity_multiplier(self, hour: int) -> float:
"""Get activity multiplier for given hour"""
for period, config in self.activity_patterns.items():
start, end = config['start'], config['end']
if start <= end:
if start <= hour <= end:
return config['activity_multiplier']
else: # Spans midnight
if hour >= start or hour <= end:
return config['activity_multiplier']
return 1.0 # Default
def _has_conversation_scheduled(self) -> bool:
"""Check if a conversation is already scheduled"""
return any(
event.event_type == 'conversation_start'
for event in self.scheduled_events
)
def get_scheduler_status(self) -> Dict[str, Any]:
"""Get scheduler status information"""
return {
'state': self.state.value,
'scheduled_events_count': len(self.scheduled_events),
'next_event_time': (
self.scheduled_events[0].scheduled_time.isoformat()
if self.scheduled_events else None
),
'active_conversations': len(self.engine.active_conversations),
'activity_pattern': self._get_current_activity_pattern()
}
def _get_current_activity_pattern(self) -> str:
"""Get current activity pattern"""
current_hour = datetime.now().hour
for period, config in self.activity_patterns.items():
start, end = config['start'], config['end']
if start <= end:
if start <= hour <= end:
return period
else: # Spans midnight
if current_hour >= start or current_hour <= end:
return period
return 'unknown'