File size: 16,763 Bytes
3d7a63c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 | # core/learning.py
import re
import json
import threading
from datetime import datetime
from memory import ConversationMemory
import sqlite3
class LearningEngine:
"""
Self-learning engine that continuously observes user conversations,
extracts communication patterns, interests, and preferences,
and consolidates them into an adaptive user model.
The more a user interacts, the better Invicta understands them.
"""
def __init__(self, memory: ConversationMemory):
self.memory = memory
self._lock = threading.Lock()
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# REAL-TIME MESSAGE ANALYSIS
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def analyze_message(self, user_id, message):
if not message or not user_id:
return
try:
self.memory.increment_interaction_count(user_id)
self._detect_interests(user_id, message)
self._detect_communication_style(user_id, message)
self._detect_emotional_state(user_id, message)
self._detect_preferences(user_id, message)
self._detect_language(user_id, message) # β ADD THIS
except Exception as e:
print(f"β οΈ Learning analysis error: {e}")
def _detect_interests(self, user_id, message):
"""Detect topics the user is interested in based on what they ask about."""
interest_patterns = [
# Technology
(r'\b(?:python|javascript|typescript|rust|golang|programming|coding|developer|software|algorithm|api|database|web\s*dev|machine\s*learning|deep\s*learning|ai|artificial\s*intelligence|neural\s*network|data\s*science|cybersecurity|blockchain|crypto|cloud\s*computing|devops)\b', 'technology'),
# Science
(r'\b(?:physics|chemistry|biology|quantum|astronomy|space|nasa|evolution|genetics|neuroscience|psychology|math|calculus|statistics|research)\b', 'science'),
# Business & Finance
(r'\b(?:startup|business|entrepreneur|invest|stock|market|finance|crypto|trading|marketing|sales|revenue|profit|startup|company|venture)\b', 'business'),
# Creative
(r'\b(?:design|art|music|writing|creative|paint|draw|compose|fiction|novel|poetry|photography|filmmaking|video|animation)\b', 'creative'),
# Health & Fitness
(r'\b(?:fitness|workout|exercise|diet|nutrition|health|medical|yoga|meditation|mental\s*health|therapy|wellness|gym|running|weight)\b', 'health'),
# Gaming
(r'\b(?:game|gaming|gamer|xbox|playstation|nintendo|steam|esports|rpg|fps|mmorpg|minecraft|fortnite)\b', 'gaming'),
# Travel
(r'\b(?:travel|trip|vacation|flight|hotel|country|city|backpack|adventure|explore|destination|tourist|visa)\b', 'travel'),
# Food & Cooking
(r'\b(?:recipe|cooking|food|cuisine|bake|chef|restaurant|meal|ingredient|kitchen|dish)\b', 'food'),
# Education
(r'\b(?:study|university|college|school|learn|course|degree|exam|academic|education|teach|student|homework|tutorial)\b', 'education'),
# Philosophy & Religion
(r'\b(?:philosophy|existential|meaning\s*of\s*life|moral|ethics|religion|spiritual|meditation|mindfulness|consciousness)\b', 'philosophy'),
]
msg_lower = message.lower()
for pattern, category in interest_patterns:
matches = re.findall(pattern, msg_lower)
if matches:
# Store each matched keyword as an interest observation
for match in matches:
keyword = match.strip().lower()
self.memory.store_learning(
user_id=user_id,
category="interest",
key=keyword,
value=f"Interested in {category}: {keyword}",
confidence=0.4
)
def _detect_language(self, user_id, message):
"""Detect if user is writing in a non-English language and store preference."""
# Simple detection for common scripts
import re
if re.search(r'[\u0900-\u097F]', message): # Devanagari (Hindi)
self.memory.store_learning(user_id, "preference", "language", "hindi", 0.6)
elif re.search(r'[\u0600-\u06FF]', message): # Arabic
self.memory.store_learning(user_id, "preference", "language", "arabic", 0.6)
elif re.search(r'[\u3040-\u30FF]', message): # Japanese
self.memory.store_learning(user_id, "preference", "language", "japanese", 0.6)
elif re.search(r'[\u4E00-\u9FFF]', message): # Chinese
self.memory.store_learning(user_id, "preference", "language", "chinese", 0.6)
elif re.search(r'[\uAC00-\uD7AF]', message): # Korean
self.memory.store_learning(user_id, "preference", "language", "korean", 0.6)
elif re.search(r'[ÑéΓΓ³ΓΊΓ±ΒΏΒ‘]', message, re.IGNORECASE): # Spanish
self.memory.store_learning(user_id, "preference", "language", "spanish", 0.3)
def _detect_communication_style(self, user_id, message):
"""Detect how the user communicates β formality, length, tone."""
# ββ FIX: msg_lower was missing here, causing the crash ββ
msg_lower = message.lower()
words = message.split()
word_count = len(words)
# ββ Formality detection ββ
formal_indicators = sum(1 for w in words if w.lower() in {
"therefore", "however", "furthermore", "consequently", "nevertheless",
"accordingly", "moreover", "henceforth", "would", "shall", "may",
"perhaps", "kindly", "regards", "sincerely", "please", "respectfully",
})
casual_indicators = sum(1 for w in words if w.lower() in {
"lol", "haha", "yeah", "nah", "yep", "nope", "gonna", "wanna",
"gotta", "dunno", "kinda", "sorta", "btw", "omg", "wtf", "lmao",
"bruh", "dude", "hey", "yo", "sup", "ngl", "fr", "tbh", "imo",
})
contractions = sum(1 for w in words if re.match(r"\w+'\w+", w))
if formal_indicators > 2 or (formal_indicators > 0 and casual_indicators == 0 and contractions == 0):
self.memory.store_learning(user_id, "style", "formality_signal", "formal", 0.3)
elif casual_indicators > 0 or contractions > 2:
self.memory.store_learning(user_id, "style", "formality_signal", "casual", 0.3)
# ββ Response length preference ββ
if word_count <= 5:
self.memory.store_learning(user_id, "style", "length_signal", "short", 0.25)
elif word_count <= 20:
self.memory.store_learning(user_id, "style", "length_signal", "medium", 0.2)
else:
self.memory.store_learning(user_id, "style", "length_signal", "detailed", 0.25)
# ββ Tone detection ββ
# Question-heavy = curious
questions = message.count('?')
exclamations = message.count('!')
emojis = len(re.findall(r'[π₯π‘β€οΈππππ€ππβ¨π―πͺππ§ βππ«]', message))
if questions >= 2:
self.memory.store_learning(user_id, "style", "tone_signal", "curious", 0.3)
if exclamations >= 2 or emojis >= 2:
self.memory.store_learning(user_id, "style", "tone_signal", "enthusiastic", 0.3)
if any(phrase in msg_lower for phrase in ['help me', 'can you', 'please', 'how do i', 'how to']):
self.memory.store_learning(user_id, "style", "tone_signal", "practical", 0.25)
# Detect humor/wit
humor_words = ['joke', 'funny', 'lol', 'haha', 'lmao', 'pun', 'humor']
if any(w in msg_lower for w in humor_words):
self.memory.store_learning(user_id, "style", "tone_signal", "witty", 0.35)
def _detect_emotional_state(self, user_id, message):
"""Detect the user's emotional state from their messages."""
msg_lower = message.lower()
# Stress/frustration
stress_words = ['stressed', 'frustrated', 'overwhelmed', 'anxious', 'worried',
'tired', 'exhausted', 'burnt out', 'burnout', 'struggling',
'can\'t handle', 'too much', 'losing hope']
if any(w in msg_lower for w in stress_words):
self.memory.store_learning(user_id, "emotion", "recent_state", "stressed", 0.5)
self.memory.store_learning(user_id, "emotion", "stress_prone", "yes", 0.2)
# Happiness/excitement
happy_words = ['excited', 'happy', 'great', 'awesome', 'amazing', 'wonderful',
'fantastic', 'love it', 'so good', 'thrilled', 'pumped', 'stoked']
if any(w in msg_lower for w in happy_words):
self.memory.store_learning(user_id, "emotion", "recent_state", "happy", 0.5)
# Sadness
sad_words = ['sad', 'depressed', 'lonely', 'miss', 'lost', 'heartbroken',
'crying', 'hurt', 'pain', 'grief', 'mourn']
if any(w in msg_lower for w in sad_words):
self.memory.store_learning(user_id, "emotion", "recent_state", "sad", 0.5)
# Curiosity
curious_words = ['wonder', 'curious', 'interesting', 'fascinating', 'tell me more',
'how come', 'why is', 'what if', 'explain']
if any(w in msg_lower for w in curious_words):
self.memory.store_learning(user_id, "emotion", "recent_state", "curious", 0.4)
def _detect_preferences(self, user_id, message):
"""Detect explicit and implicit preferences."""
msg_lower = message.lower()
# Explicit preferences
pref_patterns = [
(r"i (?:prefer|like|want|need) (?:my (?:answers?|responses?) )?(?:to be )?(short|brief|concise)", "short"),
(r"i (?:prefer|like|want|need) (?:my (?:answers?|responses?) )?(?:to be )?(long|detailed|thorough|in.depth|comprehensive)", "detailed"),
(r"i (?:prefer|like|want|need) (?:my (?:answers?|responses?) )?(?:to be )?(simple|easy|basic|beginner)", "simple"),
(r"i (?:prefer|like|want|need) (?:my (?:answers?|responses?) )?(?:to be )?(technical|advanced|complex)", "technical"),
(r"(?:don't|do not) (?:use|give me) (?:code|programming)", "no_code"),
(r"(?:give|show) me (?:the )?code", "wants_code"),
(r"(?:give|show) me (?:an )?example", "wants_examples"),
(r"(?:step.by.step|step by step)", "step_by_step"),
]
for pattern, pref in pref_patterns:
if re.search(pattern, msg_lower):
self.memory.store_learning(user_id, "preference", pref, "true", 0.6)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# CONSOLIDATION (runs periodically)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def run_daily_consolidation(self):
"""
Consolidate raw learning observations into user insights.
Called periodically by the background thread.
"""
try:
# Get all users who have learning data
all_users = self._get_active_users()
for user_id in all_users:
try:
self._consolidate_user(user_id)
except Exception as e:
print(f"β οΈ Consolidation error for user {user_id}: {e}")
except Exception as e:
print(f"β οΈ Daily consolidation error: {e}")
def _get_active_users(self):
"""Get list of user IDs that have learning data."""
db_path = self.memory.db_path
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
cur = conn.cursor()
cur.execute("SELECT DISTINCT user_id FROM user_learning")
rows = cur.fetchall()
conn.close()
return [row["user_id"] for row in rows]
def _consolidate_user(self, user_id):
"""Consolidate all learning data for one user into insights."""
insights = self.memory.get_user_insights(user_id) or {}
# ββ Formality ββ
style_data = self.memory.get_learning_by_category(user_id, "style")
formality_signals = []
for key, val in style_data.items():
if key == "formality_signal":
formality_signals.append(val)
if formality_signals:
# Most frequent signal wins
from collections import Counter
most_common = Counter(formality_signals).most_common(1)[0][0]
insights["preferred_formality"] = most_common
# ββ Response length ββ
length_signals = [v for k, v in style_data.items() if k == "length_signal"]
if length_signals:
from collections import Counter
most_common = Counter(length_signals).most_common(1)[0][0]
insights["preferred_response_length"] = most_common
# ββ Tone ββ
tone_signals = [v for k, v in style_data.items() if k == "tone_signal"]
if tone_signals:
from collections import Counter
most_common = Counter(tone_signals).most_common(1)[0][0]
insights["preferred_tone"] = most_common
# ββ Topics of interest ββ
top_topics = self.memory.get_top_learning_topics(user_id, limit=15)
interest_list = [t["key"] for t in top_topics if t.get("times_observed", 0) >= 2]
if interest_list:
insights["topics_of_interest"] = interest_list[:10]
# ββ Communication patterns ββ
patterns = {}
pref_data = self.memory.get_learning_by_category(user_id, "preference")
for k, v in pref_data.items():
patterns[k] = v
if patterns:
insights["communication_patterns"] = patterns
# Save consolidated insights
self.memory.update_user_insights(user_id, insights)
# Prune old learning data
self.memory.prune_old_learning(user_id, max_entries=500)
print(f"π§ Consolidated learning for user {user_id}")
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# USER MODEL (used by Brain to adapt responses)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def get_user_model(self, user_id):
"""
Build the current user model from consolidated insights
+ real-time learning data. Used by Brain._build_adaptation_prompt().
"""
if not user_id:
return None
insights = self.memory.get_user_insights(user_id)
model = {}
# Formality
model["formality"] = insights.get("preferred_formality", "casual") if insights else "casual"
# Response length
model["response_length"] = insights.get("preferred_response_length", "medium") if insights else "medium"
# Tone
model["tone"] = insights.get("preferred_tone", "friendly") if insights else "friendly"
# Interests
model["interests"] = insights.get("topics_of_interest", []) if insights else []
# Preferences
if insights and insights.get("communication_patterns"):
model["preferences"] = insights["communication_patterns"]
else:
model["preferences"] = {}
# Emotional state (from real-time learning)
emotion_data = self.memory.get_learning_by_category(user_id, "emotion")
recent_state = emotion_data.get("recent_state", "")
if recent_state:
model["recent_emotional_state"] = recent_state
# Interaction count
if insights:
model["interaction_count"] = insights.get("total_interactions", 0)
else:
model["interaction_count"] = 0
return model |