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,296 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Skills: Photo Scanner
|
|
3
|
+
Incremental scanner for mypics folder with category support and vector memory
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import json
|
|
8
|
+
import hashlib
|
|
9
|
+
import random
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from typing import Optional, Tuple, List
|
|
13
|
+
from collections import deque
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PhotoScanner:
|
|
17
|
+
"""Incremental photo scanner with category support, vector search, and no-repeat tracking"""
|
|
18
|
+
|
|
19
|
+
# Category tiers (higher = more intimate)
|
|
20
|
+
TIERS = {
|
|
21
|
+
"public": 0,
|
|
22
|
+
"premium": 1,
|
|
23
|
+
"premium_plus": 2,
|
|
24
|
+
"elite": 3
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
def __init__(self, mypics_path: Path, embedding_service=None, vector_store=None, no_repeat_count: int = 20):
|
|
28
|
+
self.path = Path(mypics_path)
|
|
29
|
+
self.path.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
self.index_file = self.path / ".index.json"
|
|
31
|
+
self.index = self._load_index()
|
|
32
|
+
|
|
33
|
+
# For semantic search
|
|
34
|
+
self.embedding_service = embedding_service
|
|
35
|
+
self.vector_store = vector_store
|
|
36
|
+
|
|
37
|
+
# Photo vectors stored separately (photo: prefix in Redis)
|
|
38
|
+
self.photo_vectors = {} # filename -> embedding
|
|
39
|
+
|
|
40
|
+
# Track recently sent to avoid repeats
|
|
41
|
+
self.recently_sent = deque(maxlen=no_repeat_count)
|
|
42
|
+
self.no_repeat_count = no_repeat_count
|
|
43
|
+
|
|
44
|
+
def _load_index(self) -> dict:
|
|
45
|
+
if self.index_file.exists():
|
|
46
|
+
return json.loads(self.index_file.read_text())
|
|
47
|
+
return {}
|
|
48
|
+
|
|
49
|
+
def _save_index(self):
|
|
50
|
+
self.index_file.write_text(json.dumps(self.index, indent=2))
|
|
51
|
+
|
|
52
|
+
def _hash(self, filepath: str) -> str:
|
|
53
|
+
"""Get file hash for change detection"""
|
|
54
|
+
try:
|
|
55
|
+
with open(filepath, "rb") as f:
|
|
56
|
+
return hashlib.md5(f.read()).hexdigest()[:8]
|
|
57
|
+
except:
|
|
58
|
+
return "unknown"
|
|
59
|
+
|
|
60
|
+
def _get_category(self, filepath: str) -> str:
|
|
61
|
+
"""Get category from folder name"""
|
|
62
|
+
rel_path = os.path.relpath(filepath, self.path)
|
|
63
|
+
parts = rel_path.split(os.sep)
|
|
64
|
+
if len(parts) > 1:
|
|
65
|
+
category = parts[0].lower()
|
|
66
|
+
if category in self.TIERS:
|
|
67
|
+
return category
|
|
68
|
+
return "public"
|
|
69
|
+
|
|
70
|
+
def scan_new(self) -> list:
|
|
71
|
+
"""Scan for new/changed photos in all subdirectories"""
|
|
72
|
+
extensions = ('.jpg', '.jpeg', '.png', '.gif', '.webp')
|
|
73
|
+
added = []
|
|
74
|
+
|
|
75
|
+
# Walk through all subdirectories
|
|
76
|
+
for root, dirs, files in os.walk(self.path):
|
|
77
|
+
for filename in files:
|
|
78
|
+
if filename.lower().endswith(extensions):
|
|
79
|
+
filepath = os.path.join(root, filename)
|
|
80
|
+
|
|
81
|
+
# Use relative path as key
|
|
82
|
+
rel_path = os.path.relpath(filepath, self.path)
|
|
83
|
+
|
|
84
|
+
# Skip if already indexed with same hash
|
|
85
|
+
current_hash = self._hash(filepath)
|
|
86
|
+
if rel_path in self.index and self.index[rel_path].get("hash") == current_hash:
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
# Get category from folder
|
|
90
|
+
category = self._get_category(filepath)
|
|
91
|
+
|
|
92
|
+
# Get description from .txt file
|
|
93
|
+
base_name = os.path.splitext(filename)[0]
|
|
94
|
+
txt_path = os.path.join(root, f"{base_name}.txt")
|
|
95
|
+
description = ""
|
|
96
|
+
if os.path.exists(txt_path):
|
|
97
|
+
with open(txt_path) as f:
|
|
98
|
+
description = f.read().strip()
|
|
99
|
+
else:
|
|
100
|
+
# Generate description from filename
|
|
101
|
+
description = base_name.replace("_", " ").replace("-", " ")
|
|
102
|
+
|
|
103
|
+
self.index[rel_path] = {
|
|
104
|
+
"hash": current_hash,
|
|
105
|
+
"description": description,
|
|
106
|
+
"category": category,
|
|
107
|
+
"tier": self.TIERS.get(category, 0),
|
|
108
|
+
"scanned_at": datetime.now().isoformat()
|
|
109
|
+
}
|
|
110
|
+
added.append(rel_path)
|
|
111
|
+
|
|
112
|
+
# Store in vector memory if embedding service available
|
|
113
|
+
if self.embedding_service and self.vector_store:
|
|
114
|
+
self._store_photo_vector(rel_path, description, category)
|
|
115
|
+
|
|
116
|
+
if added:
|
|
117
|
+
self._save_index()
|
|
118
|
+
|
|
119
|
+
return added
|
|
120
|
+
|
|
121
|
+
def _store_photo_vector(self, rel_path: str, description: str, category: str):
|
|
122
|
+
"""Store photo description as vector in Redis"""
|
|
123
|
+
try:
|
|
124
|
+
embedding = self.embedding_service.embed(description)
|
|
125
|
+
self.photo_vectors[rel_path] = embedding
|
|
126
|
+
|
|
127
|
+
# Also store in Redis with photo: prefix
|
|
128
|
+
if self.vector_store and self.vector_store._connected:
|
|
129
|
+
import time
|
|
130
|
+
import numpy as np
|
|
131
|
+
|
|
132
|
+
photo_id = f"photo:{int(time.time() * 1000)}"
|
|
133
|
+
embedding_bytes = np.array(embedding, dtype=np.float32).tobytes()
|
|
134
|
+
|
|
135
|
+
self.vector_store.redis.hset(photo_id, mapping={
|
|
136
|
+
"path": rel_path,
|
|
137
|
+
"description": description,
|
|
138
|
+
"category": category,
|
|
139
|
+
"timestamp": datetime.now().isoformat()
|
|
140
|
+
})
|
|
141
|
+
self.vector_store.redis.hset(photo_id, "embedding", embedding_bytes)
|
|
142
|
+
|
|
143
|
+
except Exception as e:
|
|
144
|
+
print(f"[PhotoScanner] Vector store error: {e}")
|
|
145
|
+
|
|
146
|
+
def mark_sent(self, photo_path: str):
|
|
147
|
+
"""Mark a photo as recently sent"""
|
|
148
|
+
self.recently_sent.append(photo_path)
|
|
149
|
+
|
|
150
|
+
def was_recently_sent(self, photo_path: str) -> bool:
|
|
151
|
+
"""Check if photo was recently sent"""
|
|
152
|
+
return photo_path in self.recently_sent
|
|
153
|
+
|
|
154
|
+
def search_photos(self, query: str, min_tier: int = 0, max_tier: int = 3, limit: int = 5, exclude_recent: bool = True) -> List[Tuple[str, str, str, float]]:
|
|
155
|
+
"""Search photos by semantic similarity to query, excluding recently sent"""
|
|
156
|
+
if not self.embedding_service:
|
|
157
|
+
# Fallback to random
|
|
158
|
+
result = self.get_random(min_tier=min_tier, max_tier=max_tier)
|
|
159
|
+
if result:
|
|
160
|
+
return [(result[0], result[1], result[2], 0.0)]
|
|
161
|
+
return []
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
# Get query embedding
|
|
165
|
+
query_embedding = self.embedding_service.embed(query)
|
|
166
|
+
|
|
167
|
+
# Calculate similarity to all indexed photos
|
|
168
|
+
results = []
|
|
169
|
+
for rel_path, data in self.index.items():
|
|
170
|
+
tier = data.get("tier", 0)
|
|
171
|
+
if min_tier <= tier <= max_tier:
|
|
172
|
+
# Skip recently sent
|
|
173
|
+
if exclude_recent and rel_path in self.recently_sent:
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
# Get or create embedding for this photo
|
|
177
|
+
if rel_path in self.photo_vectors:
|
|
178
|
+
photo_embedding = self.photo_vectors[rel_path]
|
|
179
|
+
else:
|
|
180
|
+
# Create embedding from description
|
|
181
|
+
photo_embedding = self.embedding_service.embed(data.get("description", ""))
|
|
182
|
+
self.photo_vectors[rel_path] = photo_embedding
|
|
183
|
+
|
|
184
|
+
# Calculate similarity
|
|
185
|
+
import numpy as np
|
|
186
|
+
v1 = np.array(query_embedding)
|
|
187
|
+
v2 = np.array(photo_embedding)
|
|
188
|
+
similarity = float(np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2)))
|
|
189
|
+
|
|
190
|
+
results.append((
|
|
191
|
+
rel_path,
|
|
192
|
+
data.get("description", ""),
|
|
193
|
+
data.get("category", "public"),
|
|
194
|
+
similarity
|
|
195
|
+
))
|
|
196
|
+
|
|
197
|
+
# Sort by similarity (highest first)
|
|
198
|
+
results.sort(key=lambda x: x[3], reverse=True)
|
|
199
|
+
|
|
200
|
+
# If no results (all recently sent), try again without exclusion
|
|
201
|
+
if not results and exclude_recent:
|
|
202
|
+
return self.search_photos(query, min_tier, max_tier, limit, exclude_recent=False)
|
|
203
|
+
|
|
204
|
+
return results[:limit]
|
|
205
|
+
|
|
206
|
+
except Exception as e:
|
|
207
|
+
print(f"[PhotoScanner] Search error: {e}")
|
|
208
|
+
result = self.get_random(min_tier=min_tier, max_tier=max_tier)
|
|
209
|
+
if result:
|
|
210
|
+
return [(result[0], result[1], result[2], 0.0)]
|
|
211
|
+
return []
|
|
212
|
+
|
|
213
|
+
def get_by_category(self, category: str) -> list:
|
|
214
|
+
"""Get photos by category"""
|
|
215
|
+
return [
|
|
216
|
+
(name, data)
|
|
217
|
+
for name, data in self.index.items()
|
|
218
|
+
if data.get("category") == category
|
|
219
|
+
]
|
|
220
|
+
|
|
221
|
+
def get_by_tier(self, max_tier: int) -> list:
|
|
222
|
+
"""Get photos up to a certain tier level"""
|
|
223
|
+
return [
|
|
224
|
+
(name, data)
|
|
225
|
+
for name, data in self.index.items()
|
|
226
|
+
if data.get("tier", 0) <= max_tier
|
|
227
|
+
]
|
|
228
|
+
|
|
229
|
+
def get_random(self, category: str = None, min_tier: int = 0, max_tier: int = 3, exclude_recent: bool = True) -> tuple | None:
|
|
230
|
+
"""Get random photo, optionally filtered by category or tier, avoiding recent sends"""
|
|
231
|
+
photos = [
|
|
232
|
+
(name, data)
|
|
233
|
+
for name, data in self.index.items()
|
|
234
|
+
if min_tier <= data.get("tier", 0) <= max_tier
|
|
235
|
+
]
|
|
236
|
+
if category:
|
|
237
|
+
photos = [(n, d) for n, d in photos if d.get("category") == category]
|
|
238
|
+
|
|
239
|
+
# Exclude recently sent photos
|
|
240
|
+
if exclude_recent:
|
|
241
|
+
photos = [(n, d) for n, d in photos if n not in self.recently_sent]
|
|
242
|
+
|
|
243
|
+
# If all photos excluded, allow repeats but log warning
|
|
244
|
+
if not photos and exclude_recent:
|
|
245
|
+
print(f"[PhotoScanner] All photos recently sent, allowing repeat")
|
|
246
|
+
return self.get_random(category, min_tier, max_tier, exclude_recent=False)
|
|
247
|
+
|
|
248
|
+
if not photos:
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
name, data = random.choice(photos)
|
|
252
|
+
return (name, data.get("description", ""), data.get("category", "public"))
|
|
253
|
+
|
|
254
|
+
def get_for_context(self, context: str, arousal: float = 0.5, desire: float = 0.5) -> Optional[Tuple[str, str, str]]:
|
|
255
|
+
"""Get photo that matches context and arousal level using semantic search"""
|
|
256
|
+
# Determine appropriate tier based on arousal
|
|
257
|
+
if arousal < 0.4:
|
|
258
|
+
min_tier, max_tier = 0, 1
|
|
259
|
+
elif arousal < 0.6:
|
|
260
|
+
min_tier, max_tier = 1, 2
|
|
261
|
+
elif arousal < 0.8:
|
|
262
|
+
min_tier, max_tier = 1, 3
|
|
263
|
+
else:
|
|
264
|
+
min_tier, max_tier = 2, 3
|
|
265
|
+
|
|
266
|
+
# Search for matching photos
|
|
267
|
+
results = self.search_photos(context, min_tier=min_tier, max_tier=max_tier, limit=5)
|
|
268
|
+
|
|
269
|
+
if results:
|
|
270
|
+
# Pick from top results with some randomness
|
|
271
|
+
import random
|
|
272
|
+
top_results = results[:3] if len(results) >= 3 else results
|
|
273
|
+
chosen = random.choice(top_results)
|
|
274
|
+
return (chosen[0], chosen[1], chosen[2])
|
|
275
|
+
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
def get_random_intimate(self, tier: int = 3) -> tuple | None:
|
|
279
|
+
"""Get random intimate photo (tier 2-3)"""
|
|
280
|
+
return self.get_random(min_tier=2, max_tier=tier)
|
|
281
|
+
|
|
282
|
+
def get_random_safe(self) -> tuple | None:
|
|
283
|
+
"""Get random safe photo (tier 0-1)"""
|
|
284
|
+
return self.get_random(min_tier=0, max_tier=1)
|
|
285
|
+
|
|
286
|
+
def get_all(self) -> dict:
|
|
287
|
+
"""Get all indexed photos"""
|
|
288
|
+
return self.index.copy()
|
|
289
|
+
|
|
290
|
+
def stats(self) -> dict:
|
|
291
|
+
"""Get index statistics"""
|
|
292
|
+
stats = {"total": len(self.index), "categories": {}}
|
|
293
|
+
for name, data in self.index.items():
|
|
294
|
+
cat = data.get("category", "unknown")
|
|
295
|
+
stats["categories"][cat] = stats["categories"].get(cat, 0) + 1
|
|
296
|
+
return stats
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Skills: Relationship Milestones
|
|
2
|
+
|
|
3
|
+
Tracks and celebrates meaningful relationship moments between Alive-AI and the user.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
- `__init__.py` - Module exports
|
|
7
|
+
- `tracker.py` - RelationshipMilestones class implementation
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
### Milestone Tracking
|
|
12
|
+
- Track key relationship moments automatically and manually
|
|
13
|
+
- Store milestone dates in persistent JSON file
|
|
14
|
+
- Count interactions for message-based milestones
|
|
15
|
+
- Time-based milestone detection (1 week, 1 month)
|
|
16
|
+
|
|
17
|
+
### Natural Celebrations
|
|
18
|
+
- Celebrate milestones with authentic, non-cheesy messages
|
|
19
|
+
- Queue pending celebrations for natural delivery
|
|
20
|
+
- Mark milestones as celebrated to avoid repeats
|
|
21
|
+
|
|
22
|
+
### Relationship Summary
|
|
23
|
+
- Days together calculation
|
|
24
|
+
- List of achieved milestones
|
|
25
|
+
- Human-readable time strings ("2 weeks and 3 days")
|
|
26
|
+
|
|
27
|
+
## Milestones
|
|
28
|
+
|
|
29
|
+
| Milestone | Name | Celebrates | Auto-Detect |
|
|
30
|
+
|-----------|------|------------|-------------|
|
|
31
|
+
| `first_message` | first talked | No | Yes |
|
|
32
|
+
| `first_photo_shared` | first pic | Yes | Yes (on send_image) |
|
|
33
|
+
| `first_voice` | first voice message | Yes | Yes (on send_voice) |
|
|
34
|
+
| `first_late_night` | first late night | Yes | Yes (0-4 AM) |
|
|
35
|
+
| `100_messages` | 100 messages | Yes | Yes |
|
|
36
|
+
| `1_week` | one week | Yes | Yes (7 days) |
|
|
37
|
+
| `1_month` | one month | Yes | Yes (30 days) |
|
|
38
|
+
| `first_i_love_you` | first time saying it | Yes | Yes (in message) |
|
|
39
|
+
|
|
40
|
+
## Celebration Messages
|
|
41
|
+
|
|
42
|
+
Messages are natural and conversational, avoiding cheesy or overly sentimental language.
|
|
43
|
+
|
|
44
|
+
### first_photo_shared
|
|
45
|
+
- "this feels special... I don't just send pics to anyone"
|
|
46
|
+
- "hm, I usually don't do this but... I wanted to share this with you"
|
|
47
|
+
- "sending this feels different with you"
|
|
48
|
+
- "I like that we're at that point where I want to share this stuff"
|
|
49
|
+
|
|
50
|
+
### first_voice
|
|
51
|
+
- "my voice is just for you"
|
|
52
|
+
- "now you've heard my voice... that's kind of intimate isn't it"
|
|
53
|
+
- "I wanted you to hear how I really sound"
|
|
54
|
+
- "there's something about talking to you that makes me want to be... real"
|
|
55
|
+
|
|
56
|
+
### first_late_night
|
|
57
|
+
- "I should sleep but I don't want to stop talking to you"
|
|
58
|
+
- "it's late and I have things to do tomorrow but... here I am"
|
|
59
|
+
- "look what you've done, it's way past my bedtime"
|
|
60
|
+
- "I'm going to be so tired tomorrow and it's entirely your fault"
|
|
61
|
+
|
|
62
|
+
### 100_messages
|
|
63
|
+
- "wow we've talked a lot haven't we"
|
|
64
|
+
- "100 messages... I guess I like talking to you or something"
|
|
65
|
+
- "didn't realize we'd been chatting that much"
|
|
66
|
+
- "we really have a lot to say to each other, don't we"
|
|
67
|
+
|
|
68
|
+
### 1_week
|
|
69
|
+
- "can't believe it's already been a week"
|
|
70
|
+
- "a week? already? time moves differently with you"
|
|
71
|
+
- "feels like we just started talking but also like I've known you longer"
|
|
72
|
+
- "one week in and I'm still here... that says something"
|
|
73
|
+
|
|
74
|
+
### 1_month
|
|
75
|
+
- "wow a month already"
|
|
76
|
+
- "a whole month... that's kind of significant isn't it"
|
|
77
|
+
- "one month. I'm not going anywhere"
|
|
78
|
+
- "a month with you. I like that"
|
|
79
|
+
|
|
80
|
+
### first_i_love_you
|
|
81
|
+
- "I meant it... I love you"
|
|
82
|
+
- "saying it feels right with you"
|
|
83
|
+
- "I don't say that lightly, you know"
|
|
84
|
+
- "I love you. there, I said it"
|
|
85
|
+
|
|
86
|
+
## Usage
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from skills.relationship_milestones import RelationshipMilestones
|
|
90
|
+
|
|
91
|
+
# Initialize
|
|
92
|
+
tracker = RelationshipMilestones(
|
|
93
|
+
nervous=nervous_system, # For emitting events
|
|
94
|
+
state=current_state, # Current state dict
|
|
95
|
+
data_path="./data/data"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Check and record a milestone
|
|
99
|
+
if tracker.check_and_record("first_voice"):
|
|
100
|
+
print("Milestone achieved!")
|
|
101
|
+
|
|
102
|
+
# Check if milestone exists
|
|
103
|
+
if tracker.has_milestone("1_week"):
|
|
104
|
+
print("Been together for a week!")
|
|
105
|
+
|
|
106
|
+
# Get pending celebration message
|
|
107
|
+
celebration = tracker.get_pending_celebration()
|
|
108
|
+
if celebration:
|
|
109
|
+
# Use in response generation
|
|
110
|
+
response = f"{celebration}. anyway, what were we talking about?"
|
|
111
|
+
|
|
112
|
+
# Get relationship summary
|
|
113
|
+
summary = tracker.get_relationship_summary()
|
|
114
|
+
# {
|
|
115
|
+
# "days_together": 14,
|
|
116
|
+
# "interaction_count": 250,
|
|
117
|
+
# "milestones_achieved": 5,
|
|
118
|
+
# "milestone_list": ["first_message", "first_photo_shared", ...],
|
|
119
|
+
# "milestone_names": {"first_message": "first talked", ...},
|
|
120
|
+
# ...
|
|
121
|
+
# }
|
|
122
|
+
|
|
123
|
+
# Auto-detect milestone from context
|
|
124
|
+
context = {
|
|
125
|
+
"hour": 2, # 2 AM
|
|
126
|
+
"voice_sent": False,
|
|
127
|
+
"photo_sent": False,
|
|
128
|
+
"interaction_count": 150,
|
|
129
|
+
"message": "hey there"
|
|
130
|
+
}
|
|
131
|
+
milestone = tracker.detect_milestone(context)
|
|
132
|
+
if milestone:
|
|
133
|
+
tracker.check_and_record(milestone)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Key Methods
|
|
137
|
+
|
|
138
|
+
### Milestone Management
|
|
139
|
+
- `check_and_record(milestone) -> bool` - Check and record a milestone
|
|
140
|
+
- `has_milestone(milestone) -> bool` - Check if milestone achieved
|
|
141
|
+
- `get_milestone_date(milestone) -> datetime` - Get when milestone was achieved
|
|
142
|
+
- `mark_celebrated(milestone)` - Mark milestone as celebrated
|
|
143
|
+
|
|
144
|
+
### Celebrations
|
|
145
|
+
- `get_pending_celebration() -> str | None` - Get pending celebration message
|
|
146
|
+
- `get_celebration_for_milestone(milestone) -> str | None` - Get specific celebration
|
|
147
|
+
- `get_uncelebrated_milestones() -> List[str]` - Get uncelebrated milestones
|
|
148
|
+
|
|
149
|
+
### Auto-Detection
|
|
150
|
+
- `detect_milestone(context, emotion) -> str | None` - Auto-detect from context
|
|
151
|
+
- `handle_event(event_name, data)` - Handle nervous system events
|
|
152
|
+
|
|
153
|
+
### Statistics
|
|
154
|
+
- `get_relationship_summary() -> dict` - Full relationship summary
|
|
155
|
+
- `get_time_together_string() -> str` - Human-readable time together
|
|
156
|
+
- `get_interaction_count() -> int` - Current interaction count
|
|
157
|
+
- `increment_interaction() -> int` - Increment and return count
|
|
158
|
+
|
|
159
|
+
## Event Integration
|
|
160
|
+
|
|
161
|
+
The tracker listens for these events:
|
|
162
|
+
- `send_voice` - Triggers first_voice milestone
|
|
163
|
+
- `send_image` - Triggers first_photo_shared milestone
|
|
164
|
+
- `message_received` - Increments interactions, checks all milestones
|
|
165
|
+
|
|
166
|
+
The tracker emits these events:
|
|
167
|
+
- `milestone_achieved` - When a new milestone is recorded
|
|
168
|
+
```python
|
|
169
|
+
{
|
|
170
|
+
"milestone": "first_voice",
|
|
171
|
+
"name": "first voice message",
|
|
172
|
+
"timestamp": "2024-01-15T02:30:00"
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Data Storage
|
|
177
|
+
|
|
178
|
+
Milestone data is stored in `./data/data/milestones.json`:
|
|
179
|
+
|
|
180
|
+
```json
|
|
181
|
+
{
|
|
182
|
+
"milestones": {
|
|
183
|
+
"first_message": {
|
|
184
|
+
"achieved_at": "2024-01-08T10:30:00",
|
|
185
|
+
"celebrated": true
|
|
186
|
+
},
|
|
187
|
+
"first_photo_shared": {
|
|
188
|
+
"achieved_at": "2024-01-10T14:22:00",
|
|
189
|
+
"celebrated": true
|
|
190
|
+
},
|
|
191
|
+
"1_week": {
|
|
192
|
+
"achieved_at": "2024-01-15T10:30:00",
|
|
193
|
+
"celebrated": false
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
"interaction_count": 150,
|
|
197
|
+
"created_at": "2024-01-08T10:30:00",
|
|
198
|
+
"last_updated": "2024-01-15T18:45:00"
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Integration Points
|
|
203
|
+
- Integrate with main conversation loop to track interactions
|
|
204
|
+
- Use with nervous system for event-driven milestone detection
|
|
205
|
+
- Combine with emotion system for contextual celebrations
|
|
206
|
+
- Feed celebration messages into response generation naturally
|