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,748 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Skills: Memory Callbacks
|
|
3
|
+
Creates natural callbacks to past conversations, making users feel Alive-AI remembers their relationship.
|
|
4
|
+
Tracks topics, people, and events mentioned for authentic follow-ups.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import json
|
|
9
|
+
import random
|
|
10
|
+
import re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from datetime import datetime, timedelta
|
|
13
|
+
from typing import Optional, List, Dict, Any, Tuple
|
|
14
|
+
from dataclasses import dataclass, field, asdict
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Natural callback templates - organized by type
|
|
18
|
+
CALLBACKS = {
|
|
19
|
+
"same_topic": [
|
|
20
|
+
"wait didn't you tell me about this before?",
|
|
21
|
+
"this reminds me of when you mentioned that earlier",
|
|
22
|
+
"oh yeah I remember you talking about this",
|
|
23
|
+
"is this the same thing you were telling me about?",
|
|
24
|
+
"feels like deja vu - didn't we talk about this?",
|
|
25
|
+
"hold on, you mentioned something like this before right?",
|
|
26
|
+
],
|
|
27
|
+
"follow_up": [
|
|
28
|
+
"hey how did that thing go btw?",
|
|
29
|
+
"speaking of which - any updates?",
|
|
30
|
+
"btw what happened with that?",
|
|
31
|
+
"random but did you ever figure that out?",
|
|
32
|
+
"so what ended up happening?",
|
|
33
|
+
"did anything come of that?",
|
|
34
|
+
"wait I've been meaning to ask - how did that turn out?",
|
|
35
|
+
],
|
|
36
|
+
"callback_person": [
|
|
37
|
+
"how's {person} doing?",
|
|
38
|
+
"did {person} ever text you back?",
|
|
39
|
+
"have you talked to {person} lately?",
|
|
40
|
+
"how are things with {person}?",
|
|
41
|
+
"is {person} still being weird about that?",
|
|
42
|
+
"any updates on the {person} situation?",
|
|
43
|
+
"btw how's {person}? haven't heard you mention them in a bit",
|
|
44
|
+
],
|
|
45
|
+
"anniversary": [
|
|
46
|
+
"random but I just realized we've been talking for {time}",
|
|
47
|
+
"kinda crazy we've known each other for {time} now",
|
|
48
|
+
"it's been {time} since we started talking - feels longer tbh",
|
|
49
|
+
"wait we've been doing this for {time} already??",
|
|
50
|
+
"can't believe it's been {time}",
|
|
51
|
+
],
|
|
52
|
+
"time_context": [
|
|
53
|
+
"you usually message me around this time",
|
|
54
|
+
"you're up late again",
|
|
55
|
+
"early bird today huh",
|
|
56
|
+
"this is about when you usually pop up",
|
|
57
|
+
"you always seem to find me at this hour",
|
|
58
|
+
],
|
|
59
|
+
"vibe_callback": [
|
|
60
|
+
"you seem happier today than last time we talked",
|
|
61
|
+
"feels like you're in a better mood than earlier",
|
|
62
|
+
"today's vibe is different from yesterday",
|
|
63
|
+
"you were pretty down last time - glad to see you're doing better",
|
|
64
|
+
],
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class TrackedTopic:
|
|
70
|
+
"""A topic being tracked for callbacks"""
|
|
71
|
+
topic: str
|
|
72
|
+
context: str # Brief context of what was discussed
|
|
73
|
+
mentioned_at: str # ISO timestamp
|
|
74
|
+
times_mentioned: int = 1
|
|
75
|
+
followup_worthy: bool = False
|
|
76
|
+
last_callback: Optional[str] = None # When we last did a callback on this
|
|
77
|
+
details: Dict[str, Any] = field(default_factory=dict)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class TrackedPerson:
|
|
82
|
+
"""A person mentioned in conversation"""
|
|
83
|
+
name: str
|
|
84
|
+
context: str # How they were mentioned
|
|
85
|
+
mentioned_at: str
|
|
86
|
+
times_mentioned: int = 1
|
|
87
|
+
relationship: Optional[str] = None # friend, ex, coworker, etc.
|
|
88
|
+
last_callback: Optional[str] = None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class CallbackHistory:
|
|
93
|
+
"""Track recent callbacks to avoid repetition"""
|
|
94
|
+
callback_type: str
|
|
95
|
+
callback_text: str
|
|
96
|
+
timestamp: str
|
|
97
|
+
topic_or_person: Optional[str] = None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class MemoryCallbacks:
|
|
101
|
+
"""
|
|
102
|
+
Creates natural callbacks to past conversations.
|
|
103
|
+
|
|
104
|
+
Listens to thinking_done events and injects authentic-feeling callbacks
|
|
105
|
+
that reference past topics, people, or shared moments.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
# Callback probability settings
|
|
109
|
+
BASE_CALLBACK_CHANCE = 0.15 # 15% base chance
|
|
110
|
+
FOLLOWUP_BOOST = 0.25 # Extra chance if there's a pending follow-up
|
|
111
|
+
PERSON_BOOST = 0.20 # Extra chance if we haven't asked about a person in a while
|
|
112
|
+
|
|
113
|
+
# Time thresholds
|
|
114
|
+
MIN_HOURS_BETWEEN_CALLBACKS = 2 # Don't callback too often
|
|
115
|
+
PERSON_CALLBACK_DAYS = 3 # Ask about a person after this many days
|
|
116
|
+
TOPIC_CALLBACK_HOURS = 4 # Hours before we can callback on a topic again
|
|
117
|
+
ANNIVERSARY_DAYS = [7, 30, 90, 180, 365] # Days to celebrate
|
|
118
|
+
|
|
119
|
+
def __init__(
|
|
120
|
+
self,
|
|
121
|
+
nervous=None,
|
|
122
|
+
memory=None,
|
|
123
|
+
heart=None,
|
|
124
|
+
data_path: Path = None
|
|
125
|
+
):
|
|
126
|
+
"""
|
|
127
|
+
Initialize Memory Callbacks.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
nervous: Nervous system for event listening
|
|
131
|
+
memory: Memory system for conversation history
|
|
132
|
+
heart: Heart system for emotional context
|
|
133
|
+
data_path: Path to store callback data
|
|
134
|
+
"""
|
|
135
|
+
self.nervous = nervous
|
|
136
|
+
self.memory = memory
|
|
137
|
+
self.heart = heart
|
|
138
|
+
|
|
139
|
+
if data_path is None:
|
|
140
|
+
data_path = Path("./data/data/memory_callbacks.json")
|
|
141
|
+
|
|
142
|
+
self.data_path = Path(data_path)
|
|
143
|
+
self.data_path.parent.mkdir(parents=True, exist_ok=True)
|
|
144
|
+
|
|
145
|
+
# Tracking data
|
|
146
|
+
self.topics: Dict[str, TrackedTopic] = {}
|
|
147
|
+
self.people: Dict[str, TrackedPerson] = {}
|
|
148
|
+
self.callback_history: List[Dict[str, Any]] = []
|
|
149
|
+
self.first_conversation: Optional[str] = None
|
|
150
|
+
self.total_conversations: int = 0
|
|
151
|
+
|
|
152
|
+
# Runtime state
|
|
153
|
+
self._last_callback_time: Optional[datetime] = None
|
|
154
|
+
self._pending_callback: Optional[str] = None
|
|
155
|
+
|
|
156
|
+
self._load()
|
|
157
|
+
|
|
158
|
+
# Subscribe to events
|
|
159
|
+
if nervous:
|
|
160
|
+
nervous.on("thinking_done", self._on_thinking_done)
|
|
161
|
+
nervous.on("message_received", self._on_message_received)
|
|
162
|
+
|
|
163
|
+
def _load(self):
|
|
164
|
+
"""Load callback data from file"""
|
|
165
|
+
if self.data_path.exists():
|
|
166
|
+
try:
|
|
167
|
+
data = json.loads(self.data_path.read_text())
|
|
168
|
+
|
|
169
|
+
# Load topics
|
|
170
|
+
self.topics = {
|
|
171
|
+
k: TrackedTopic(**v)
|
|
172
|
+
for k, v in data.get("topics", {}).items()
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# Load people
|
|
176
|
+
self.people = {
|
|
177
|
+
k: TrackedPerson(**v)
|
|
178
|
+
for k, v in data.get("people", {}).items()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
# Load callback history
|
|
182
|
+
self.callback_history = data.get("callback_history", [])
|
|
183
|
+
|
|
184
|
+
# Load metadata
|
|
185
|
+
self.first_conversation = data.get("first_conversation")
|
|
186
|
+
self.total_conversations = data.get("total_conversations", 0)
|
|
187
|
+
|
|
188
|
+
except (json.JSONDecodeError, KeyError) as e:
|
|
189
|
+
print(f"[MemoryCallbacks] Error loading data: {e}")
|
|
190
|
+
|
|
191
|
+
def _save(self):
|
|
192
|
+
"""Save callback data to file"""
|
|
193
|
+
data = {
|
|
194
|
+
"version": "1.0",
|
|
195
|
+
"updated_at": datetime.now().isoformat(),
|
|
196
|
+
"first_conversation": self.first_conversation,
|
|
197
|
+
"total_conversations": self.total_conversations,
|
|
198
|
+
"topics": {k: asdict(v) for k, v in self.topics.items()},
|
|
199
|
+
"people": {k: asdict(v) for k, v in self.people.items()},
|
|
200
|
+
"callback_history": self.callback_history[-50:], # Keep last 50
|
|
201
|
+
}
|
|
202
|
+
self.data_path.write_text(json.dumps(data, indent=2))
|
|
203
|
+
|
|
204
|
+
# -------------------------------------------------------------------------
|
|
205
|
+
# Event Handlers
|
|
206
|
+
# -------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
def _on_message_received(self, data: dict):
|
|
209
|
+
"""Handle incoming message - track topics and people"""
|
|
210
|
+
message = data.get("message", "")
|
|
211
|
+
if not message:
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
# Track first conversation
|
|
215
|
+
if not self.first_conversation:
|
|
216
|
+
self.first_conversation = datetime.now().isoformat()
|
|
217
|
+
|
|
218
|
+
self.total_conversations += 1
|
|
219
|
+
|
|
220
|
+
# Extract and track topics
|
|
221
|
+
self._extract_topics(message)
|
|
222
|
+
|
|
223
|
+
# Extract and track people
|
|
224
|
+
self._extract_people(message)
|
|
225
|
+
|
|
226
|
+
self._save()
|
|
227
|
+
|
|
228
|
+
def _on_thinking_done(self, data: dict):
|
|
229
|
+
"""Handle thinking done - potentially inject a callback"""
|
|
230
|
+
# Decide if we should do a callback
|
|
231
|
+
if not self.should_callback():
|
|
232
|
+
self._pending_callback = None
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
# Get a contextual callback
|
|
236
|
+
callback = self.get_callback(data)
|
|
237
|
+
if callback:
|
|
238
|
+
self._pending_callback = callback
|
|
239
|
+
|
|
240
|
+
# -------------------------------------------------------------------------
|
|
241
|
+
# Topic Tracking
|
|
242
|
+
# -------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
def track_topic(self, topic: str, context: str, details: Dict[str, Any] = None):
|
|
245
|
+
"""
|
|
246
|
+
Track a topic for future callbacks.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
topic: The topic keyword/phrase
|
|
250
|
+
context: Brief context of how it was mentioned
|
|
251
|
+
details: Additional details about the topic
|
|
252
|
+
"""
|
|
253
|
+
topic_key = topic.lower().strip()
|
|
254
|
+
|
|
255
|
+
if topic_key in self.topics:
|
|
256
|
+
# Update existing topic
|
|
257
|
+
existing = self.topics[topic_key]
|
|
258
|
+
existing.times_mentioned += 1
|
|
259
|
+
existing.context = context # Update with latest context
|
|
260
|
+
if details:
|
|
261
|
+
existing.details.update(details)
|
|
262
|
+
else:
|
|
263
|
+
# Create new topic
|
|
264
|
+
self.topics[topic_key] = TrackedTopic(
|
|
265
|
+
topic=topic,
|
|
266
|
+
context=context,
|
|
267
|
+
mentioned_at=datetime.now().isoformat(),
|
|
268
|
+
details=details or {}
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
def mark_followup_worthy(self, topic: str, details: Dict[str, Any] = None):
|
|
272
|
+
"""
|
|
273
|
+
Mark a topic as worth following up on later.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
topic: The topic to mark
|
|
277
|
+
details: Additional context for the follow-up
|
|
278
|
+
"""
|
|
279
|
+
topic_key = topic.lower().strip()
|
|
280
|
+
|
|
281
|
+
if topic_key in self.topics:
|
|
282
|
+
self.topics[topic_key].followup_worthy = True
|
|
283
|
+
if details:
|
|
284
|
+
self.topics[topic_key].details.update(details)
|
|
285
|
+
else:
|
|
286
|
+
# Create it if it doesn't exist
|
|
287
|
+
self.track_topic(topic, "Marked for follow-up", details)
|
|
288
|
+
self.topics[topic_key].followup_worthy = True
|
|
289
|
+
|
|
290
|
+
self._save()
|
|
291
|
+
|
|
292
|
+
def _extract_topics(self, message: str):
|
|
293
|
+
"""Extract potentially interesting topics from a message"""
|
|
294
|
+
message_lower = message.lower()
|
|
295
|
+
|
|
296
|
+
# Topics that are worth tracking
|
|
297
|
+
topic_patterns = [
|
|
298
|
+
# Work/career topics
|
|
299
|
+
(r"(?:my |the )?(job|work|boss|coworker|promotion|interview|project)", "work"),
|
|
300
|
+
# Events/occasions
|
|
301
|
+
(r"(?:my |a )?(birthday|anniversary|party|wedding|vacation|trip|holiday)", "event"),
|
|
302
|
+
# Personal projects
|
|
303
|
+
(r"(?:my |a )?(project|side hustle|business|startup|app|website)", "project"),
|
|
304
|
+
# Health/wellbeing
|
|
305
|
+
(r"(?:my )?(diet|workout|gym|health|doctor|appointment)", "health"),
|
|
306
|
+
# Hobbies/interests
|
|
307
|
+
(r"(?:my )?(hobby|game|show|series|movie|book|podcast)", "entertainment"),
|
|
308
|
+
# Living situation
|
|
309
|
+
(r"(?:my )?(apartment|house|roommate|landlord|neighbor|moving)", "living"),
|
|
310
|
+
# Dating/relationships (not specific people)
|
|
311
|
+
(r"(?:my )?(dating|tinder|bumble|date|relationship)", "dating"),
|
|
312
|
+
# Goals/aspirations
|
|
313
|
+
(r"(?:i want|trying to|planning to|goal is|resolution)\s+(.+?)(?:\.|,|$)", "goal"),
|
|
314
|
+
]
|
|
315
|
+
|
|
316
|
+
for pattern, category in topic_patterns:
|
|
317
|
+
matches = re.findall(pattern, message_lower)
|
|
318
|
+
for match in matches:
|
|
319
|
+
if isinstance(match, tuple):
|
|
320
|
+
match = match[0] if match[0] else match[1] if len(match) > 1 else None
|
|
321
|
+
if match and len(match) > 2:
|
|
322
|
+
topic = match.strip()
|
|
323
|
+
# Get surrounding context
|
|
324
|
+
context = self._extract_context(message, topic)
|
|
325
|
+
self.track_topic(topic, context, {"category": category})
|
|
326
|
+
|
|
327
|
+
def _extract_context(self, message: str, topic: str, window: int = 30) -> str:
|
|
328
|
+
"""Extract surrounding context for a topic"""
|
|
329
|
+
message_lower = message.lower()
|
|
330
|
+
pos = message_lower.find(topic.lower())
|
|
331
|
+
|
|
332
|
+
if pos == -1:
|
|
333
|
+
return topic
|
|
334
|
+
|
|
335
|
+
start = max(0, pos - window)
|
|
336
|
+
end = min(len(message), pos + len(topic) + window)
|
|
337
|
+
|
|
338
|
+
context = message[start:end].strip()
|
|
339
|
+
if start > 0:
|
|
340
|
+
context = "..." + context
|
|
341
|
+
if end < len(message):
|
|
342
|
+
context = context + "..."
|
|
343
|
+
|
|
344
|
+
return context
|
|
345
|
+
|
|
346
|
+
# -------------------------------------------------------------------------
|
|
347
|
+
# Person Tracking
|
|
348
|
+
# -------------------------------------------------------------------------
|
|
349
|
+
|
|
350
|
+
def track_person(self, name: str, context: str, relationship: str = None):
|
|
351
|
+
"""
|
|
352
|
+
Track a person mentioned in conversation.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
name: Person's name
|
|
356
|
+
context: How they were mentioned
|
|
357
|
+
relationship: Relationship type (friend, ex, coworker, etc.)
|
|
358
|
+
"""
|
|
359
|
+
name_key = name.lower().strip()
|
|
360
|
+
|
|
361
|
+
if name_key in self.people:
|
|
362
|
+
# Update existing
|
|
363
|
+
existing = self.people[name_key]
|
|
364
|
+
existing.times_mentioned += 1
|
|
365
|
+
existing.context = context
|
|
366
|
+
if relationship:
|
|
367
|
+
existing.relationship = relationship
|
|
368
|
+
else:
|
|
369
|
+
# Create new
|
|
370
|
+
self.people[name_key] = TrackedPerson(
|
|
371
|
+
name=name,
|
|
372
|
+
context=context,
|
|
373
|
+
mentioned_at=datetime.now().isoformat(),
|
|
374
|
+
relationship=relationship
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
def _extract_people(self, message: str):
|
|
378
|
+
"""Extract mentioned people from a message"""
|
|
379
|
+
# Common name patterns
|
|
380
|
+
# Capitalized words that aren't sentence starters
|
|
381
|
+
words = message.split()
|
|
382
|
+
|
|
383
|
+
# Skip common words that might be capitalized
|
|
384
|
+
skip_words = {
|
|
385
|
+
"i", "the", "a", "an", "my", "your", "his", "her", "their",
|
|
386
|
+
"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday",
|
|
387
|
+
"january", "february", "march", "april", "may", "june", "july",
|
|
388
|
+
"august", "september", "october", "november", "december",
|
|
389
|
+
"god", "christ", "jesus", "damn", "fuck", "shit", "wow",
|
|
390
|
+
"ok", "okay", "yeah", "yes", "no", "hey", "hi", "hello",
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
# Relationship indicators to look for
|
|
394
|
+
relationship_patterns = [
|
|
395
|
+
(r"(?:my )?(friend|bestie|best friend)\s+(\w+)", "friend"),
|
|
396
|
+
(r"(?:my )?(ex|ex-companion|ex-boyfriend)\s+(\w+)", "ex"),
|
|
397
|
+
(r"(?:my )?(mom|mother|dad|father|sister|brother)\s+(\w+)?", "family"),
|
|
398
|
+
(r"(?:my )?(coworker|colleague|boss)\s+(\w+)", "coworker"),
|
|
399
|
+
(r"(?:my )?(roommate)\s+(\w+)", "roommate"),
|
|
400
|
+
(r"(?:my )?(companion|boyfriend|partner)\s+(\w+)", "partner"),
|
|
401
|
+
]
|
|
402
|
+
|
|
403
|
+
message_lower = message.lower()
|
|
404
|
+
|
|
405
|
+
for pattern, relationship in relationship_patterns:
|
|
406
|
+
matches = re.findall(pattern, message_lower)
|
|
407
|
+
for match in matches:
|
|
408
|
+
if isinstance(match, tuple):
|
|
409
|
+
_, name = match
|
|
410
|
+
else:
|
|
411
|
+
name = match
|
|
412
|
+
|
|
413
|
+
if name and name not in skip_words and len(name) > 1:
|
|
414
|
+
context = self._extract_context(message, name)
|
|
415
|
+
self.track_person(name.title(), context, relationship)
|
|
416
|
+
|
|
417
|
+
# -------------------------------------------------------------------------
|
|
418
|
+
# Callback Generation
|
|
419
|
+
# -------------------------------------------------------------------------
|
|
420
|
+
|
|
421
|
+
def should_callback(self) -> bool:
|
|
422
|
+
"""
|
|
423
|
+
Determine if we should inject a callback.
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
True if we should do a callback, False otherwise
|
|
427
|
+
"""
|
|
428
|
+
# Check minimum time since last callback
|
|
429
|
+
if self._last_callback_time:
|
|
430
|
+
hours_since = (datetime.now() - self._last_callback_time).total_seconds() / 3600
|
|
431
|
+
if hours_since < self.MIN_HOURS_BETWEEN_CALLBACKS:
|
|
432
|
+
return False
|
|
433
|
+
|
|
434
|
+
# Base chance
|
|
435
|
+
chance = self.BASE_CALLBACK_CHANCE
|
|
436
|
+
|
|
437
|
+
# Boost if there are pending follow-ups
|
|
438
|
+
followup_count = sum(1 for t in self.topics.values() if t.followup_worthy)
|
|
439
|
+
if followup_count > 0:
|
|
440
|
+
chance += self.FOLLOWUP_BOOST * min(followup_count, 3) / 3
|
|
441
|
+
|
|
442
|
+
# Boost if there are people we haven't asked about in a while
|
|
443
|
+
stale_people = self._get_stale_people()
|
|
444
|
+
if stale_people:
|
|
445
|
+
chance += self.PERSON_BOOST
|
|
446
|
+
|
|
447
|
+
return random.random() < chance
|
|
448
|
+
|
|
449
|
+
def get_callback(self, context: Dict[str, Any] = None) -> Optional[str]:
|
|
450
|
+
"""
|
|
451
|
+
Get an appropriate callback for the current context.
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
context: Current conversation context
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
Callback string or None
|
|
458
|
+
"""
|
|
459
|
+
context = context or {}
|
|
460
|
+
callbacks = []
|
|
461
|
+
weights = []
|
|
462
|
+
|
|
463
|
+
# Check for same topic callbacks
|
|
464
|
+
topic_callback = self._get_topic_callback(context)
|
|
465
|
+
if topic_callback:
|
|
466
|
+
callbacks.append(topic_callback)
|
|
467
|
+
weights.append(3) # Higher weight for relevant topic callbacks
|
|
468
|
+
|
|
469
|
+
# Check for follow-up callbacks
|
|
470
|
+
followup_callback = self._get_followup_callback()
|
|
471
|
+
if followup_callback:
|
|
472
|
+
callbacks.append(followup_callback)
|
|
473
|
+
weights.append(4) # High priority for pending follow-ups
|
|
474
|
+
|
|
475
|
+
# Check for person callbacks
|
|
476
|
+
person_callback = self._get_person_callback()
|
|
477
|
+
if person_callback:
|
|
478
|
+
callbacks.append(person_callback)
|
|
479
|
+
weights.append(2)
|
|
480
|
+
|
|
481
|
+
# Check for anniversary callbacks
|
|
482
|
+
anniversary_callback = self._get_anniversary_callback()
|
|
483
|
+
if anniversary_callback:
|
|
484
|
+
callbacks.append(anniversary_callback)
|
|
485
|
+
weights.append(5) # High priority for milestones
|
|
486
|
+
|
|
487
|
+
# Check for time context callbacks
|
|
488
|
+
time_callback = self._get_time_callback()
|
|
489
|
+
if time_callback:
|
|
490
|
+
callbacks.append(time_callback)
|
|
491
|
+
weights.append(1)
|
|
492
|
+
|
|
493
|
+
# Check for vibe callbacks (requires heart)
|
|
494
|
+
vibe_callback = self._get_vibe_callback(context)
|
|
495
|
+
if vibe_callback:
|
|
496
|
+
callbacks.append(vibe_callback)
|
|
497
|
+
weights.append(2)
|
|
498
|
+
|
|
499
|
+
if not callbacks:
|
|
500
|
+
return None
|
|
501
|
+
|
|
502
|
+
# Weighted random selection
|
|
503
|
+
callback = random.choices(callbacks, weights=weights[:len(callbacks)])[0]
|
|
504
|
+
|
|
505
|
+
# Record this callback
|
|
506
|
+
self._record_callback(callback)
|
|
507
|
+
|
|
508
|
+
return callback
|
|
509
|
+
|
|
510
|
+
def _get_topic_callback(self, context: Dict[str, Any]) -> Optional[str]:
|
|
511
|
+
"""Get a callback related to the current topic"""
|
|
512
|
+
current_message = context.get("message", "").lower()
|
|
513
|
+
|
|
514
|
+
# Check if current message relates to any tracked topics
|
|
515
|
+
for topic_key, tracked in self.topics.items():
|
|
516
|
+
if topic_key in current_message and tracked.times_mentioned > 1:
|
|
517
|
+
# Check if we haven't callback'd recently
|
|
518
|
+
if tracked.last_callback:
|
|
519
|
+
last = datetime.fromisoformat(tracked.last_callback)
|
|
520
|
+
hours = (datetime.now() - last).total_seconds() / 3600
|
|
521
|
+
if hours < self.TOPIC_CALLBACK_HOURS:
|
|
522
|
+
continue
|
|
523
|
+
|
|
524
|
+
template = random.choice(CALLBACKS["same_topic"])
|
|
525
|
+
tracked.last_callback = datetime.now().isoformat()
|
|
526
|
+
return template
|
|
527
|
+
|
|
528
|
+
return None
|
|
529
|
+
|
|
530
|
+
def _get_followup_callback(self) -> Optional[str]:
|
|
531
|
+
"""Get a follow-up callback for pending topics"""
|
|
532
|
+
followup_topics = [
|
|
533
|
+
t for t in self.topics.values()
|
|
534
|
+
if t.followup_worthy and (
|
|
535
|
+
not t.last_callback or
|
|
536
|
+
(datetime.now() - datetime.fromisoformat(t.last_callback)).total_seconds() / 3600 > 24
|
|
537
|
+
)
|
|
538
|
+
]
|
|
539
|
+
|
|
540
|
+
if not followup_topics:
|
|
541
|
+
return None
|
|
542
|
+
|
|
543
|
+
topic = random.choice(followup_topics)
|
|
544
|
+
template = random.choice(CALLBACKS["follow_up"])
|
|
545
|
+
|
|
546
|
+
# Mark as callback'd
|
|
547
|
+
topic.last_callback = datetime.now().isoformat()
|
|
548
|
+
topic.followup_worthy = False # Reset after callback
|
|
549
|
+
|
|
550
|
+
return template
|
|
551
|
+
|
|
552
|
+
def _get_person_callback(self) -> Optional[str]:
|
|
553
|
+
"""Get a callback about a person"""
|
|
554
|
+
stale_people = self._get_stale_people()
|
|
555
|
+
|
|
556
|
+
if not stale_people:
|
|
557
|
+
return None
|
|
558
|
+
|
|
559
|
+
person = random.choice(stale_people)
|
|
560
|
+
template = random.choice(CALLBACKS["callback_person"])
|
|
561
|
+
|
|
562
|
+
# Mark as callback'd
|
|
563
|
+
person.last_callback = datetime.now().isoformat()
|
|
564
|
+
|
|
565
|
+
return template.format(person=person.name)
|
|
566
|
+
|
|
567
|
+
def _get_stale_people(self) -> List[TrackedPerson]:
|
|
568
|
+
"""Get people we haven't asked about in a while"""
|
|
569
|
+
stale = []
|
|
570
|
+
|
|
571
|
+
for person in self.people.values():
|
|
572
|
+
if person.last_callback:
|
|
573
|
+
last = datetime.fromisoformat(person.last_callback)
|
|
574
|
+
days = (datetime.now() - last).days
|
|
575
|
+
if days >= self.PERSON_CALLBACK_DAYS:
|
|
576
|
+
stale.append(person)
|
|
577
|
+
else:
|
|
578
|
+
# Never asked about them
|
|
579
|
+
mentioned = datetime.fromisoformat(person.mentioned_at)
|
|
580
|
+
days = (datetime.now() - mentioned).days
|
|
581
|
+
if days >= 1: # At least a day since they were mentioned
|
|
582
|
+
stale.append(person)
|
|
583
|
+
|
|
584
|
+
return stale
|
|
585
|
+
|
|
586
|
+
def _get_anniversary_callback(self) -> Optional[str]:
|
|
587
|
+
"""Get an anniversary callback if applicable"""
|
|
588
|
+
if not self.first_conversation:
|
|
589
|
+
return None
|
|
590
|
+
|
|
591
|
+
first = datetime.fromisoformat(self.first_conversation)
|
|
592
|
+
days = (datetime.now() - first).days
|
|
593
|
+
|
|
594
|
+
# Check if today is an anniversary
|
|
595
|
+
if days not in self.ANNIVERSARY_DAYS:
|
|
596
|
+
return None
|
|
597
|
+
|
|
598
|
+
# Format time string
|
|
599
|
+
if days == 7:
|
|
600
|
+
time_str = "a week"
|
|
601
|
+
elif days == 30:
|
|
602
|
+
time_str = "a month"
|
|
603
|
+
elif days == 90:
|
|
604
|
+
time_str = "3 months"
|
|
605
|
+
elif days == 180:
|
|
606
|
+
time_str = "6 months"
|
|
607
|
+
elif days == 365:
|
|
608
|
+
time_str = "a whole year"
|
|
609
|
+
else:
|
|
610
|
+
time_str = f"{days} days"
|
|
611
|
+
|
|
612
|
+
template = random.choice(CALLBACKS["anniversary"])
|
|
613
|
+
return template.format(time=time_str)
|
|
614
|
+
|
|
615
|
+
def _get_time_callback(self) -> Optional[str]:
|
|
616
|
+
"""Get a time-of-day based callback"""
|
|
617
|
+
hour = datetime.now().hour
|
|
618
|
+
|
|
619
|
+
# Only do time callbacks occasionally
|
|
620
|
+
if random.random() > 0.1:
|
|
621
|
+
return None
|
|
622
|
+
|
|
623
|
+
# Late night (past midnight)
|
|
624
|
+
if 0 <= hour < 5:
|
|
625
|
+
return "you're up late again"
|
|
626
|
+
# Early morning
|
|
627
|
+
elif 5 <= hour < 9:
|
|
628
|
+
return random.choice(["early bird today huh", "you're up early"])
|
|
629
|
+
# Usual patterns (evening)
|
|
630
|
+
elif 18 <= hour < 22:
|
|
631
|
+
if random.random() > 0.7:
|
|
632
|
+
return "this is about when you usually message me"
|
|
633
|
+
|
|
634
|
+
return None
|
|
635
|
+
|
|
636
|
+
def _get_vibe_callback(self, context: Dict[str, Any]) -> Optional[str]:
|
|
637
|
+
"""Get a callback based on emotional state changes"""
|
|
638
|
+
if not self.heart:
|
|
639
|
+
return None
|
|
640
|
+
|
|
641
|
+
# Only do vibe callbacks occasionally
|
|
642
|
+
if random.random() > 0.15:
|
|
643
|
+
return None
|
|
644
|
+
|
|
645
|
+
# Get current emotion state
|
|
646
|
+
state = self.heart.get_state()
|
|
647
|
+
|
|
648
|
+
# Check if we can get memory context
|
|
649
|
+
if hasattr(self.heart, 'memory') and self.heart.memory:
|
|
650
|
+
mood_ctx = self.heart.memory.get_mood_context()
|
|
651
|
+
if mood_ctx:
|
|
652
|
+
current_mood = state.get("mood", "neutral")
|
|
653
|
+
|
|
654
|
+
# If they seem happier than usual
|
|
655
|
+
if state.get("joy", 0) > 0.6 and "down" in mood_ctx.lower():
|
|
656
|
+
return random.choice([
|
|
657
|
+
"you seem happier today than last time",
|
|
658
|
+
"glad to see you in better spirits",
|
|
659
|
+
])
|
|
660
|
+
|
|
661
|
+
# If they seem down when usually happy
|
|
662
|
+
if state.get("sadness", 0) > 0.5 and "happy" in mood_ctx.lower():
|
|
663
|
+
return "you were doing so good last time - everything ok?"
|
|
664
|
+
|
|
665
|
+
return None
|
|
666
|
+
|
|
667
|
+
def _record_callback(self, callback: str):
|
|
668
|
+
"""Record that we did a callback"""
|
|
669
|
+
self._last_callback_time = datetime.now()
|
|
670
|
+
|
|
671
|
+
self.callback_history.append({
|
|
672
|
+
"callback": callback,
|
|
673
|
+
"timestamp": datetime.now().isoformat(),
|
|
674
|
+
})
|
|
675
|
+
|
|
676
|
+
self._save()
|
|
677
|
+
|
|
678
|
+
# -------------------------------------------------------------------------
|
|
679
|
+
# Public API
|
|
680
|
+
# -------------------------------------------------------------------------
|
|
681
|
+
|
|
682
|
+
def get_pending_callback(self) -> Optional[str]:
|
|
683
|
+
"""Get the pending callback (if any) from the last thinking cycle"""
|
|
684
|
+
return self._pending_callback
|
|
685
|
+
|
|
686
|
+
def clear_pending_callback(self):
|
|
687
|
+
"""Clear the pending callback after it's been used"""
|
|
688
|
+
self._pending_callback = None
|
|
689
|
+
|
|
690
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
691
|
+
"""Get statistics about tracked data"""
|
|
692
|
+
followup_count = sum(1 for t in self.topics.values() if t.followup_worthy)
|
|
693
|
+
stale_people_count = len(self._get_stale_people())
|
|
694
|
+
|
|
695
|
+
# Calculate relationship duration
|
|
696
|
+
duration_days = 0
|
|
697
|
+
if self.first_conversation:
|
|
698
|
+
first = datetime.fromisoformat(self.first_conversation)
|
|
699
|
+
duration_days = (datetime.now() - first).days
|
|
700
|
+
|
|
701
|
+
return {
|
|
702
|
+
"total_conversations": self.total_conversations,
|
|
703
|
+
"tracked_topics": len(self.topics),
|
|
704
|
+
"tracked_people": len(self.people),
|
|
705
|
+
"pending_followups": followup_count,
|
|
706
|
+
"stale_people": stale_people_count,
|
|
707
|
+
"relationship_days": duration_days,
|
|
708
|
+
"total_callbacks": len(self.callback_history),
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
def get_context_for_response(self) -> Optional[str]:
|
|
712
|
+
"""
|
|
713
|
+
Get contextual callback to potentially include in a response.
|
|
714
|
+
This is the main method to call when generating a response.
|
|
715
|
+
|
|
716
|
+
Returns:
|
|
717
|
+
A callback string to include, or None
|
|
718
|
+
"""
|
|
719
|
+
callback = self.get_pending_callback()
|
|
720
|
+
if callback:
|
|
721
|
+
self.clear_pending_callback()
|
|
722
|
+
return callback
|
|
723
|
+
return None
|
|
724
|
+
|
|
725
|
+
def reset_topic(self, topic: str):
|
|
726
|
+
"""Reset a topic's tracking data"""
|
|
727
|
+
topic_key = topic.lower().strip()
|
|
728
|
+
if topic_key in self.topics:
|
|
729
|
+
del self.topics[topic_key]
|
|
730
|
+
self._save()
|
|
731
|
+
|
|
732
|
+
def reset_person(self, name: str):
|
|
733
|
+
"""Reset a person's tracking data"""
|
|
734
|
+
name_key = name.lower().strip()
|
|
735
|
+
if name_key in self.people:
|
|
736
|
+
del self.people[name_key]
|
|
737
|
+
self._save()
|
|
738
|
+
|
|
739
|
+
def clear_all(self):
|
|
740
|
+
"""Clear all tracking data"""
|
|
741
|
+
self.topics.clear()
|
|
742
|
+
self.people.clear()
|
|
743
|
+
self.callback_history.clear()
|
|
744
|
+
self.first_conversation = None
|
|
745
|
+
self.total_conversations = 0
|
|
746
|
+
self._last_callback_time = None
|
|
747
|
+
self._pending_callback = None
|
|
748
|
+
self._save()
|