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,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
|
+
}
|