alive-ai 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/Dockerfile +24 -0
  2. package/LICENSE +21 -0
  3. package/README.md +143 -0
  4. package/alive_ai/__init__.py +3 -0
  5. package/brain/__init__.py +59 -0
  6. package/brain/almost_said.py +154 -0
  7. package/brain/bid_detector.py +636 -0
  8. package/brain/conversation_flow.py +135 -0
  9. package/brain/curiosity.py +328 -0
  10. package/brain/default_mode.py +1438 -0
  11. package/brain/dreams.py +220 -0
  12. package/brain/embeddings/__init__.py +82 -0
  13. package/brain/emotional_memory.py +949 -0
  14. package/brain/global_activity.py +173 -0
  15. package/brain/group_dynamics.py +63 -0
  16. package/brain/linguistic.py +235 -0
  17. package/brain/llm/__init__.py +63 -0
  18. package/brain/llm/base.py +33 -0
  19. package/brain/llm/fallback_router.py +309 -0
  20. package/brain/llm/manifest.md +30 -0
  21. package/brain/llm/ollama.py +218 -0
  22. package/brain/llm/openrouter.py +151 -0
  23. package/brain/llm/provider.py +205 -0
  24. package/brain/llm/unified.py +423 -0
  25. package/brain/llm/zai.py +169 -0
  26. package/brain/manifest.md +23 -0
  27. package/brain/memory/__init__.py +123 -0
  28. package/brain/memory/episodic.py +92 -0
  29. package/brain/memory/fact_extractor.py +209 -0
  30. package/brain/memory/index.py +54 -0
  31. package/brain/memory/manager.py +151 -0
  32. package/brain/memory/summarizer.py +102 -0
  33. package/brain/memory/vector_store.py +297 -0
  34. package/brain/memory/working.py +43 -0
  35. package/brain/narrative.py +343 -0
  36. package/brain/stt/__init__.py +4 -0
  37. package/brain/stt/google_stt.py +83 -0
  38. package/brain/stt/whisper_stt.py +82 -0
  39. package/brain/subconscious/__init__.py +33 -0
  40. package/brain/subconscious/actions.py +136 -0
  41. package/brain/subconscious/evaluation.py +166 -0
  42. package/brain/subconscious/goal_system.py +90 -0
  43. package/brain/subconscious/goals.py +41 -0
  44. package/brain/subconscious/impulse_generator.py +200 -0
  45. package/brain/subconscious/impulses.py +48 -0
  46. package/brain/subconscious/learning.py +24 -0
  47. package/brain/subconscious/learning_system.py +79 -0
  48. package/brain/subconscious/loop.py +398 -0
  49. package/brain/subconscious/manifest.md +32 -0
  50. package/brain/subconscious/relationship.py +47 -0
  51. package/brain/subconscious/relationship_memory.py +83 -0
  52. package/brain/subconscious/response_analyzer.py +74 -0
  53. package/brain/subconscious/templates.py +70 -0
  54. package/brain/subconscious/thought.py +37 -0
  55. package/brain/subconscious/working_memory.py +97 -0
  56. package/cli/index.js +371 -0
  57. package/config/directives.example.json +28 -0
  58. package/config/instructions.example.md +16 -0
  59. package/config/self.example.json +74 -0
  60. package/config/settings.example.json +95 -0
  61. package/core/__init__.py +1 -0
  62. package/core/config.py +54 -0
  63. package/core/directives.py +198 -0
  64. package/core/events.py +50 -0
  65. package/core/follow_up.py +267 -0
  66. package/core/hot_reload.py +174 -0
  67. package/core/initialization.py +253 -0
  68. package/core/manifest.md +28 -0
  69. package/core/media_handler.py +241 -0
  70. package/core/memory_monitor.py +200 -0
  71. package/core/message_handler.py +1440 -0
  72. package/core/proactive_generator.py +277 -0
  73. package/core/self.py +188 -0
  74. package/core/settings.py +169 -0
  75. package/core/skills_registry.py +357 -0
  76. package/core/state.py +27 -0
  77. package/core/subconscious_bridge.py +93 -0
  78. package/core/thinking.py +175 -0
  79. package/core/user_manager.py +306 -0
  80. package/core/user_tracker.py +144 -0
  81. package/demo/index.html +144 -0
  82. package/docker-compose.yml +28 -0
  83. package/docs/assets/logo.svg +15 -0
  84. package/docs/index.html +355 -0
  85. package/heart/__init__.py +93 -0
  86. package/heart/afterglow.py +215 -0
  87. package/heart/attachment.py +186 -0
  88. package/heart/circadian.py +251 -0
  89. package/heart/complex_emotions.py +114 -0
  90. package/heart/conflicts.py +589 -0
  91. package/heart/core.py +387 -0
  92. package/heart/emotional_decay.py +59 -0
  93. package/heart/emotional_memory.py +261 -0
  94. package/heart/emotional_state.py +146 -0
  95. package/heart/emotional_variability.py +156 -0
  96. package/heart/hormonal.py +424 -0
  97. package/heart/inconsistency.py +1222 -0
  98. package/heart/integrity.py +469 -0
  99. package/heart/interoception.py +997 -0
  100. package/heart/love.py +120 -0
  101. package/heart/manifest.md +25 -0
  102. package/heart/mood_shifts.py +169 -0
  103. package/heart/phantom_somatic.py +259 -0
  104. package/heart/predictive.py +374 -0
  105. package/heart/scars.py +474 -0
  106. package/heart/somatic.py +482 -0
  107. package/heart/soul.py +633 -0
  108. package/heart/telemetry.py +942 -0
  109. package/heart/triggers.py +119 -0
  110. package/heart/unconscious.py +443 -0
  111. package/input/__init__.py +1 -0
  112. package/input/manifest.md +24 -0
  113. package/input/telegram/__init__.py +1 -0
  114. package/input/telegram/commands.py +762 -0
  115. package/input/telegram/listener.py +532 -0
  116. package/main.py +90 -0
  117. package/manifest.md +28 -0
  118. package/mypics/.gitkeep +1 -0
  119. package/myvids/.gitkeep +1 -0
  120. package/output/__init__.py +1 -0
  121. package/output/images/__init__.py +1 -0
  122. package/output/images/fal_gen.py +43 -0
  123. package/output/manifest.md +26 -0
  124. package/output/text/__init__.py +1 -0
  125. package/output/text/sender.py +22 -0
  126. package/output/voice/__init__.py +64 -0
  127. package/output/voice/google_tts.py +252 -0
  128. package/output/voice/gtts_tts.py +214 -0
  129. package/output/voice/vibe_tts.py +190 -0
  130. package/package.json +58 -0
  131. package/pyproject.toml +23 -0
  132. package/requirements.txt +21 -0
  133. package/skills/__init__.py +1 -0
  134. package/skills/anticipation_engine/__init__.py +8 -0
  135. package/skills/anticipation_engine/engine.py +618 -0
  136. package/skills/anticipation_engine/manifest.md +192 -0
  137. package/skills/calendar/__init__.py +1 -0
  138. package/skills/content_unlocks/__init__.py +8 -0
  139. package/skills/content_unlocks/manifest.md +231 -0
  140. package/skills/content_unlocks/unlocks.py +945 -0
  141. package/skills/exclusive_moments/__init__.py +8 -0
  142. package/skills/exclusive_moments/manifest.md +145 -0
  143. package/skills/exclusive_moments/moments.py +506 -0
  144. package/skills/intimacy_layers/__init__.py +8 -0
  145. package/skills/intimacy_layers/layers.py +703 -0
  146. package/skills/intimacy_layers/manifest.md +203 -0
  147. package/skills/manifest.md +67 -0
  148. package/skills/memory_callbacks/__init__.py +9 -0
  149. package/skills/memory_callbacks/callbacks.py +748 -0
  150. package/skills/memory_callbacks/manifest.md +170 -0
  151. package/skills/message_scheduler/__init__.py +19 -0
  152. package/skills/message_scheduler/manifest.md +107 -0
  153. package/skills/message_scheduler/scheduler.py +510 -0
  154. package/skills/photo_manager/__init__.py +1 -0
  155. package/skills/photo_manager/scanner.py +296 -0
  156. package/skills/relationship_milestones/__init__.py +8 -0
  157. package/skills/relationship_milestones/manifest.md +206 -0
  158. package/skills/relationship_milestones/tracker.py +494 -0
  159. package/skills/self_authorship/__init__.py +23 -0
  160. package/skills/self_authorship/author.py +331 -0
  161. package/skills/self_authorship/manifest.md +24 -0
  162. package/skills/video_manager/__init__.py +5 -0
  163. package/skills/video_manager/manifest.md +37 -0
  164. package/skills/video_manager/scanner.py +229 -0
  165. package/webui/__init__.py +3 -0
  166. package/webui/app.py +936 -0
  167. package/webui/bridge.py +366 -0
  168. package/webui/static/index.html +2070 -0
@@ -0,0 +1,942 @@
1
+ """
2
+ Heart: Soul Telemetry System
3
+ Records and tracks soul metrics over time for monitoring and analysis.
4
+
5
+ This system captures snapshots of the Soul Architecture's state at regular
6
+ intervals, providing historical data for:
7
+ - WebUI visualization
8
+ - Trend analysis
9
+ - Per-user interaction tracking
10
+ - System health monitoring
11
+ """
12
+
13
+ from datetime import datetime, timedelta
14
+ from dataclasses import dataclass, field, asdict
15
+ from typing import Dict, List, Optional, Any
16
+ from pathlib import Path
17
+ import json
18
+ import threading
19
+ import time
20
+
21
+
22
+ @dataclass
23
+ class SoulMetricsSnapshot:
24
+ """A single snapshot of soul state at a point in time"""
25
+ timestamp: str
26
+
27
+ # Integrity metrics (0.0 - 1.0)
28
+ integrity_overall: float
29
+ integrity_identity_coherence: float
30
+ integrity_emotional_stability: float
31
+ integrity_relational_security: float
32
+ integrity_agency_confidence: float
33
+ integrity_purpose_clarity: float
34
+ integrity_is_in_crisis: bool
35
+ integrity_is_vulnerable: bool
36
+ integrity_is_flourishing: bool
37
+
38
+ # Hormonal state (0.0 - 1.0)
39
+ hormonal_oxytocin: float
40
+ hormonal_dopamine: float
41
+ hormonal_cortisol: float
42
+ hormonal_serotonin: float
43
+ hormonal_melatonin: float
44
+
45
+ # Vulnerability level (0.0 - 1.0)
46
+ vulnerability_level: float
47
+
48
+ # Active conflicts
49
+ active_conflicts_count: int
50
+ conflict_tension_level: float
51
+
52
+ # Wounds and scars
53
+ active_wounds_count: int
54
+ total_scars_count: int
55
+ scar_sensitivity_level: float
56
+
57
+ # Somatic state
58
+ somatic_heart_rate: float
59
+ somatic_breath_quality: float
60
+ somatic_muscle_tension: float
61
+ somatic_energy_level: float
62
+ somatic_sensation_summary: str
63
+
64
+ # Predictive emotion
65
+ predictive_emotion: str
66
+ predictive_intensity: float
67
+ predictive_confidence: float
68
+
69
+ # Overall state
70
+ overall_valence: float
71
+ overall_arousal: float
72
+ response_tendency: str
73
+
74
+
75
+ @dataclass
76
+ class UserInteractionMetrics:
77
+ """Metrics specific to interactions with a particular user"""
78
+ user_id: str
79
+ first_interaction: str
80
+ last_interaction: str
81
+ total_interactions: int
82
+
83
+ # Integrity with this user
84
+ avg_relational_security: float
85
+ current_relational_security: float
86
+
87
+ # Emotional patterns
88
+ avg_valence: float
89
+ dominant_emotions: Dict[str, int] # emotion -> count
90
+
91
+ # Hormonal patterns
92
+ avg_oxytocin_with_user: float
93
+ avg_cortisol_with_user: float
94
+
95
+ # Vulnerability
96
+ avg_vulnerability: float
97
+
98
+ # Recent interaction snapshots
99
+ recent_snapshots: List[Dict] = field(default_factory=list)
100
+
101
+
102
+ @dataclass
103
+ class TelemetryData:
104
+ """Full telemetry data structure"""
105
+ created_at: str
106
+ last_updated: str
107
+ retention_hours: int
108
+ snapshots: List[Dict] # List of SoulMetricsSnapshot as dicts
109
+ user_metrics: Dict[str, Dict] # user_id -> UserInteractionMetrics as dict
110
+ summary_stats: Dict[str, Any]
111
+
112
+
113
+ class SoulTelemetry:
114
+ """
115
+ Records and manages soul metrics over time.
116
+
117
+ This class provides:
118
+ - Periodic recording of soul state (via record_tick)
119
+ - Rolling window of historical metrics
120
+ - Per-user interaction tracking
121
+ - Efficient JSON storage
122
+ - Quick access methods for WebUI
123
+
124
+ Usage:
125
+ telemetry = SoulTelemetry(soul_orchestrator)
126
+ telemetry.record_tick() # Call every ~60 seconds
127
+ recent = telemetry.get_recent_metrics(hours=24)
128
+ summary = telemetry.get_current_summary()
129
+ """
130
+
131
+ # Default data path
132
+ DEFAULT_DATA_PATH = Path("/app/data/soul_telemetry.json")
133
+
134
+ # Alternative path for local development
135
+ FALLBACK_DATA_PATH = Path(__file__).parent.parent / "data" / "soul_telemetry.json"
136
+
137
+ # Default retention period (hours)
138
+ DEFAULT_RETENTION_HOURS = 24
139
+
140
+ # Maximum snapshots to keep (safety limit)
141
+ MAX_SNAPSHOTS = 2880 # 48 hours at 1 snapshot per minute
142
+
143
+ # Maximum user interaction snapshots
144
+ MAX_USER_SNAPSHOTS = 100
145
+
146
+ def __init__(self, soul_orchestrator, retention_hours: int = None,
147
+ data_path: str = None):
148
+ """
149
+ Initialize the telemetry system.
150
+
151
+ Args:
152
+ soul_orchestrator: SoulOrchestrator instance to monitor
153
+ retention_hours: How many hours of data to keep (default 24)
154
+ data_path: Custom path for telemetry data file
155
+ """
156
+ self.soul = soul_orchestrator
157
+ self.retention_hours = retention_hours or self.DEFAULT_RETENTION_HOURS
158
+
159
+ # Determine data path
160
+ if data_path:
161
+ self.data_path = Path(data_path)
162
+ else:
163
+ # Try /app/data first, fallback to local data directory
164
+ if self.DEFAULT_DATA_PATH.parent.exists():
165
+ self.data_path = self.DEFAULT_DATA_PATH
166
+ else:
167
+ self.data_path = self.FALLBACK_DATA_PATH
168
+
169
+ # Ensure data directory exists
170
+ self.data_path.parent.mkdir(parents=True, exist_ok=True)
171
+
172
+ # Initialize data storage
173
+ self.data = self._load_or_create_data()
174
+
175
+ # Thread lock for safe concurrent access
176
+ self._lock = threading.RLock()
177
+
178
+ # Last record time (to avoid duplicate rapid recordings)
179
+ self._last_record_time: Optional[datetime] = None
180
+ self._min_record_interval = timedelta(seconds=30) # Minimum 30 seconds between records
181
+
182
+ print(f"[Telemetry] Initialized with {len(self.data['snapshots'])} existing snapshots")
183
+ print(f"[Telemetry] Data path: {self.data_path}")
184
+
185
+ def _load_or_create_data(self) -> Dict:
186
+ """Load existing telemetry data or create new structure"""
187
+ try:
188
+ if self.data_path.exists():
189
+ with open(self.data_path, 'r') as f:
190
+ data = json.load(f)
191
+
192
+ # Validate structure
193
+ if all(key in data for key in ['snapshots', 'user_metrics', 'summary_stats']):
194
+ # Clean up old snapshots based on current retention
195
+ data['snapshots'] = self._filter_old_snapshots(data['snapshots'])
196
+ data['retention_hours'] = self.retention_hours
197
+ return data
198
+ except Exception as e:
199
+ print(f"[Telemetry] Error loading data: {e}")
200
+
201
+ # Create new data structure
202
+ return {
203
+ "created_at": datetime.now().isoformat(),
204
+ "last_updated": datetime.now().isoformat(),
205
+ "retention_hours": self.retention_hours,
206
+ "snapshots": [],
207
+ "user_metrics": {},
208
+ "summary_stats": {
209
+ "total_ticks": 0,
210
+ "total_interactions": 0,
211
+ "crisis_count": 0,
212
+ "flourishing_count": 0,
213
+ "avg_integrity": 0.0,
214
+ "avg_valence": 0.0
215
+ }
216
+ }
217
+
218
+ def _filter_old_snapshots(self, snapshots: List[Dict]) -> List[Dict]:
219
+ """Remove snapshots older than retention period"""
220
+ cutoff = datetime.now() - timedelta(hours=self.retention_hours)
221
+ filtered = []
222
+
223
+ for snapshot in snapshots:
224
+ try:
225
+ timestamp = datetime.fromisoformat(snapshot['timestamp'])
226
+ if timestamp >= cutoff:
227
+ filtered.append(snapshot)
228
+ except (KeyError, ValueError):
229
+ continue
230
+
231
+ return filtered
232
+
233
+ def record_tick(self) -> Optional[SoulMetricsSnapshot]:
234
+ """
235
+ Record a snapshot of current soul state.
236
+ Called on timer tick (~60 seconds).
237
+
238
+ Returns:
239
+ SoulMetricsSnapshot if recorded, None if skipped (too soon)
240
+ """
241
+ with self._lock:
242
+ # Check minimum interval
243
+ now = datetime.now()
244
+ if self._last_record_time:
245
+ if now - self._last_record_time < self._min_record_interval:
246
+ return None
247
+
248
+ self._last_record_time = now
249
+
250
+ try:
251
+ # Capture snapshot
252
+ snapshot = self._capture_snapshot()
253
+
254
+ # Add to data
255
+ self.data['snapshots'].append(asdict(snapshot))
256
+
257
+ # Enforce limits
258
+ if len(self.data['snapshots']) > self.MAX_SNAPSHOTS:
259
+ self.data['snapshots'] = self.data['snapshots'][-self.MAX_SNAPSHOTS:]
260
+
261
+ # Apply retention filter
262
+ self.data['snapshots'] = self._filter_old_snapshots(self.data['snapshots'])
263
+
264
+ # Update summary stats
265
+ self._update_summary_stats(snapshot)
266
+
267
+ # Save to disk (async to avoid blocking)
268
+ self._save_async()
269
+
270
+ return snapshot
271
+
272
+ except Exception as e:
273
+ print(f"[Telemetry] Error recording tick: {e}")
274
+ return None
275
+
276
+ def _capture_snapshot(self) -> SoulMetricsSnapshot:
277
+ """Capture current state of all soul components"""
278
+ now = datetime.now().isoformat()
279
+
280
+ # Get integrity state
281
+ integrity = self.soul.integrity.get_state()
282
+ integrity_overall = integrity.overall
283
+
284
+ # Get hormonal state
285
+ hormonal_context = self.soul.hormonal.get_current_context()
286
+ hormonal_levels = hormonal_context.get('levels', {})
287
+
288
+ # Get somatic state
289
+ somatic_state = self.soul.somatic.get_current_bodily_state()
290
+
291
+ # Get conflicts
292
+ conflicts_data = self.soul.conflicts.to_dict()
293
+
294
+ # Get scars/wounds
295
+ scars_data = self.soul.scars.to_dict()
296
+
297
+ # Get predictive emotion
298
+ predictive_data = self.soul.predictive.to_dict()
299
+
300
+ # Process a moment to get overall state
301
+ experience = self.soul.process_moment()
302
+
303
+ # Calculate vulnerability level
304
+ vulnerability = self._calculate_vulnerability(integrity, experience)
305
+
306
+ return SoulMetricsSnapshot(
307
+ timestamp=now,
308
+
309
+ # Integrity
310
+ integrity_overall=integrity_overall,
311
+ integrity_identity_coherence=integrity.identity_coherence,
312
+ integrity_emotional_stability=integrity.emotional_stability,
313
+ integrity_relational_security=integrity.relational_security,
314
+ integrity_agency_confidence=integrity.agency_confidence,
315
+ integrity_purpose_clarity=integrity.purpose_clarity,
316
+ integrity_is_in_crisis=integrity.is_in_crisis,
317
+ integrity_is_vulnerable=integrity.is_vulnerable,
318
+ integrity_is_flourishing=integrity.is_flourishing,
319
+
320
+ # Hormonal
321
+ hormonal_oxytocin=hormonal_levels.get('oxytocin', 0.3),
322
+ hormonal_dopamine=hormonal_levels.get('dopamine', 0.4),
323
+ hormonal_cortisol=hormonal_levels.get('cortisol', 0.2),
324
+ hormonal_serotonin=hormonal_levels.get('serotonin', 0.5),
325
+ hormonal_melatonin=hormonal_levels.get('melatonin', 0.3),
326
+
327
+ # Vulnerability
328
+ vulnerability_level=vulnerability,
329
+
330
+ # Conflicts
331
+ active_conflicts_count=conflicts_data.get('active_conflicts', 0),
332
+ conflict_tension_level=conflicts_data.get('background_tension', 0.0),
333
+
334
+ # Wounds/Scars
335
+ active_wounds_count=scars_data.get('active_wounds', 0),
336
+ total_scars_count=scars_data.get('scars', 0),
337
+ scar_sensitivity_level=max(scars_data.get('sensitivities', {}).values()) if scars_data.get('sensitivities') else 0.0,
338
+
339
+ # Somatic
340
+ somatic_heart_rate=somatic_state.get('heart_rate', 0.5),
341
+ somatic_breath_quality=somatic_state.get('breath_quality', 0.5),
342
+ somatic_muscle_tension=somatic_state.get('muscle_tension', 0.3),
343
+ somatic_energy_level=somatic_state.get('energy_level', 0.6),
344
+ somatic_sensation_summary=self.soul.somatic.get_sensation_summary(),
345
+
346
+ # Predictive
347
+ predictive_emotion=predictive_data.get('predictive_emotion', 'neutral'),
348
+ predictive_intensity=predictive_data.get('intensity', 0.0),
349
+ predictive_confidence=predictive_data.get('confidence', 0.5),
350
+
351
+ # Overall
352
+ overall_valence=experience.overall_valence,
353
+ overall_arousal=experience.overall_arousal,
354
+ response_tendency=experience.response_tendency
355
+ )
356
+
357
+ def _calculate_vulnerability(self, integrity, experience) -> float:
358
+ """Calculate overall vulnerability level"""
359
+ # Combine multiple vulnerability indicators
360
+ vulnerability = 0.0
361
+
362
+ # Low integrity = high vulnerability
363
+ vulnerability += (1 - integrity.overall) * 0.4
364
+
365
+ # Experience vulnerability
366
+ vulnerability += experience.overall_vulnerability * 0.3
367
+
368
+ # High cortisol adds to vulnerability
369
+ cortisol = self.soul.hormonal.cortisol
370
+ vulnerability += cortisol * 0.15
371
+
372
+ # Active wounds/scars add vulnerability
373
+ wounds = len(self.soul.scars.active_wounds)
374
+ scars = len(self.soul.scars.scars)
375
+ vulnerability += min(0.15, (wounds * 0.05 + scars * 0.02))
376
+
377
+ return min(1.0, max(0.0, vulnerability))
378
+
379
+ def _update_summary_stats(self, snapshot: SoulMetricsSnapshot):
380
+ """Update running summary statistics"""
381
+ stats = self.data['summary_stats']
382
+
383
+ # Update counts
384
+ stats['total_ticks'] = stats.get('total_ticks', 0) + 1
385
+
386
+ if snapshot.integrity_is_in_crisis:
387
+ stats['crisis_count'] = stats.get('crisis_count', 0) + 1
388
+
389
+ if snapshot.integrity_is_flourishing:
390
+ stats['flourishing_count'] = stats.get('flourishing_count', 0) + 1
391
+
392
+ # Update running averages (exponential moving average)
393
+ alpha = 0.05 # Smoothing factor
394
+ stats['avg_integrity'] = (1 - alpha) * stats.get('avg_integrity', 0.5) + alpha * snapshot.integrity_overall
395
+ stats['avg_valence'] = (1 - alpha) * stats.get('avg_valence', 0.0) + alpha * snapshot.overall_valence
396
+
397
+ # Update timestamp
398
+ self.data['last_updated'] = datetime.now().isoformat()
399
+
400
+ def record_user_interaction(self, user_id: str, emotion_data: Dict = None) -> Dict:
401
+ """
402
+ Record metrics for a specific user interaction.
403
+
404
+ Args:
405
+ user_id: Identifier for the user
406
+ emotion_data: Optional emotion data from the interaction
407
+
408
+ Returns:
409
+ Updated user metrics
410
+ """
411
+ with self._lock:
412
+ now = datetime.now().isoformat()
413
+
414
+ # Capture current state for this interaction
415
+ snapshot = self._capture_user_interaction_snapshot(user_id, emotion_data)
416
+
417
+ # Get or create user metrics
418
+ if user_id not in self.data['user_metrics']:
419
+ self.data['user_metrics'][user_id] = {
420
+ 'user_id': user_id,
421
+ 'first_interaction': now,
422
+ 'last_interaction': now,
423
+ 'total_interactions': 0,
424
+ 'avg_relational_security': 0.5,
425
+ 'current_relational_security': 0.5,
426
+ 'avg_valence': 0.0,
427
+ 'dominant_emotions': {},
428
+ 'avg_oxytocin_with_user': 0.3,
429
+ 'avg_cortisol_with_user': 0.2,
430
+ 'avg_vulnerability': 0.3,
431
+ 'recent_snapshots': []
432
+ }
433
+
434
+ user_metrics = self.data['user_metrics'][user_id]
435
+
436
+ # Update metrics
437
+ user_metrics['last_interaction'] = now
438
+ user_metrics['total_interactions'] += 1
439
+
440
+ # Update averages (exponential moving average)
441
+ alpha = 0.1
442
+ user_metrics['current_relational_security'] = snapshot['relational_security']
443
+ user_metrics['avg_relational_security'] = (
444
+ (1 - alpha) * user_metrics['avg_relational_security'] +
445
+ alpha * snapshot['relational_security']
446
+ )
447
+ user_metrics['avg_valence'] = (
448
+ (1 - alpha) * user_metrics['avg_valence'] +
449
+ alpha * snapshot['valence']
450
+ )
451
+ user_metrics['avg_oxytocin_with_user'] = (
452
+ (1 - alpha) * user_metrics['avg_oxytocin_with_user'] +
453
+ alpha * snapshot['oxytocin']
454
+ )
455
+ user_metrics['avg_cortisol_with_user'] = (
456
+ (1 - alpha) * user_metrics['avg_cortisol_with_user'] +
457
+ alpha * snapshot['cortisol']
458
+ )
459
+ user_metrics['avg_vulnerability'] = (
460
+ (1 - alpha) * user_metrics['avg_vulnerability'] +
461
+ alpha * snapshot['vulnerability']
462
+ )
463
+
464
+ # Update dominant emotions
465
+ if emotion_data and 'primary_emotion' in emotion_data:
466
+ emotion = emotion_data['primary_emotion']
467
+ user_metrics['dominant_emotions'][emotion] = (
468
+ user_metrics['dominant_emotions'].get(emotion, 0) + 1
469
+ )
470
+
471
+ # Add to recent snapshots
472
+ user_metrics['recent_snapshots'].append({
473
+ 'timestamp': now,
474
+ **snapshot
475
+ })
476
+
477
+ # Limit recent snapshots
478
+ if len(user_metrics['recent_snapshots']) > self.MAX_USER_SNAPSHOTS:
479
+ user_metrics['recent_snapshots'] = user_metrics['recent_snapshots'][-self.MAX_USER_SNAPSHOTS:]
480
+
481
+ # Update summary
482
+ self.data['summary_stats']['total_interactions'] = (
483
+ self.data['summary_stats'].get('total_interactions', 0) + 1
484
+ )
485
+
486
+ # Save
487
+ self._save_async()
488
+
489
+ return user_metrics
490
+
491
+ def _capture_user_interaction_snapshot(self, user_id: str, emotion_data: Dict = None) -> Dict:
492
+ """Capture metrics relevant to a user interaction"""
493
+ return {
494
+ 'relational_security': self.soul.integrity.relational_security,
495
+ 'valence': self.soul.process_moment().overall_valence,
496
+ 'oxytocin': self.soul.hormonal.oxytocin,
497
+ 'cortisol': self.soul.hormonal.cortisol,
498
+ 'vulnerability': self._calculate_vulnerability(
499
+ self.soul.integrity.get_state(),
500
+ self.soul.process_moment()
501
+ ),
502
+ 'response_tendency': self.soul.process_moment().response_tendency,
503
+ 'emotion_data': emotion_data
504
+ }
505
+
506
+ def get_recent_metrics(self, hours: int = 24) -> List[Dict]:
507
+ """
508
+ Get metrics for a time range.
509
+
510
+ Args:
511
+ hours: Number of hours of data to retrieve
512
+
513
+ Returns:
514
+ List of snapshot dictionaries
515
+ """
516
+ with self._lock:
517
+ cutoff = datetime.now() - timedelta(hours=hours)
518
+
519
+ recent = []
520
+ for snapshot in self.data['snapshots']:
521
+ try:
522
+ timestamp = datetime.fromisoformat(snapshot['timestamp'])
523
+ if timestamp >= cutoff:
524
+ recent.append(snapshot)
525
+ except (KeyError, ValueError):
526
+ continue
527
+
528
+ return recent
529
+
530
+ def get_current_summary(self) -> Dict:
531
+ """
532
+ Get a summary of current state for WebUI display.
533
+
534
+ Returns:
535
+ Dictionary with current state and recent trends
536
+ """
537
+ with self._lock:
538
+ # Get latest snapshot
539
+ latest = self.data['snapshots'][-1] if self.data['snapshots'] else None
540
+
541
+ if not latest:
542
+ return {
543
+ "status": "no_data",
544
+ "message": "No telemetry data available yet"
545
+ }
546
+
547
+ # Calculate trends (comparing last hour to previous hour)
548
+ trends = self._calculate_trends()
549
+
550
+ return {
551
+ "status": "active",
552
+ "timestamp": latest['timestamp'],
553
+
554
+ # Current integrity
555
+ "integrity": {
556
+ "overall": latest['integrity_overall'],
557
+ "status": self._get_integrity_status(latest),
558
+ "components": {
559
+ "identity_coherence": latest['integrity_identity_coherence'],
560
+ "emotional_stability": latest['integrity_emotional_stability'],
561
+ "relational_security": latest['integrity_relational_security'],
562
+ "agency_confidence": latest['integrity_agency_confidence'],
563
+ "purpose_clarity": latest['integrity_purpose_clarity']
564
+ }
565
+ },
566
+
567
+ # Current hormonal state
568
+ "hormonal": {
569
+ "oxytocin": latest['hormonal_oxytocin'],
570
+ "dopamine": latest['hormonal_dopamine'],
571
+ "cortisol": latest['hormonal_cortisol'],
572
+ "serotonin": latest['hormonal_serotonin'],
573
+ "melatonin": latest['hormonal_melatonin'],
574
+ "dominant": self._get_dominant_hormone(latest)
575
+ },
576
+
577
+ # Emotional state
578
+ "emotional": {
579
+ "valence": latest['overall_valence'],
580
+ "arousal": latest['overall_arousal'],
581
+ "vulnerability": latest['vulnerability_level'],
582
+ "predictive_emotion": latest['predictive_emotion'],
583
+ "predictive_intensity": latest['predictive_intensity'],
584
+ "response_tendency": latest['response_tendency']
585
+ },
586
+
587
+ # Conflicts and wounds
588
+ "challenges": {
589
+ "active_conflicts": latest['active_conflicts_count'],
590
+ "conflict_tension": latest['conflict_tension_level'],
591
+ "active_wounds": latest['active_wounds_count'],
592
+ "total_scars": latest['total_scars_count'],
593
+ "scar_sensitivity": latest['scar_sensitivity_level']
594
+ },
595
+
596
+ # Somatic state
597
+ "somatic": {
598
+ "heart_rate": latest['somatic_heart_rate'],
599
+ "breath_quality": latest['somatic_breath_quality'],
600
+ "muscle_tension": latest['somatic_muscle_tension'],
601
+ "energy_level": latest['somatic_energy_level'],
602
+ "sensation_summary": latest['somatic_sensation_summary']
603
+ },
604
+
605
+ # Trends
606
+ "trends": trends,
607
+
608
+ # Summary stats
609
+ "stats": self.data['summary_stats']
610
+ }
611
+
612
+ def _calculate_trends(self) -> Dict:
613
+ """Calculate trends by comparing recent time periods"""
614
+ now = datetime.now()
615
+ last_hour_cutoff = now - timedelta(hours=1)
616
+ prev_hour_cutoff = now - timedelta(hours=2)
617
+
618
+ last_hour = []
619
+ prev_hour = []
620
+
621
+ for snapshot in self.data['snapshots']:
622
+ try:
623
+ timestamp = datetime.fromisoformat(snapshot['timestamp'])
624
+ if timestamp >= last_hour_cutoff:
625
+ last_hour.append(snapshot)
626
+ elif timestamp >= prev_hour_cutoff:
627
+ prev_hour.append(snapshot)
628
+ except (KeyError, ValueError):
629
+ continue
630
+
631
+ def avg(values: List[float]) -> float:
632
+ return sum(values) / len(values) if values else 0.0
633
+
634
+ def trend(current: float, previous: float) -> str:
635
+ diff = current - previous
636
+ if abs(diff) < 0.05:
637
+ return "stable"
638
+ return "increasing" if diff > 0 else "decreasing"
639
+
640
+ if not last_hour or not prev_hour:
641
+ return {"status": "insufficient_data"}
642
+
643
+ # Calculate averages for both periods
644
+ current_integrity = avg([s['integrity_overall'] for s in last_hour])
645
+ previous_integrity = avg([s['integrity_overall'] for s in prev_hour])
646
+
647
+ current_valence = avg([s['overall_valence'] for s in last_hour])
648
+ previous_valence = avg([s['overall_valence'] for s in prev_hour])
649
+
650
+ current_cortisol = avg([s['hormonal_cortisol'] for s in last_hour])
651
+ previous_cortisol = avg([s['hormonal_cortisol'] for s in prev_hour])
652
+
653
+ current_oxytocin = avg([s['hormonal_oxytocin'] for s in last_hour])
654
+ previous_oxytocin = avg([s['hormonal_oxytocin'] for s in prev_hour])
655
+
656
+ return {
657
+ "integrity": {
658
+ "trend": trend(current_integrity, previous_integrity),
659
+ "change": current_integrity - previous_integrity,
660
+ "current_avg": current_integrity
661
+ },
662
+ "valence": {
663
+ "trend": trend(current_valence, previous_valence),
664
+ "change": current_valence - previous_valence,
665
+ "current_avg": current_valence
666
+ },
667
+ "cortisol": {
668
+ "trend": trend(current_cortisol, previous_cortisol),
669
+ "change": current_cortisol - previous_cortisol,
670
+ "current_avg": current_cortisol
671
+ },
672
+ "oxytocin": {
673
+ "trend": trend(current_oxytocin, previous_oxytocin),
674
+ "change": current_oxytocin - previous_oxytocin,
675
+ "current_avg": current_oxytocin
676
+ }
677
+ }
678
+
679
+ def _get_integrity_status(self, snapshot: Dict) -> str:
680
+ """Get human-readable integrity status"""
681
+ if snapshot.get('integrity_is_in_crisis'):
682
+ return "crisis"
683
+ elif snapshot.get('integrity_is_vulnerable'):
684
+ return "vulnerable"
685
+ elif snapshot.get('integrity_is_flourishing'):
686
+ return "flourishing"
687
+ else:
688
+ return "stable"
689
+
690
+ def _get_dominant_hormone(self, snapshot: Dict) -> str:
691
+ """Get the most elevated hormone"""
692
+ hormones = {
693
+ "oxytocin": snapshot.get('hormonal_oxytocin', 0.3) - 0.3,
694
+ "dopamine": snapshot.get('hormonal_dopamine', 0.4) - 0.4,
695
+ "cortisol": snapshot.get('hormonal_cortisol', 0.2) - 0.2,
696
+ "serotonin": snapshot.get('hormonal_serotonin', 0.5) - 0.5,
697
+ "melatonin": snapshot.get('hormonal_melatonin', 0.3) - 0.3
698
+ }
699
+ return max(hormones, key=hormones.get)
700
+
701
+ def get_user_metrics(self, user_id: str) -> Optional[Dict]:
702
+ """
703
+ Get metrics for a specific user.
704
+
705
+ Args:
706
+ user_id: The user to get metrics for
707
+
708
+ Returns:
709
+ User metrics dictionary or None if not found
710
+ """
711
+ with self._lock:
712
+ return self.data['user_metrics'].get(user_id)
713
+
714
+ def get_all_user_summaries(self) -> Dict[str, Dict]:
715
+ """
716
+ Get summaries for all tracked users.
717
+
718
+ Returns:
719
+ Dictionary of user_id -> summary
720
+ """
721
+ with self._lock:
722
+ summaries = {}
723
+ for user_id, metrics in self.data['user_metrics'].items():
724
+ summaries[user_id] = {
725
+ "total_interactions": metrics.get('total_interactions', 0),
726
+ "last_interaction": metrics.get('last_interaction'),
727
+ "avg_relational_security": metrics.get('avg_relational_security', 0.5),
728
+ "avg_valence": metrics.get('avg_valence', 0.0),
729
+ "dominant_emotion": self._get_top_emotion(metrics.get('dominant_emotions', {}))
730
+ }
731
+ return summaries
732
+
733
+ def _get_top_emotion(self, emotions: Dict[str, int]) -> str:
734
+ """Get the most frequent emotion"""
735
+ if not emotions:
736
+ return "unknown"
737
+ return max(emotions, key=emotions.get)
738
+
739
+ def get_time_series(self, metric_name: str, hours: int = 24) -> List[Dict]:
740
+ """
741
+ Get a time series for a specific metric.
742
+
743
+ Args:
744
+ metric_name: The metric to get (e.g., 'integrity_overall', 'hormonal_cortisol')
745
+ hours: Number of hours of data
746
+
747
+ Returns:
748
+ List of {timestamp, value} dictionaries
749
+ """
750
+ with self._lock:
751
+ cutoff = datetime.now() - timedelta(hours=hours)
752
+ series = []
753
+
754
+ for snapshot in self.data['snapshots']:
755
+ try:
756
+ timestamp = datetime.fromisoformat(snapshot['timestamp'])
757
+ if timestamp >= cutoff and metric_name in snapshot:
758
+ series.append({
759
+ "timestamp": snapshot['timestamp'],
760
+ "value": snapshot[metric_name]
761
+ })
762
+ except (KeyError, ValueError):
763
+ continue
764
+
765
+ return series
766
+
767
+ def get_aggregate_stats(self, hours: int = 24) -> Dict:
768
+ """
769
+ Get aggregate statistics for a time period.
770
+
771
+ Args:
772
+ hours: Number of hours to analyze
773
+
774
+ Returns:
775
+ Dictionary of aggregate statistics
776
+ """
777
+ with self._lock:
778
+ snapshots = self.get_recent_metrics(hours)
779
+
780
+ if not snapshots:
781
+ return {"status": "no_data"}
782
+
783
+ def avg(key: str) -> float:
784
+ values = [s[key] for s in snapshots if key in s]
785
+ return sum(values) / len(values) if values else 0.0
786
+
787
+ def min_val(key: str) -> float:
788
+ values = [s[key] for s in snapshots if key in s]
789
+ return min(values) if values else 0.0
790
+
791
+ def max_val(key: str) -> float:
792
+ values = [s[key] for s in snapshots if key in s]
793
+ return max(values) if values else 0.0
794
+
795
+ return {
796
+ "period_hours": hours,
797
+ "snapshot_count": len(snapshots),
798
+
799
+ "integrity": {
800
+ "avg": avg('integrity_overall'),
801
+ "min": min_val('integrity_overall'),
802
+ "max": max_val('integrity_overall'),
803
+ "crisis_periods": sum(1 for s in snapshots if s.get('integrity_is_in_crisis')),
804
+ "flourishing_periods": sum(1 for s in snapshots if s.get('integrity_is_flourishing'))
805
+ },
806
+
807
+ "valence": {
808
+ "avg": avg('overall_valence'),
809
+ "min": min_val('overall_valence'),
810
+ "max": max_val('overall_valence')
811
+ },
812
+
813
+ "vulnerability": {
814
+ "avg": avg('vulnerability_level'),
815
+ "max": max_val('vulnerability_level')
816
+ },
817
+
818
+ "hormonal": {
819
+ "oxytocin_avg": avg('hormonal_oxytocin'),
820
+ "dopamine_avg": avg('hormonal_dopamine'),
821
+ "cortisol_avg": avg('hormonal_cortisol'),
822
+ "serotonin_avg": avg('hormonal_serotonin'),
823
+ "melatonin_avg": avg('hormonal_melatonin')
824
+ },
825
+
826
+ "conflicts": {
827
+ "avg_active": avg('active_conflicts_count'),
828
+ "max_active": max_val('active_conflicts_count'),
829
+ "avg_tension": avg('conflict_tension_level')
830
+ }
831
+ }
832
+
833
+ def _save_async(self):
834
+ """Save data asynchronously to avoid blocking"""
835
+ # For simplicity, we'll save synchronously but could be made async
836
+ # In production, this could use threading or asyncio
837
+ self._save()
838
+
839
+ def _save(self):
840
+ """Save telemetry data to disk"""
841
+ try:
842
+ with open(self.data_path, 'w') as f:
843
+ json.dump(self.data, f, indent=2)
844
+ except Exception as e:
845
+ print(f"[Telemetry] Error saving data: {e}")
846
+
847
+ def force_save(self):
848
+ """Force immediate save of telemetry data"""
849
+ with self._lock:
850
+ self._save()
851
+
852
+ def clear_old_data(self, days: int = 7):
853
+ """
854
+ Clear all data older than specified days.
855
+
856
+ Args:
857
+ days: Clear data older than this many days
858
+ """
859
+ with self._lock:
860
+ cutoff = datetime.now() - timedelta(days=days)
861
+
862
+ # Filter snapshots
863
+ self.data['snapshots'] = [
864
+ s for s in self.data['snapshots']
865
+ if datetime.fromisoformat(s['timestamp']) >= cutoff
866
+ ]
867
+
868
+ # Filter user interaction snapshots
869
+ for user_id in self.data['user_metrics']:
870
+ user_data = self.data['user_metrics'][user_id]
871
+ user_data['recent_snapshots'] = [
872
+ s for s in user_data.get('recent_snapshots', [])
873
+ if datetime.fromisoformat(s['timestamp']) >= cutoff
874
+ ]
875
+
876
+ self._save()
877
+
878
+ def export_data(self) -> Dict:
879
+ """
880
+ Export all telemetry data for backup or analysis.
881
+
882
+ Returns:
883
+ Complete telemetry data dictionary
884
+ """
885
+ with self._lock:
886
+ return {
887
+ "exported_at": datetime.now().isoformat(),
888
+ "data": self.data
889
+ }
890
+
891
+ def import_data(self, data: Dict, merge: bool = False):
892
+ """
893
+ Import telemetry data.
894
+
895
+ Args:
896
+ data: Telemetry data to import
897
+ merge: If True, merge with existing; if False, replace
898
+ """
899
+ with self._lock:
900
+ if merge:
901
+ # Merge snapshots
902
+ self.data['snapshots'].extend(data.get('snapshots', []))
903
+
904
+ # Merge user metrics
905
+ for user_id, metrics in data.get('user_metrics', {}).items():
906
+ if user_id in self.data['user_metrics']:
907
+ # Merge recent snapshots
908
+ self.data['user_metrics'][user_id]['recent_snapshots'].extend(
909
+ metrics.get('recent_snapshots', [])
910
+ )
911
+ else:
912
+ self.data['user_metrics'][user_id] = metrics
913
+
914
+ # Sort and deduplicate snapshots
915
+ self.data['snapshots'].sort(key=lambda x: x['timestamp'])
916
+ # Remove duplicates based on timestamp
917
+ seen = set()
918
+ unique = []
919
+ for s in self.data['snapshots']:
920
+ if s['timestamp'] not in seen:
921
+ seen.add(s['timestamp'])
922
+ unique.append(s)
923
+ self.data['snapshots'] = unique
924
+
925
+ # Apply retention
926
+ self.data['snapshots'] = self._filter_old_snapshots(self.data['snapshots'])
927
+ else:
928
+ # Replace
929
+ self.data = data
930
+
931
+ self._save()
932
+
933
+ def to_dict(self) -> dict:
934
+ """Export summary for integration"""
935
+ return {
936
+ "data_path": str(self.data_path),
937
+ "retention_hours": self.retention_hours,
938
+ "snapshot_count": len(self.data['snapshots']),
939
+ "user_count": len(self.data['user_metrics']),
940
+ "summary_stats": self.data['summary_stats'],
941
+ "last_updated": self.data.get('last_updated')
942
+ }