feat: Implement Living AI system

Complete implementation of the Living AI features:

Phase 1 - Foundation:
- MoodService: Valence-arousal mood model with time decay
- RelationshipService: Stranger→Close Friend progression
- Enhanced system prompt with personality modifiers

Phase 2 - Autonomous Learning:
- FactExtractionService: AI-powered fact extraction from conversations
- Rate-limited extraction (configurable, default 30%)
- Deduplication and importance scoring

Phase 3 - Personalization:
- CommunicationStyleService: Learn user preferences
- OpinionService: Bot opinion formation on topics
- SelfAwarenessService: Bot statistics and self-reflection

Phase 4 - Proactive Features:
- ProactiveService: Scheduled events (birthdays, follow-ups)
- Event detection from conversations
- Recurring event support

Phase 5 - Social Features:
- AssociationService: Cross-user memory connections
- Shared interest discovery
- Connection suggestions

New database tables:
- bot_states, bot_opinions, user_relationships
- user_communication_styles, scheduled_events
- fact_associations, mood_history

Configuration:
- Living AI feature toggles
- Individual command enable/disable
- All features work naturally through conversation when commands disabled
This commit is contained in:
2026-01-12 19:51:48 +01:00
parent 7d2fab57c4
commit 0d43b5b29a
17 changed files with 3524 additions and 4 deletions

View File

@@ -9,13 +9,22 @@ from discord.ext import commands
from daemon_boyfriend.config import settings
from daemon_boyfriend.services import (
AIService,
CommunicationStyleService,
ConversationManager,
FactExtractionService,
ImageAttachment,
Message,
MoodService,
OpinionService,
PersistentConversationManager,
ProactiveService,
RelationshipService,
SearXNGService,
UserService,
db,
detect_emoji_usage,
detect_formal_language,
extract_topics_from_message,
)
from daemon_boyfriend.utils import get_monitor
@@ -414,6 +423,8 @@ class AIChatCog(commands.Cog):
async with db.session() as session:
user_service = UserService(session)
conv_manager = PersistentConversationManager(session)
mood_service = MoodService(session)
relationship_service = RelationshipService(session)
# Get or create user
user = await user_service.get_or_create_user(
@@ -422,10 +433,12 @@ class AIChatCog(commands.Cog):
display_name=message.author.display_name,
)
guild_id = message.guild.id if message.guild else None
# Get or create conversation
conversation = await conv_manager.get_or_create_conversation(
user=user,
guild_id=message.guild.id if message.guild else None,
guild_id=guild_id,
channel_id=message.channel.id,
)
@@ -446,8 +459,43 @@ class AIChatCog(commands.Cog):
# Get context about mentioned users
mentioned_users_context = self._get_mentioned_users_context(message)
# Build system prompt with additional context
system_prompt = self.ai_service.get_system_prompt()
# Get Living AI context (mood, relationship, style, opinions)
mood = None
relationship_data = None
communication_style = None
relevant_opinions = None
if settings.living_ai_enabled:
if settings.mood_enabled:
mood = await mood_service.get_current_mood(guild_id)
if settings.relationship_enabled:
rel = await relationship_service.get_or_create_relationship(user, guild_id)
level = relationship_service.get_level(rel.relationship_score)
relationship_data = (level, rel)
if settings.style_learning_enabled:
style_service = CommunicationStyleService(session)
communication_style = await style_service.get_or_create_style(user)
if settings.opinion_formation_enabled:
opinion_service = OpinionService(session)
topics = extract_topics_from_message(user_message)
if topics:
relevant_opinions = await opinion_service.get_relevant_opinions(
topics, guild_id
)
# Build system prompt with personality context
if settings.living_ai_enabled and (mood or relationship_data or communication_style):
system_prompt = self.ai_service.get_enhanced_system_prompt(
mood=mood,
relationship=relationship_data,
communication_style=communication_style,
bot_opinions=relevant_opinions,
)
else:
system_prompt = self.ai_service.get_system_prompt()
# Add user context from database (custom name, known facts)
user_context = await user_service.get_user_context(user)
@@ -482,6 +530,20 @@ class AIChatCog(commands.Cog):
image_urls=image_urls,
)
# Post-response Living AI updates (mood, relationship, style, opinions, facts, proactive)
if settings.living_ai_enabled:
await self._update_living_ai_state(
session=session,
user=user,
guild_id=guild_id,
channel_id=message.channel.id,
user_message=user_message,
bot_response=response.content,
discord_message_id=message.id,
mood_service=mood_service,
relationship_service=relationship_service,
)
logger.debug(
f"Generated response for user {user.discord_id}: "
f"{len(response.content)} chars, {response.usage}"
@@ -489,6 +551,171 @@ class AIChatCog(commands.Cog):
return response.content
async def _update_living_ai_state(
self,
session,
user,
guild_id: int | None,
channel_id: int,
user_message: str,
bot_response: str,
discord_message_id: int,
mood_service: MoodService,
relationship_service: RelationshipService,
) -> None:
"""Update Living AI state after a response (mood, relationship, style, opinions, facts, proactive)."""
try:
# Simple sentiment estimation based on message characteristics
sentiment = self._estimate_sentiment(user_message)
engagement = min(1.0, len(user_message) / 300) # Longer = more engaged
# Update mood
if settings.mood_enabled:
await mood_service.update_mood(
guild_id=guild_id,
sentiment_delta=sentiment * 0.5,
engagement_delta=engagement * 0.5,
trigger_type="conversation",
trigger_user_id=user.id,
trigger_description=f"Conversation with {user.display_name}",
)
# Increment message count
await mood_service.increment_stats(guild_id, messages_sent=1)
# Update relationship
if settings.relationship_enabled:
await relationship_service.record_interaction(
user=user,
guild_id=guild_id,
sentiment=sentiment,
message_length=len(user_message),
conversation_turns=1,
)
# Update communication style learning
if settings.style_learning_enabled:
style_service = CommunicationStyleService(session)
await style_service.record_engagement(
user=user,
user_message_length=len(user_message),
bot_response_length=len(bot_response),
conversation_continued=True, # Assume continued for now
user_used_emoji=detect_emoji_usage(user_message),
user_used_formal_language=detect_formal_language(user_message),
)
# Update opinion tracking
if settings.opinion_formation_enabled:
topics = extract_topics_from_message(user_message)
if topics:
opinion_service = OpinionService(session)
for topic in topics[:3]: # Limit to 3 topics per message
await opinion_service.record_topic_discussion(
topic=topic,
guild_id=guild_id,
sentiment=sentiment,
engagement_level=engagement,
)
# Autonomous fact extraction (rate-limited internally)
if settings.fact_extraction_enabled:
fact_service = FactExtractionService(session, self.ai_service)
new_facts = await fact_service.maybe_extract_facts(
user=user,
message_content=user_message,
discord_message_id=discord_message_id,
)
if new_facts:
# Update stats for facts learned
await mood_service.increment_stats(guild_id, facts_learned=len(new_facts))
logger.debug(f"Auto-extracted {len(new_facts)} facts from message")
# Proactive event detection (follow-ups, birthdays)
if settings.proactive_enabled:
proactive_service = ProactiveService(session, self.ai_service)
# Try to detect follow-up opportunities (rate-limited by message length)
if len(user_message) > 30: # Only check substantial messages
await proactive_service.detect_and_schedule_followup(
user=user,
message_content=user_message,
guild_id=guild_id,
channel_id=channel_id,
)
# Try to detect birthday mentions
await proactive_service.detect_and_schedule_birthday(
user=user,
message_content=user_message,
guild_id=guild_id,
channel_id=channel_id,
)
except Exception as e:
logger.warning(f"Failed to update Living AI state: {e}")
def _estimate_sentiment(self, text: str) -> float:
"""Estimate sentiment from text using simple heuristics.
Returns a value from -1 (negative) to 1 (positive).
This is a placeholder until we add AI-based sentiment analysis.
"""
text_lower = text.lower()
# Positive indicators
positive_words = [
"thanks",
"thank you",
"awesome",
"great",
"love",
"amazing",
"wonderful",
"excellent",
"perfect",
"happy",
"glad",
"appreciate",
"helpful",
"nice",
"good",
"cool",
"fantastic",
"brilliant",
]
# Negative indicators
negative_words = [
"hate",
"awful",
"terrible",
"bad",
"stupid",
"annoying",
"frustrated",
"angry",
"disappointed",
"wrong",
"broken",
"useless",
"horrible",
"worst",
"sucks",
"boring",
]
positive_count = sum(1 for word in positive_words if word in text_lower)
negative_count = sum(1 for word in negative_words if word in text_lower)
# Check for exclamation marks (usually positive energy)
exclamation_bonus = min(0.2, text.count("!") * 0.05)
# Calculate sentiment
if positive_count + negative_count == 0:
return 0.1 + exclamation_bonus # Slightly positive by default
sentiment = (positive_count - negative_count) / (positive_count + negative_count)
return max(-1.0, min(1.0, sentiment + exclamation_bonus))
async def _generate_response_in_memory(
self, message: discord.Message, user_message: str
) -> str: