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.
- package/Dockerfile +24 -0
- package/LICENSE +21 -0
- package/README.md +143 -0
- package/alive_ai/__init__.py +3 -0
- package/brain/__init__.py +59 -0
- package/brain/almost_said.py +154 -0
- package/brain/bid_detector.py +636 -0
- package/brain/conversation_flow.py +135 -0
- package/brain/curiosity.py +328 -0
- package/brain/default_mode.py +1438 -0
- package/brain/dreams.py +220 -0
- package/brain/embeddings/__init__.py +82 -0
- package/brain/emotional_memory.py +949 -0
- package/brain/global_activity.py +173 -0
- package/brain/group_dynamics.py +63 -0
- package/brain/linguistic.py +235 -0
- package/brain/llm/__init__.py +63 -0
- package/brain/llm/base.py +33 -0
- package/brain/llm/fallback_router.py +309 -0
- package/brain/llm/manifest.md +30 -0
- package/brain/llm/ollama.py +218 -0
- package/brain/llm/openrouter.py +151 -0
- package/brain/llm/provider.py +205 -0
- package/brain/llm/unified.py +423 -0
- package/brain/llm/zai.py +169 -0
- package/brain/manifest.md +23 -0
- package/brain/memory/__init__.py +123 -0
- package/brain/memory/episodic.py +92 -0
- package/brain/memory/fact_extractor.py +209 -0
- package/brain/memory/index.py +54 -0
- package/brain/memory/manager.py +151 -0
- package/brain/memory/summarizer.py +102 -0
- package/brain/memory/vector_store.py +297 -0
- package/brain/memory/working.py +43 -0
- package/brain/narrative.py +343 -0
- package/brain/stt/__init__.py +4 -0
- package/brain/stt/google_stt.py +83 -0
- package/brain/stt/whisper_stt.py +82 -0
- package/brain/subconscious/__init__.py +33 -0
- package/brain/subconscious/actions.py +136 -0
- package/brain/subconscious/evaluation.py +166 -0
- package/brain/subconscious/goal_system.py +90 -0
- package/brain/subconscious/goals.py +41 -0
- package/brain/subconscious/impulse_generator.py +200 -0
- package/brain/subconscious/impulses.py +48 -0
- package/brain/subconscious/learning.py +24 -0
- package/brain/subconscious/learning_system.py +79 -0
- package/brain/subconscious/loop.py +398 -0
- package/brain/subconscious/manifest.md +32 -0
- package/brain/subconscious/relationship.py +47 -0
- package/brain/subconscious/relationship_memory.py +83 -0
- package/brain/subconscious/response_analyzer.py +74 -0
- package/brain/subconscious/templates.py +70 -0
- package/brain/subconscious/thought.py +37 -0
- package/brain/subconscious/working_memory.py +97 -0
- package/cli/index.js +371 -0
- package/config/directives.example.json +28 -0
- package/config/instructions.example.md +16 -0
- package/config/self.example.json +74 -0
- package/config/settings.example.json +95 -0
- package/core/__init__.py +1 -0
- package/core/config.py +54 -0
- package/core/directives.py +198 -0
- package/core/events.py +50 -0
- package/core/follow_up.py +267 -0
- package/core/hot_reload.py +174 -0
- package/core/initialization.py +253 -0
- package/core/manifest.md +28 -0
- package/core/media_handler.py +241 -0
- package/core/memory_monitor.py +200 -0
- package/core/message_handler.py +1440 -0
- package/core/proactive_generator.py +277 -0
- package/core/self.py +188 -0
- package/core/settings.py +169 -0
- package/core/skills_registry.py +357 -0
- package/core/state.py +27 -0
- package/core/subconscious_bridge.py +93 -0
- package/core/thinking.py +175 -0
- package/core/user_manager.py +306 -0
- package/core/user_tracker.py +144 -0
- package/demo/index.html +144 -0
- package/docker-compose.yml +28 -0
- package/docs/assets/logo.svg +15 -0
- package/docs/index.html +355 -0
- package/heart/__init__.py +93 -0
- package/heart/afterglow.py +215 -0
- package/heart/attachment.py +186 -0
- package/heart/circadian.py +251 -0
- package/heart/complex_emotions.py +114 -0
- package/heart/conflicts.py +589 -0
- package/heart/core.py +387 -0
- package/heart/emotional_decay.py +59 -0
- package/heart/emotional_memory.py +261 -0
- package/heart/emotional_state.py +146 -0
- package/heart/emotional_variability.py +156 -0
- package/heart/hormonal.py +424 -0
- package/heart/inconsistency.py +1222 -0
- package/heart/integrity.py +469 -0
- package/heart/interoception.py +997 -0
- package/heart/love.py +120 -0
- package/heart/manifest.md +25 -0
- package/heart/mood_shifts.py +169 -0
- package/heart/phantom_somatic.py +259 -0
- package/heart/predictive.py +374 -0
- package/heart/scars.py +474 -0
- package/heart/somatic.py +482 -0
- package/heart/soul.py +633 -0
- package/heart/telemetry.py +942 -0
- package/heart/triggers.py +119 -0
- package/heart/unconscious.py +443 -0
- package/input/__init__.py +1 -0
- package/input/manifest.md +24 -0
- package/input/telegram/__init__.py +1 -0
- package/input/telegram/commands.py +762 -0
- package/input/telegram/listener.py +532 -0
- package/main.py +90 -0
- package/manifest.md +28 -0
- package/mypics/.gitkeep +1 -0
- package/myvids/.gitkeep +1 -0
- package/output/__init__.py +1 -0
- package/output/images/__init__.py +1 -0
- package/output/images/fal_gen.py +43 -0
- package/output/manifest.md +26 -0
- package/output/text/__init__.py +1 -0
- package/output/text/sender.py +22 -0
- package/output/voice/__init__.py +64 -0
- package/output/voice/google_tts.py +252 -0
- package/output/voice/gtts_tts.py +214 -0
- package/output/voice/vibe_tts.py +190 -0
- package/package.json +58 -0
- package/pyproject.toml +23 -0
- package/requirements.txt +21 -0
- package/skills/__init__.py +1 -0
- package/skills/anticipation_engine/__init__.py +8 -0
- package/skills/anticipation_engine/engine.py +618 -0
- package/skills/anticipation_engine/manifest.md +192 -0
- package/skills/calendar/__init__.py +1 -0
- package/skills/content_unlocks/__init__.py +8 -0
- package/skills/content_unlocks/manifest.md +231 -0
- package/skills/content_unlocks/unlocks.py +945 -0
- package/skills/exclusive_moments/__init__.py +8 -0
- package/skills/exclusive_moments/manifest.md +145 -0
- package/skills/exclusive_moments/moments.py +506 -0
- package/skills/intimacy_layers/__init__.py +8 -0
- package/skills/intimacy_layers/layers.py +703 -0
- package/skills/intimacy_layers/manifest.md +203 -0
- package/skills/manifest.md +67 -0
- package/skills/memory_callbacks/__init__.py +9 -0
- package/skills/memory_callbacks/callbacks.py +748 -0
- package/skills/memory_callbacks/manifest.md +170 -0
- package/skills/message_scheduler/__init__.py +19 -0
- package/skills/message_scheduler/manifest.md +107 -0
- package/skills/message_scheduler/scheduler.py +510 -0
- package/skills/photo_manager/__init__.py +1 -0
- package/skills/photo_manager/scanner.py +296 -0
- package/skills/relationship_milestones/__init__.py +8 -0
- package/skills/relationship_milestones/manifest.md +206 -0
- package/skills/relationship_milestones/tracker.py +494 -0
- package/skills/self_authorship/__init__.py +23 -0
- package/skills/self_authorship/author.py +331 -0
- package/skills/self_authorship/manifest.md +24 -0
- package/skills/video_manager/__init__.py +5 -0
- package/skills/video_manager/manifest.md +37 -0
- package/skills/video_manager/scanner.py +229 -0
- package/webui/__init__.py +3 -0
- package/webui/app.py +936 -0
- package/webui/bridge.py +366 -0
- 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}>"
|