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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user