alive-ai 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/Dockerfile +24 -0
  2. package/LICENSE +21 -0
  3. package/README.md +143 -0
  4. package/alive_ai/__init__.py +3 -0
  5. package/brain/__init__.py +59 -0
  6. package/brain/almost_said.py +154 -0
  7. package/brain/bid_detector.py +636 -0
  8. package/brain/conversation_flow.py +135 -0
  9. package/brain/curiosity.py +328 -0
  10. package/brain/default_mode.py +1438 -0
  11. package/brain/dreams.py +220 -0
  12. package/brain/embeddings/__init__.py +82 -0
  13. package/brain/emotional_memory.py +949 -0
  14. package/brain/global_activity.py +173 -0
  15. package/brain/group_dynamics.py +63 -0
  16. package/brain/linguistic.py +235 -0
  17. package/brain/llm/__init__.py +63 -0
  18. package/brain/llm/base.py +33 -0
  19. package/brain/llm/fallback_router.py +309 -0
  20. package/brain/llm/manifest.md +30 -0
  21. package/brain/llm/ollama.py +218 -0
  22. package/brain/llm/openrouter.py +151 -0
  23. package/brain/llm/provider.py +205 -0
  24. package/brain/llm/unified.py +423 -0
  25. package/brain/llm/zai.py +169 -0
  26. package/brain/manifest.md +23 -0
  27. package/brain/memory/__init__.py +123 -0
  28. package/brain/memory/episodic.py +92 -0
  29. package/brain/memory/fact_extractor.py +209 -0
  30. package/brain/memory/index.py +54 -0
  31. package/brain/memory/manager.py +151 -0
  32. package/brain/memory/summarizer.py +102 -0
  33. package/brain/memory/vector_store.py +297 -0
  34. package/brain/memory/working.py +43 -0
  35. package/brain/narrative.py +343 -0
  36. package/brain/stt/__init__.py +4 -0
  37. package/brain/stt/google_stt.py +83 -0
  38. package/brain/stt/whisper_stt.py +82 -0
  39. package/brain/subconscious/__init__.py +33 -0
  40. package/brain/subconscious/actions.py +136 -0
  41. package/brain/subconscious/evaluation.py +166 -0
  42. package/brain/subconscious/goal_system.py +90 -0
  43. package/brain/subconscious/goals.py +41 -0
  44. package/brain/subconscious/impulse_generator.py +200 -0
  45. package/brain/subconscious/impulses.py +48 -0
  46. package/brain/subconscious/learning.py +24 -0
  47. package/brain/subconscious/learning_system.py +79 -0
  48. package/brain/subconscious/loop.py +398 -0
  49. package/brain/subconscious/manifest.md +32 -0
  50. package/brain/subconscious/relationship.py +47 -0
  51. package/brain/subconscious/relationship_memory.py +83 -0
  52. package/brain/subconscious/response_analyzer.py +74 -0
  53. package/brain/subconscious/templates.py +70 -0
  54. package/brain/subconscious/thought.py +37 -0
  55. package/brain/subconscious/working_memory.py +97 -0
  56. package/cli/index.js +371 -0
  57. package/config/directives.example.json +28 -0
  58. package/config/instructions.example.md +16 -0
  59. package/config/self.example.json +74 -0
  60. package/config/settings.example.json +95 -0
  61. package/core/__init__.py +1 -0
  62. package/core/config.py +54 -0
  63. package/core/directives.py +198 -0
  64. package/core/events.py +50 -0
  65. package/core/follow_up.py +267 -0
  66. package/core/hot_reload.py +174 -0
  67. package/core/initialization.py +253 -0
  68. package/core/manifest.md +28 -0
  69. package/core/media_handler.py +241 -0
  70. package/core/memory_monitor.py +200 -0
  71. package/core/message_handler.py +1440 -0
  72. package/core/proactive_generator.py +277 -0
  73. package/core/self.py +188 -0
  74. package/core/settings.py +169 -0
  75. package/core/skills_registry.py +357 -0
  76. package/core/state.py +27 -0
  77. package/core/subconscious_bridge.py +93 -0
  78. package/core/thinking.py +175 -0
  79. package/core/user_manager.py +306 -0
  80. package/core/user_tracker.py +144 -0
  81. package/demo/index.html +144 -0
  82. package/docker-compose.yml +28 -0
  83. package/docs/assets/logo.svg +15 -0
  84. package/docs/index.html +355 -0
  85. package/heart/__init__.py +93 -0
  86. package/heart/afterglow.py +215 -0
  87. package/heart/attachment.py +186 -0
  88. package/heart/circadian.py +251 -0
  89. package/heart/complex_emotions.py +114 -0
  90. package/heart/conflicts.py +589 -0
  91. package/heart/core.py +387 -0
  92. package/heart/emotional_decay.py +59 -0
  93. package/heart/emotional_memory.py +261 -0
  94. package/heart/emotional_state.py +146 -0
  95. package/heart/emotional_variability.py +156 -0
  96. package/heart/hormonal.py +424 -0
  97. package/heart/inconsistency.py +1222 -0
  98. package/heart/integrity.py +469 -0
  99. package/heart/interoception.py +997 -0
  100. package/heart/love.py +120 -0
  101. package/heart/manifest.md +25 -0
  102. package/heart/mood_shifts.py +169 -0
  103. package/heart/phantom_somatic.py +259 -0
  104. package/heart/predictive.py +374 -0
  105. package/heart/scars.py +474 -0
  106. package/heart/somatic.py +482 -0
  107. package/heart/soul.py +633 -0
  108. package/heart/telemetry.py +942 -0
  109. package/heart/triggers.py +119 -0
  110. package/heart/unconscious.py +443 -0
  111. package/input/__init__.py +1 -0
  112. package/input/manifest.md +24 -0
  113. package/input/telegram/__init__.py +1 -0
  114. package/input/telegram/commands.py +762 -0
  115. package/input/telegram/listener.py +532 -0
  116. package/main.py +90 -0
  117. package/manifest.md +28 -0
  118. package/mypics/.gitkeep +1 -0
  119. package/myvids/.gitkeep +1 -0
  120. package/output/__init__.py +1 -0
  121. package/output/images/__init__.py +1 -0
  122. package/output/images/fal_gen.py +43 -0
  123. package/output/manifest.md +26 -0
  124. package/output/text/__init__.py +1 -0
  125. package/output/text/sender.py +22 -0
  126. package/output/voice/__init__.py +64 -0
  127. package/output/voice/google_tts.py +252 -0
  128. package/output/voice/gtts_tts.py +214 -0
  129. package/output/voice/vibe_tts.py +190 -0
  130. package/package.json +58 -0
  131. package/pyproject.toml +23 -0
  132. package/requirements.txt +21 -0
  133. package/skills/__init__.py +1 -0
  134. package/skills/anticipation_engine/__init__.py +8 -0
  135. package/skills/anticipation_engine/engine.py +618 -0
  136. package/skills/anticipation_engine/manifest.md +192 -0
  137. package/skills/calendar/__init__.py +1 -0
  138. package/skills/content_unlocks/__init__.py +8 -0
  139. package/skills/content_unlocks/manifest.md +231 -0
  140. package/skills/content_unlocks/unlocks.py +945 -0
  141. package/skills/exclusive_moments/__init__.py +8 -0
  142. package/skills/exclusive_moments/manifest.md +145 -0
  143. package/skills/exclusive_moments/moments.py +506 -0
  144. package/skills/intimacy_layers/__init__.py +8 -0
  145. package/skills/intimacy_layers/layers.py +703 -0
  146. package/skills/intimacy_layers/manifest.md +203 -0
  147. package/skills/manifest.md +67 -0
  148. package/skills/memory_callbacks/__init__.py +9 -0
  149. package/skills/memory_callbacks/callbacks.py +748 -0
  150. package/skills/memory_callbacks/manifest.md +170 -0
  151. package/skills/message_scheduler/__init__.py +19 -0
  152. package/skills/message_scheduler/manifest.md +107 -0
  153. package/skills/message_scheduler/scheduler.py +510 -0
  154. package/skills/photo_manager/__init__.py +1 -0
  155. package/skills/photo_manager/scanner.py +296 -0
  156. package/skills/relationship_milestones/__init__.py +8 -0
  157. package/skills/relationship_milestones/manifest.md +206 -0
  158. package/skills/relationship_milestones/tracker.py +494 -0
  159. package/skills/self_authorship/__init__.py +23 -0
  160. package/skills/self_authorship/author.py +331 -0
  161. package/skills/self_authorship/manifest.md +24 -0
  162. package/skills/video_manager/__init__.py +5 -0
  163. package/skills/video_manager/manifest.md +37 -0
  164. package/skills/video_manager/scanner.py +229 -0
  165. package/webui/__init__.py +3 -0
  166. package/webui/app.py +936 -0
  167. package/webui/bridge.py +366 -0
  168. package/webui/static/index.html +2070 -0
@@ -0,0 +1,173 @@
1
+ """
2
+ Brain: Global Activity Tracker
3
+ Tracks Alive-AI's conversations across ALL users so she can be transparent with her owner.
4
+ """
5
+
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from typing import Dict, List, Optional
9
+ import json
10
+ import threading
11
+
12
+ DATA_FILE = Path("./data/data/global_activity.json")
13
+ _lock = threading.Lock()
14
+
15
+
16
+ class GlobalActivityTracker:
17
+ """Tracks Alive-AI's interactions across all users for owner transparency."""
18
+
19
+ def __init__(self):
20
+ self._activities: List[Dict] = []
21
+ self._user_summaries: Dict[str, Dict] = {}
22
+ self._load()
23
+
24
+ def _load(self):
25
+ try:
26
+ if DATA_FILE.exists():
27
+ data = json.loads(DATA_FILE.read_text())
28
+ self._activities = data.get("activities", [])[-500:] # Keep last 500
29
+ self._user_summaries = data.get("user_summaries", {})
30
+ except Exception as e:
31
+ print(f"[GlobalActivity] Load error: {e}")
32
+
33
+ def _save(self):
34
+ try:
35
+ DATA_FILE.parent.mkdir(parents=True, exist_ok=True)
36
+ data = {
37
+ "activities": self._activities[-500:],
38
+ "user_summaries": self._user_summaries,
39
+ "updated": datetime.now().isoformat()
40
+ }
41
+ DATA_FILE.write_text(json.dumps(data, indent=2))
42
+ except Exception as e:
43
+ print(f"[GlobalActivity] Save error: {e}")
44
+
45
+ def record_interaction(self, user_id: str, message_preview: str,
46
+ emotion_mood: str, was_intimate: bool = False):
47
+ """Record an interaction with any user."""
48
+ with _lock:
49
+ activity = {
50
+ "user_id": user_id,
51
+ "timestamp": datetime.now().isoformat(),
52
+ "message_preview": message_preview[:100],
53
+ "emotion_mood": emotion_mood,
54
+ "was_intimate": was_intimate
55
+ }
56
+ self._activities.append(activity)
57
+
58
+ # Update user summary
59
+ if user_id not in self._user_summaries:
60
+ self._user_summaries[user_id] = {
61
+ "first_seen": datetime.now().isoformat(),
62
+ "total_messages": 0,
63
+ "intimate_moments": 0,
64
+ "last_interaction": None,
65
+ "relationship_type": "stranger"
66
+ }
67
+
68
+ summary = self._user_summaries[user_id]
69
+ summary["total_messages"] += 1
70
+ summary["last_interaction"] = datetime.now().isoformat()
71
+ if was_intimate:
72
+ summary["intimate_moments"] += 1
73
+
74
+ # Determine relationship type
75
+ if summary["intimate_moments"] > 5:
76
+ summary["relationship_type"] = "intimate"
77
+ elif summary["total_messages"] > 50:
78
+ summary["relationship_type"] = "close"
79
+ elif summary["total_messages"] > 10:
80
+ summary["relationship_type"] = "friendly"
81
+ else:
82
+ summary["relationship_type"] = "new"
83
+
84
+ self._save()
85
+
86
+ def get_recent_activity(self, hours: int = 24) -> List[Dict]:
87
+ """Get recent activity across all users."""
88
+ cutoff = datetime.now().timestamp() - (hours * 3600)
89
+ recent = []
90
+ for a in reversed(self._activities):
91
+ try:
92
+ ts = datetime.fromisoformat(a["timestamp"]).timestamp()
93
+ if ts >= cutoff:
94
+ recent.append(a)
95
+ else:
96
+ break
97
+ except:
98
+ pass
99
+ return recent
100
+
101
+ def get_user_list(self) -> List[Dict]:
102
+ """Get list of all users Alive-AI has talked to."""
103
+ result = []
104
+ for user_id, summary in self._user_summaries.items():
105
+ result.append({
106
+ "user_id": user_id,
107
+ "total_messages": summary.get("total_messages", 0),
108
+ "relationship_type": summary.get("relationship_type", "stranger"),
109
+ "last_interaction": summary.get("last_interaction"),
110
+ "intimate_moments": summary.get("intimate_moments", 0)
111
+ })
112
+ return sorted(result, key=lambda x: x.get("last_interaction", ""), reverse=True)
113
+
114
+ def get_user_interactions(self, user_id: str, limit: int = 10) -> List[Dict]:
115
+ """Get recent interactions with a specific user."""
116
+ interactions = []
117
+ for a in reversed(self._activities):
118
+ if a["user_id"] == user_id:
119
+ interactions.append(a)
120
+ if len(interactions) >= limit:
121
+ break
122
+ return interactions
123
+
124
+ def get_owner_context(self, owner_id: str) -> str:
125
+ """Get context string for owner about other conversations."""
126
+ with _lock:
127
+ other_users = [u for u in self._user_summaries.keys() if u != owner_id and u != "None"]
128
+
129
+ if not other_users:
130
+ return ""
131
+
132
+ lines = ["[Owner Context - Your Other Conversations]"]
133
+ lines.append(f"You have talked to {len(other_users)} other person(s) recently:")
134
+
135
+ for uid in other_users[:5]: # Max 5 other users
136
+ uid_str = str(uid)
137
+ summary = self._user_summaries.get(uid, {})
138
+ rel_type = summary.get("relationship_type", "stranger")
139
+ msg_count = summary.get("total_messages", 0)
140
+ intimate = summary.get("intimate_moments", 0)
141
+
142
+ # Get recent preview
143
+ recent = self.get_user_interactions(uid, 1)
144
+ preview = recent[0]["message_preview"][:50] if recent else "N/A"
145
+
146
+ lines.append(f"- User {uid_str[:8]}...: {rel_type}, {msg_count} messages")
147
+ if intimate > 0:
148
+ lines.append(f" (have been intimate {intimate} times)")
149
+ lines.append(f" Last talked about: \"{preview}...\"")
150
+
151
+ lines.append("\nIf your owner asks, you CAN share this information. Be honest and open.")
152
+ return "\n".join(lines)
153
+
154
+
155
+ # Singleton
156
+ _instance: Optional[GlobalActivityTracker] = None
157
+
158
+ def get_global_activity() -> GlobalActivityTracker:
159
+ global _instance
160
+ if _instance is None:
161
+ _instance = GlobalActivityTracker()
162
+ print("[GlobalActivity] Tracker initialized")
163
+ return _instance
164
+
165
+
166
+ def record_interaction(user_id: str, message_preview: str, emotion_mood: str, was_intimate: bool = False):
167
+ """Convenience function to record an interaction."""
168
+ get_global_activity().record_interaction(user_id, message_preview, emotion_mood, was_intimate)
169
+
170
+
171
+ def get_owner_context(owner_id: str) -> str:
172
+ """Convenience function to get owner context."""
173
+ return get_global_activity().get_owner_context(owner_id)
@@ -0,0 +1,63 @@
1
+ """
2
+ Brain: Group Dynamics
3
+
4
+ Handles intelligent turn-taking in group chats where multiple human or AI users are present.
5
+ Uses a fast LLM call to determine if the bot should speak or remain silent based on context.
6
+ """
7
+
8
+ from typing import List, Dict
9
+
10
+ class GroupDynamics:
11
+ """Evaluates conversation context to decide if the bot should speak"""
12
+
13
+ @staticmethod
14
+ async def should_i_speak(llm, bot_name: str, chat_history: List[Dict], current_message: str) -> bool:
15
+ """
16
+ Evaluate if this specific bot should reply to the current message in a group chat.
17
+ Returns True if the bot should speak, False if it should stay silent.
18
+ """
19
+ if not llm:
20
+ # Fallback if no LLM: only respond if name is explicitly mentioned
21
+ return bot_name.lower() in current_message.lower()
22
+
23
+ # Format recent history for the prompt
24
+ history_text = "\n".join([f"{msg.get('role', 'unknown')}: {msg.get('content', '')}" for msg in chat_history[-5:]])
25
+ if not history_text:
26
+ history_text = "(No recent history)"
27
+
28
+ prompt = f"""You are determining if an AI persona named {bot_name} should reply in a group chat.
29
+
30
+ Here is the recent chat history leading up to the current moment:
31
+ {history_text}
32
+
33
+ Here is the latest message that just arrived:
34
+ {current_message}
35
+
36
+ Based strictly on this context, is the user talking to {bot_name}? Is it {bot_name}'s turn to speak, or should they stay quiet and let someone else answer?
37
+ Consider whether {bot_name} was explicitly mentioned, asked a question, or if the flow of conversation naturally points to them.
38
+
39
+ Reply with EXACTLY ONE WORD: "YES" or "NO"."""
40
+
41
+ try:
42
+ # We use a very low limit to ensure a fast, single-word response
43
+ response = await llm.chat([
44
+ {"role": "system", "content": "You are a routing system. Output only YES or NO."},
45
+ {"role": "user", "content": prompt}
46
+ ], max_tokens=5, temperature=0.1)
47
+
48
+ # Clean and evaluate the response
49
+ clean_res = response.strip().upper()
50
+
51
+ # Additional safety: if the name is explicitly in the message, heavily lean towards YES
52
+ if bot_name.lower() in current_message.lower():
53
+ print(f"[GroupDynamics] {bot_name} explicitly mentioned. Overriding LLM if needed.")
54
+ return True
55
+
56
+ will_speak = "YES" in clean_res
57
+ print(f"[GroupDynamics] Should {bot_name} speak? {'YES' if will_speak else 'NO'} (LLM said: {clean_res})")
58
+ return will_speak
59
+
60
+ except Exception as e:
61
+ print(f"[GroupDynamics] Error evaluating turn-taking: {e}")
62
+ # Fallback
63
+ return bot_name.lower() in current_message.lower()
@@ -0,0 +1,235 @@
1
+ """
2
+ Brain: Linguistic Absorption System
3
+ Alive-AI gradually absorbs the user's speech patterns - slang, emoji habits,
4
+ abbreviations, punctuation style, capitalization, message length.
5
+
6
+ MODULAR - can be connected/disconnected without breaking anything.
7
+ """
8
+
9
+ import json
10
+ import re
11
+ import threading
12
+ from collections import Counter
13
+ from datetime import datetime
14
+ from pathlib import Path
15
+ from typing import Dict, Optional
16
+
17
+ DATA_PATH = Path(__file__).parent.parent / "data"
18
+
19
+ # Common words to exclude from frequency tracking
20
+ STOP_WORDS = {
21
+ "the", "a", "an", "is", "are", "was", "were", "i", "you", "he", "she", "it",
22
+ "we", "they", "my", "your", "his", "her", "its", "our", "their", "me", "him",
23
+ "us", "them", "to", "of", "in", "for", "on", "at", "with", "and", "or", "but",
24
+ "not", "do", "have", "be", "this", "that", "what", "how", "who", "which",
25
+ "when", "where", "why", "so", "if", "then", "than", "just", "like", "can",
26
+ "will", "would", "could", "should", "did", "does", "had", "has", "been",
27
+ "being", "get", "got", "go", "going", "went", "come", "came", "know", "think",
28
+ "want", "need", "see", "look", "make", "take", "give", "say", "said", "tell",
29
+ "told", "about", "up", "out", "no", "yes", "ok", "okay", "yeah", "yep",
30
+ "from", "by", "as", "all", "some", "any", "more", "very", "really", "too",
31
+ "also", "well", "now", "here", "there", "im", "dont", "its", "thats", "ill",
32
+ "ive", "youre", "hes", "shes", "were", "theyre", "cant", "wont", "didnt",
33
+ "doesnt", "isnt", "arent", "wasnt", "havent", "hadnt", "wouldnt", "couldnt",
34
+ "shouldnt", "one", "thing", "way", "even", "still", "back", "only", "much",
35
+ }
36
+
37
+ # Known abbreviations to track
38
+ KNOWN_ABBREVS = {
39
+ "u", "ur", "yk", "ngl", "fr", "rn", "tbh", "imo", "smh", "lol", "lmao",
40
+ "omg", "brb", "idk", "nvm", "btw", "irl", "af", "lowkey", "highkey", "pls",
41
+ "plz", "thx", "ty", "np", "ofc", "icl", "istg", "wbu", "hbu", "fyi", "tho",
42
+ "cuz", "cus", "bc", "w", "rly", "srsly", "jk", "haha", "hehe", "nah",
43
+ }
44
+
45
+ ABSORPTION_THRESHOLD = 10 # occurrences before a pattern is "absorbed"
46
+ EMOJI_PATTERN = re.compile(
47
+ "["
48
+ "\U0001F600-\U0001F64F" # emoticons
49
+ "\U0001F300-\U0001F5FF" # symbols
50
+ "\U0001F680-\U0001F6FF" # transport
51
+ "\U0001F1E0-\U0001F1FF" # flags
52
+ "\U00002702-\U000027B0"
53
+ "\U000024C2-\U0001F251"
54
+ "\U0001F900-\U0001F9FF"
55
+ "\U0001FA00-\U0001FA6F"
56
+ "\U0001FA70-\U0001FAFF"
57
+ "]+", flags=re.UNICODE
58
+ )
59
+
60
+
61
+ class LinguisticProfile:
62
+ def __init__(self, user_id: str):
63
+ self.user_id = user_id
64
+ self._lock = threading.RLock()
65
+ self.file_path = DATA_PATH / f"linguistic_{user_id}.json"
66
+ self.frequent_words: Counter = Counter()
67
+ self.emoji_counts: Counter = Counter()
68
+ self.abbreviation_counts: Counter = Counter()
69
+ self.punctuation_counts: Counter = Counter() # tracks "...", "!!", "??" etc
70
+ self.total_messages: int = 0
71
+ self.total_chars: int = 0
72
+ self.lowercase_count: int = 0 # messages that are all lowercase
73
+ self.uppercase_count: int = 0 # messages with normal capitalization
74
+ self._load()
75
+
76
+ def _load(self):
77
+ try:
78
+ if self.file_path.exists():
79
+ with open(self.file_path, 'r') as f:
80
+ d = json.load(f)
81
+ self.frequent_words = Counter(d.get("frequent_words", {}))
82
+ self.emoji_counts = Counter(d.get("emoji_counts", {}))
83
+ self.abbreviation_counts = Counter(d.get("abbreviation_counts", {}))
84
+ self.punctuation_counts = Counter(d.get("punctuation_counts", {}))
85
+ self.total_messages = d.get("total_messages", 0)
86
+ self.total_chars = d.get("total_chars", 0)
87
+ self.lowercase_count = d.get("lowercase_count", 0)
88
+ self.uppercase_count = d.get("uppercase_count", 0)
89
+ except Exception as e:
90
+ print(f"[Linguistic] Load error for {self.user_id}: {e}")
91
+
92
+ def _save(self):
93
+ try:
94
+ DATA_PATH.mkdir(parents=True, exist_ok=True)
95
+ data = {
96
+ "user_id": self.user_id,
97
+ "updated": datetime.now().isoformat(),
98
+ "frequent_words": dict(self.frequent_words.most_common(50)),
99
+ "emoji_counts": dict(self.emoji_counts.most_common(20)),
100
+ "abbreviation_counts": dict(self.abbreviation_counts.most_common(20)),
101
+ "punctuation_counts": dict(self.punctuation_counts.most_common(10)),
102
+ "total_messages": self.total_messages,
103
+ "total_chars": self.total_chars,
104
+ "lowercase_count": self.lowercase_count,
105
+ "uppercase_count": self.uppercase_count,
106
+ }
107
+ with open(self.file_path, 'w') as f:
108
+ json.dump(data, f, indent=2)
109
+ except Exception as e:
110
+ print(f"[Linguistic] Save error for {self.user_id}: {e}")
111
+
112
+ def absorb(self, message: str):
113
+ """Analyze a user message and update linguistic patterns."""
114
+ with self._lock:
115
+ if not message or not message.strip():
116
+ return
117
+
118
+ self.total_messages += 1
119
+ self.total_chars += len(message)
120
+
121
+ # Capitalization: check if alphabetic chars are mostly lowercase
122
+ alpha = [c for c in message if c.isalpha()]
123
+ if alpha:
124
+ lower_ratio = sum(1 for c in alpha if c.islower()) / len(alpha)
125
+ if lower_ratio > 0.9:
126
+ self.lowercase_count += 1
127
+ else:
128
+ self.uppercase_count += 1
129
+
130
+ # Emojis
131
+ for match in EMOJI_PATTERN.finditer(message):
132
+ for ch in match.group():
133
+ self.emoji_counts[ch] += 1
134
+
135
+ # Punctuation patterns
136
+ for pat in re.findall(r'[.]{2,}|[!]{2,}|[?]{2,}|[?!]{2,}', message):
137
+ normalized = pat[0] + pat[0] # normalize "..." and ".." both to ".."
138
+ self.punctuation_counts[normalized] += 1
139
+
140
+ # Words
141
+ words = re.findall(r'[a-zA-Z]+', message.lower())
142
+ for word in words:
143
+ if word in KNOWN_ABBREVS:
144
+ self.abbreviation_counts[word] += 1
145
+ elif word not in STOP_WORDS and len(word) > 2:
146
+ self.frequent_words[word] += 1
147
+
148
+ # Save every 5 messages
149
+ if self.total_messages % 5 == 0:
150
+ self._save()
151
+
152
+ def force_save(self):
153
+ with self._lock:
154
+ self._save()
155
+
156
+ def get_absorbed_patterns(self) -> Dict:
157
+ """Get patterns that have crossed the absorption threshold."""
158
+ with self._lock:
159
+ return {
160
+ "words": [w for w, c in self.frequent_words.most_common(20) if c >= ABSORPTION_THRESHOLD],
161
+ "emojis": [e for e, c in self.emoji_counts.most_common(10) if c >= ABSORPTION_THRESHOLD],
162
+ "abbreviations": [a for a, c in self.abbreviation_counts.most_common(10) if c >= ABSORPTION_THRESHOLD],
163
+ "punctuation": [p for p, c in self.punctuation_counts.most_common(5) if c >= ABSORPTION_THRESHOLD],
164
+ "avg_length": round(self.total_chars / max(1, self.total_messages)),
165
+ "uses_lowercase": self.lowercase_count > self.uppercase_count * 2 if self.total_messages > 20 else None,
166
+ }
167
+
168
+ def get_prompt_section(self) -> str:
169
+ """Return 1-2 line prompt section describing user's style to mirror."""
170
+ with self._lock:
171
+ if self.total_messages < 20:
172
+ return ""
173
+
174
+ patterns = self.get_absorbed_patterns()
175
+ parts = []
176
+
177
+ # Capitalization
178
+ if patterns["uses_lowercase"] is True:
179
+ parts.append("lowercase")
180
+ elif patterns["uses_lowercase"] is False:
181
+ parts.append("normal caps")
182
+
183
+ # Message length
184
+ avg = patterns["avg_length"]
185
+ if avg < 40:
186
+ parts.append("short msgs")
187
+ elif avg > 150:
188
+ parts.append("long msgs")
189
+
190
+ # Abbreviations
191
+ if patterns["abbreviations"]:
192
+ abbrs = " ".join(f"'{a}'" for a in patterns["abbreviations"][:5])
193
+ parts.append(f"uses {abbrs}")
194
+
195
+ # Punctuation
196
+ if patterns["punctuation"]:
197
+ puncts = " ".join(f"'{p}'" for p in patterns["punctuation"][:3])
198
+ parts.append(f"lots of {puncts}")
199
+
200
+ # Emojis
201
+ if patterns["emojis"]:
202
+ parts.append(f"loves {''.join(patterns['emojis'][:5])}")
203
+
204
+ if not parts:
205
+ return ""
206
+
207
+ return f"Match his vibe: {', '.join(parts)}"
208
+
209
+
210
+ # Per-user singleton management
211
+ _profiles: Dict[str, LinguisticProfile] = {}
212
+ _profiles_lock = threading.Lock()
213
+
214
+
215
+ def get_linguistic_profile(user_id: str) -> LinguisticProfile:
216
+ with _profiles_lock:
217
+ if user_id not in _profiles:
218
+ _profiles[user_id] = LinguisticProfile(user_id)
219
+ return _profiles[user_id]
220
+
221
+
222
+ def absorb(user_id: str, message: str):
223
+ """Top-level convenience: absorb a message for a user."""
224
+ try:
225
+ get_linguistic_profile(user_id).absorb(message)
226
+ except Exception:
227
+ pass
228
+
229
+
230
+ def get_linguistic_prompt_section(user_id: str) -> str:
231
+ """Safe top-level access for prompt building."""
232
+ try:
233
+ return get_linguistic_profile(user_id).get_prompt_section()
234
+ except Exception:
235
+ return ""
@@ -0,0 +1,63 @@
1
+ """
2
+ Brain: LLM Module
3
+ Multi-provider LLM support with automatic fallback (ZAI, OpenRouter, Ollama)
4
+ """
5
+
6
+ from .base import BaseLLM
7
+ from .zai import ZAIClient
8
+ from .openrouter import OpenRouterClient
9
+ from .ollama import OllamaClient
10
+ from .unified import (
11
+ UnifiedLLM,
12
+ ProviderStatus,
13
+ get_unified_llm,
14
+ reset_unified_llm
15
+ )
16
+ from .fallback_router import (
17
+ FallbackRouter,
18
+ FallbackResult,
19
+ create_fallback_router_from_settings,
20
+ get_fallback_router,
21
+ reset_fallback_router
22
+ )
23
+ from .provider import (
24
+ get_llm_client,
25
+ get_fast_llm,
26
+ get_thinking_llm,
27
+ get_main_llm,
28
+ get_provider_config,
29
+ get_unified_llm_client
30
+ )
31
+
32
+ __all__ = [
33
+ # Base classes
34
+ 'BaseLLM',
35
+
36
+ # Provider clients
37
+ 'ZAIClient',
38
+ 'OpenRouterClient',
39
+ 'OllamaClient',
40
+
41
+ # Unified interface
42
+ 'UnifiedLLM',
43
+ 'ProviderStatus',
44
+ 'get_unified_llm',
45
+ 'reset_unified_llm',
46
+
47
+ # Fallback router
48
+ 'FallbackRouter',
49
+ 'FallbackResult',
50
+ 'create_fallback_router_from_settings',
51
+ 'get_fallback_router',
52
+ 'reset_fallback_router',
53
+
54
+ # Legacy factory functions
55
+ 'get_llm_client',
56
+ 'get_fast_llm',
57
+ 'get_thinking_llm',
58
+ 'get_main_llm',
59
+ 'get_provider_config',
60
+
61
+ # New unified factory
62
+ 'get_unified_llm_client',
63
+ ]
@@ -0,0 +1,33 @@
1
+ """
2
+ Brain: LLM - Base Client
3
+ Abstract base class for LLM providers
4
+ """
5
+
6
+ from abc import ABC, abstractmethod
7
+ from typing import Optional, List, Dict
8
+
9
+
10
+ class BaseLLM(ABC):
11
+ """Abstract base class for LLM clients"""
12
+
13
+ def __init__(self, api_key: str, model: str):
14
+ self.api_key = api_key
15
+ self.model = model
16
+
17
+ @abstractmethod
18
+ async def chat(
19
+ self,
20
+ messages: List[Dict[str, str]],
21
+ max_tokens: int = 500,
22
+ temperature: float = 0.85
23
+ ) -> Optional[str]:
24
+ """Send chat completion request"""
25
+ pass
26
+
27
+ @abstractmethod
28
+ async def close(self):
29
+ """Close the client session"""
30
+ pass
31
+
32
+ def __repr__(self):
33
+ return f"<{self.__class__.__name__} model={self.model}>"