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