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
package/webui/app.py ADDED
@@ -0,0 +1,936 @@
1
+ """
2
+ WebUI: FastAPI server with SSE for real-time Alive-AI dashboard
3
+ """
4
+
5
+ import asyncio
6
+ import json
7
+ import os
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Optional, Dict, List, Any
11
+ from collections import deque
12
+ from fastapi import FastAPI, Request
13
+ from fastapi.responses import HTMLResponse, StreamingResponse, FileResponse
14
+ from fastapi.staticfiles import StaticFiles
15
+ from sse_starlette.sse import EventSourceResponse
16
+
17
+
18
+ app = FastAPI(title="Alive-AI Dashboard")
19
+
20
+ # Track start time for uptime
21
+ _start_time = datetime.now()
22
+
23
+
24
+ def load_persistent_stats() -> dict:
25
+ """Load stats from actual data sources on startup"""
26
+ stats = {"messages": 0, "memories": 0, "evaluations": 0}
27
+
28
+ # Try different base paths
29
+ base_paths = [
30
+ Path("/app/data"),
31
+ Path(__file__).parent.parent / "data",
32
+ ]
33
+
34
+ # Count messages from conversation summaries (in users/*/summaries/)
35
+ for base_path in base_paths:
36
+ try:
37
+ # Look for summaries in users/*/summaries/
38
+ users_path = base_path / "users"
39
+ if users_path.exists():
40
+ count = 0
41
+ for user_dir in users_path.iterdir():
42
+ summaries_path = user_dir / "summaries"
43
+ if summaries_path.exists():
44
+ count += len(list(summaries_path.glob("*.json")))
45
+ if count > 0:
46
+ stats["messages"] = count
47
+ break
48
+ except Exception:
49
+ pass
50
+
51
+ # Count memories from vector store (Redis) or facts
52
+ for base_path in base_paths:
53
+ try:
54
+ total_facts = 0
55
+
56
+ # Check users/*/facts.json
57
+ users_path = base_path / "users"
58
+ if users_path.exists():
59
+ for user_dir in users_path.iterdir():
60
+ user_facts = user_dir / "facts.json"
61
+ if user_facts.exists():
62
+ data = json.loads(user_facts.read_text())
63
+ if isinstance(data, dict):
64
+ # Count all list values in the dict
65
+ for key, value in data.items():
66
+ if isinstance(value, list):
67
+ total_facts += len(value)
68
+
69
+ # Also check main facts.json
70
+ facts_path = base_path / "facts.json"
71
+ if facts_path.exists():
72
+ data = json.loads(facts_path.read_text())
73
+ if isinstance(data, dict):
74
+ for key, value in data.items():
75
+ if isinstance(value, list):
76
+ total_facts += len(value)
77
+
78
+ if total_facts > 0:
79
+ stats["memories"] = total_facts
80
+ break
81
+ except Exception:
82
+ pass
83
+
84
+ # Count evaluations from attachment state or telemetry
85
+ for base_path in base_paths:
86
+ try:
87
+ # Try attachment state for interaction count
88
+ attach_path = base_path / "attachment_state.json"
89
+ if attach_path.exists():
90
+ data = json.loads(attach_path.read_text())
91
+ stats["evaluations"] = data.get("interactions", 0)
92
+ break
93
+ # Try telemetry
94
+ telem_path = base_path / "soul_telemetry.json"
95
+ if telem_path.exists():
96
+ data = json.loads(telem_path.read_text())
97
+ if isinstance(data, list):
98
+ stats["evaluations"] = len(data)
99
+ break
100
+ except Exception:
101
+ pass
102
+
103
+ return stats
104
+
105
+ # Load persistent stats on startup
106
+ _persistent_stats = load_persistent_stats()
107
+
108
+ # Global state (updated by Alive-AI's nervous system)
109
+ alive_ai_state = {
110
+ "mood": "neutral",
111
+ "arousal": 0.3,
112
+ "desire": 0.0,
113
+ "love": 0.0,
114
+ "joy": 0.0,
115
+ "sadness": 0.0,
116
+ "trust": 0.5,
117
+ "fear": 0.1,
118
+ "anger": 0.0,
119
+ "boredom": 0.0,
120
+ "guilt": 0.0,
121
+ "pride": 0.0,
122
+ "jealousy": 0.0,
123
+ "embarrassment": 0.0,
124
+ "anticipation": 0.0,
125
+ "hope": 0.5,
126
+ "dread": 0.1,
127
+ "is_high_desire": False,
128
+ "is_in_love": False,
129
+ "current_thought": None,
130
+ "last_message": None,
131
+ "last_user_message": None,
132
+ "stats": _persistent_stats,
133
+ "conversation": [],
134
+ "recent_thoughts": [],
135
+ "updated_at": datetime.now().isoformat(),
136
+ "start_time": _start_time.isoformat(),
137
+ }
138
+
139
+ # Soul state (updated by Soul Architecture)
140
+ soul_state = {
141
+ "integrity": {
142
+ "overall": 0.65,
143
+ "identity_coherence": 0.7,
144
+ "emotional_stability": 0.7,
145
+ "relational_security": 0.6,
146
+ "agency_confidence": 0.65,
147
+ "purpose_clarity": 0.65,
148
+ "is_in_crisis": False,
149
+ "is_vulnerable": False,
150
+ "is_flourishing": False,
151
+ "status_description": "stable but not thriving"
152
+ },
153
+ "hormonal": {
154
+ "oxytocin": 0.3,
155
+ "dopamine": 0.4,
156
+ "serotonin": 0.5,
157
+ "cortisol": 0.2,
158
+ "melatonin": 0.3,
159
+ "state_description": "hormonally balanced",
160
+ "dominant_hormone": "serotonin"
161
+ },
162
+ "somatic": {
163
+ "heart_rate": 0.5,
164
+ "breath_quality": 0.5,
165
+ "muscle_tension": 0.3,
166
+ "stomach_state": 0.5,
167
+ "energy_level": 0.6,
168
+ "sensation_summary": "physically calm"
169
+ },
170
+ "conflicts": {
171
+ "active_conflicts": 0,
172
+ "background_tension": 0.0,
173
+ "tension_description": "feeling internally aligned",
174
+ "top_conflicts": []
175
+ },
176
+ "predictive": {
177
+ "predictive_emotion": "contentment",
178
+ "intensity": 0.2,
179
+ "description": "feeling content and stable",
180
+ "confidence": 0.5
181
+ },
182
+ "current_experience": {
183
+ "valence": 0.0,
184
+ "arousal": 0.3,
185
+ "vulnerability": 0.2,
186
+ "response_tendency": "neutral",
187
+ "description": "feeling mixed"
188
+ },
189
+ "active_user": None,
190
+ "user_context": {},
191
+ "updated_at": datetime.now().isoformat()
192
+ }
193
+
194
+ # Soul history for charts (keep last 100 entries)
195
+ soul_history: deque = deque(maxlen=100)
196
+
197
+ # Reference to Soul Orchestrator (set by bridge)
198
+ _soul_orchestrator = None
199
+
200
+ # Connected clients for SSE
201
+ clients = []
202
+
203
+ # Aliveness state - updated by bridge from various modules
204
+ aliveness_state = {
205
+ "interoceptive": {
206
+ "states": {},
207
+ "current_mood": "content",
208
+ "bodily_description": "feeling balanced and at ease",
209
+ "updated_at": datetime.now().isoformat()
210
+ },
211
+ "idle": {
212
+ "running": False,
213
+ "recent_thoughts": [],
214
+ "pending_initiations": 0,
215
+ "last_processing": None,
216
+ "updated_at": datetime.now().isoformat()
217
+ },
218
+ "bids": {
219
+ "last_bid_type": None,
220
+ "last_bid_intensity": None,
221
+ "recent_bids": [],
222
+ "updated_at": datetime.now().isoformat()
223
+ },
224
+ "memory": {
225
+ "total_memories": 0,
226
+ "average_weight": 0.0,
227
+ "high_emotion_count": 0,
228
+ "last_significant_memory": None,
229
+ "updated_at": datetime.now().isoformat()
230
+ },
231
+ "inconsistency": {
232
+ "active_conflicts": [],
233
+ "active_blind_spots": [],
234
+ "mood": {"state": "content"},
235
+ "behavioral_tendency": "neutral",
236
+ "updated_at": datetime.now().isoformat()
237
+ }
238
+ }
239
+
240
+
241
+ def update_state(data: dict):
242
+ """Called by nervous system to update state"""
243
+ global alive_ai_state
244
+ alive_ai_state.update(data)
245
+ alive_ai_state["updated_at"] = datetime.now().isoformat()
246
+ # Notify all connected clients
247
+ for client in clients:
248
+ client.set()
249
+
250
+
251
+ def add_conversation(role: str, content: str):
252
+ """Add a message to conversation history"""
253
+ alive_ai_state["conversation"].append({
254
+ "role": role,
255
+ "content": content,
256
+ "time": datetime.now().strftime("%H:%M:%S")
257
+ })
258
+ # Keep last 20 messages
259
+ alive_ai_state["conversation"] = alive_ai_state["conversation"][-20:]
260
+ if role == "user":
261
+ alive_ai_state["last_user_message"] = content
262
+ else:
263
+ alive_ai_state["last_message"] = content
264
+
265
+
266
+ def update_soul_state(data: dict):
267
+ """Update soul state from Soul Architecture"""
268
+ global soul_state
269
+ soul_state.update(data)
270
+ soul_state["updated_at"] = datetime.now().isoformat()
271
+
272
+ # Add to history for charts
273
+ history_entry = {
274
+ "timestamp": datetime.now().isoformat(),
275
+ "integrity_overall": data.get("integrity", {}).get("overall", 0.65),
276
+ "valence": data.get("current_experience", {}).get("valence", 0),
277
+ "arousal": data.get("current_experience", {}).get("arousal", 0.3),
278
+ "vulnerability": data.get("current_experience", {}).get("vulnerability", 0.2),
279
+ "oxytocin": data.get("hormonal", {}).get("oxytocin", 0.3),
280
+ "dopamine": data.get("hormonal", {}).get("dopamine", 0.4),
281
+ "cortisol": data.get("hormonal", {}).get("cortisol", 0.2),
282
+ "serotonin": data.get("hormonal", {}).get("serotonin", 0.5),
283
+ "background_tension": data.get("conflicts", {}).get("background_tension", 0)
284
+ }
285
+ soul_history.append(history_entry)
286
+
287
+ # Notify all connected clients
288
+ for client in clients:
289
+ client.set()
290
+
291
+
292
+ def set_soul_orchestrator(orchestrator):
293
+ """Set reference to Soul Orchestrator"""
294
+ global _soul_orchestrator
295
+ _soul_orchestrator = orchestrator
296
+ # Initial state load
297
+ if orchestrator:
298
+ try:
299
+ state = orchestrator.get_state_summary()
300
+ update_soul_state(state)
301
+ except Exception as e:
302
+ print(f"[WebUI] Error loading initial soul state: {e}")
303
+
304
+
305
+ async def event_generator(request: Request):
306
+ """SSE event generator"""
307
+ event = asyncio.Event()
308
+ clients.append(event)
309
+
310
+ try:
311
+ # Send initial state
312
+ yield {
313
+ "event": "state",
314
+ "data": json.dumps(alive_ai_state)
315
+ }
316
+
317
+ while True:
318
+ if await request.is_disconnected():
319
+ break
320
+ # Wait for update or timeout (keepalive every 30s)
321
+ try:
322
+ await asyncio.wait_for(event.wait(), timeout=30)
323
+ event.clear()
324
+ except asyncio.TimeoutError:
325
+ # Send keepalive
326
+ yield {"event": "ping", "data": "{}"}
327
+ continue
328
+
329
+ # Send updated state
330
+ yield {
331
+ "event": "state",
332
+ "data": json.dumps(alive_ai_state)
333
+ }
334
+ except asyncio.CancelledError:
335
+ pass # Client disconnected normally
336
+ except Exception as e:
337
+ print(f"[WebUI] SSE error: {e}")
338
+ finally:
339
+ if event in clients:
340
+ clients.remove(event)
341
+
342
+
343
+ @app.get("/", response_class=HTMLResponse)
344
+ async def dashboard():
345
+ """Serve the main dashboard HTML"""
346
+ html_path = Path(__file__).parent / "static" / "index.html"
347
+ return HTMLResponse(content=html_path.read_text())
348
+
349
+
350
+ @app.get("/events")
351
+ async def sse_events(request: Request):
352
+ """SSE endpoint for real-time updates"""
353
+ return EventSourceResponse(event_generator(request))
354
+
355
+
356
+ @app.get("/state")
357
+ async def get_state():
358
+ """Get current state (for polling fallback)"""
359
+ return alive_ai_state
360
+
361
+
362
+ @app.get("/avatar")
363
+ async def get_avatar():
364
+ """Serve a random avatar image"""
365
+ try:
366
+ pics_path = Path("/app/mypics/public")
367
+ if not pics_path.exists():
368
+ pics_path = Path(__file__).parent.parent / "mypics" / "public"
369
+
370
+ if pics_path.exists():
371
+ # Get all image files
372
+ images = list(pics_path.glob("*.jpg")) + list(pics_path.glob("*.jpeg")) + list(pics_path.glob("*.png"))
373
+ if images:
374
+ # Pick a nice one (prefer certain names)
375
+ for img in images:
376
+ name = img.name.lower()
377
+ if any(x in name for x in ["selfie", "face", "profile", "portrait"]):
378
+ return FileResponse(img, media_type="image/jpeg")
379
+ # Otherwise pick first
380
+ return FileResponse(images[0], media_type="image/jpeg")
381
+ except Exception as e:
382
+ print(f"[WebUI] Avatar error: {e}")
383
+
384
+ # Fallback placeholder (always returns something)
385
+ return HTMLResponse(content='<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><circle cx="50" cy="50" r="40" fill="#ccc"/></svg>', media_type="image/svg+xml")
386
+
387
+
388
+ @app.get("/health")
389
+ async def health():
390
+ return {"status": "ok"}
391
+
392
+
393
+ @app.get("/api/stats")
394
+ async def get_persistent_stats():
395
+ """Get stats refreshed from actual data sources"""
396
+ stats = load_persistent_stats()
397
+
398
+ # Update global state with fresh stats
399
+ alive_ai_state["stats"] = stats
400
+
401
+ # Add uptime info
402
+ uptime_seconds = (datetime.now() - _start_time).total_seconds()
403
+
404
+ return {
405
+ **stats,
406
+ "uptime_seconds": int(uptime_seconds),
407
+ "start_time": _start_time.isoformat()
408
+ }
409
+
410
+
411
+ @app.get("/api/memory")
412
+ async def get_memory_status():
413
+ """Get current memory usage and status"""
414
+ try:
415
+ from core.memory_monitor import get_memory_monitor
416
+ monitor = get_memory_monitor()
417
+ info = monitor.get_memory_info()
418
+
419
+ # Determine status color for frontend
420
+ usage_ratio = info["usage_of_limit"]
421
+ if usage_ratio >= 0.90:
422
+ status = "critical"
423
+ elif usage_ratio >= 0.75:
424
+ status = "warning"
425
+ else:
426
+ status = "ok"
427
+
428
+ return {
429
+ "status": status,
430
+ "process_gb": round(info["process_rss_gb"], 2),
431
+ "system_used_gb": round(info["system_used_gb"], 2),
432
+ "system_total_gb": round(info["system_total_gb"], 2),
433
+ "system_available_gb": round(info["system_available_gb"], 2),
434
+ "system_percent": round(info["system_percent"], 1),
435
+ "limit_gb": info["limit_gb"],
436
+ "usage_of_limit_percent": round(usage_ratio * 100, 1),
437
+ }
438
+ except Exception as e:
439
+ return {"status": "error", "error": str(e)}
440
+
441
+
442
+ @app.get("/thoughts")
443
+ async def get_thoughts():
444
+ """Get recent thoughts from subconscious"""
445
+ return {
446
+ "current_thought": alive_ai_state.get("current_thought"),
447
+ "recent_thoughts": alive_ai_state.get("recent_thoughts", [])
448
+ }
449
+
450
+
451
+ @app.get("/api/soul")
452
+ async def get_soul_state():
453
+ """Get current soul metrics from Soul Architecture"""
454
+ # If we have a live orchestrator, get fresh state
455
+ if _soul_orchestrator:
456
+ try:
457
+ fresh_state = _soul_orchestrator.get_state_summary()
458
+ update_soul_state(fresh_state)
459
+ except Exception as e:
460
+ print(f"[WebUI] Error getting soul state: {e}")
461
+ return soul_state
462
+
463
+
464
+ @app.get("/api/soul/history")
465
+ async def get_soul_history(limit: int = 50):
466
+ """Get historical soul metrics for charts"""
467
+ history_list = list(soul_history)
468
+ if limit > 0:
469
+ history_list = history_list[-limit:]
470
+ return {
471
+ "history": history_list,
472
+ "count": len(history_list)
473
+ }
474
+
475
+
476
+ @app.get("/api/soul/experience")
477
+ async def get_current_experience():
478
+ """Get the current emotional experience (processed moment)"""
479
+ if _soul_orchestrator:
480
+ try:
481
+ experience = _soul_orchestrator.process_moment()
482
+ return {
483
+ "timestamp": experience.timestamp,
484
+ "valence": experience.overall_valence,
485
+ "arousal": experience.overall_arousal,
486
+ "vulnerability": experience.overall_vulnerability,
487
+ "response_tendency": experience.response_tendency,
488
+ "description": experience.experience_description,
489
+ "somatic_sensation": experience.somatic_sensation,
490
+ "scar_active": experience.scar_activation is not None,
491
+ "conflict_count": len(experience.active_conflicts)
492
+ }
493
+ except Exception as e:
494
+ print(f"[WebUI] Error getting experience: {e}")
495
+ return soul_state.get("current_experience", {})
496
+
497
+
498
+ @app.get("/api/soul/conflicts")
499
+ async def get_active_conflicts():
500
+ """Get active internal conflicts from both Soul and Inconsistency Engine"""
501
+ result = {
502
+ "active_conflicts": [],
503
+ "count": 0,
504
+ "background_tension": 0.0,
505
+ "top_conflicts": [],
506
+ "active_desires": 0,
507
+ "ambivalences": 0,
508
+ "values_honored": 0,
509
+ "values_violated": 0,
510
+ "tension_description": "feeling internally aligned"
511
+ }
512
+
513
+ # Get Soul conflicts
514
+ if _soul_orchestrator and hasattr(_soul_orchestrator, 'conflicts'):
515
+ try:
516
+ soul_conflicts = _soul_orchestrator.conflicts.conflicts
517
+ result["active_conflicts"].extend([
518
+ {
519
+ "id": c.conflict_id,
520
+ "type": c.conflict_type.value if hasattr(c, 'conflict_type') else "soul",
521
+ "intensity": c.intensity.value if hasattr(c.intensity, 'value') else c.intensity,
522
+ "side_a": c.side_a,
523
+ "side_b": c.side_b,
524
+ "description": c.description,
525
+ "tension_level": c.tension_level,
526
+ "times_faced": c.times_faced
527
+ }
528
+ for c in soul_conflicts
529
+ ])
530
+ result["background_tension"] = _soul_orchestrator.conflicts.background_tension
531
+ result["active_desires"] = len(_soul_orchestrator.conflicts.desires)
532
+ result["ambivalences"] = len(_soul_orchestrator.conflicts.ambivalences)
533
+
534
+ # Count values honored/violated
535
+ for v in _soul_orchestrator.conflicts.values:
536
+ result["values_honored"] += v.times_honored
537
+ result["values_violated"] += v.times_violated
538
+ except Exception as e:
539
+ print(f"[WebUI] Error getting soul conflicts: {e}")
540
+
541
+ # Get Inconsistency Engine conflicts (these are the main ones!)
542
+ try:
543
+ from heart.inconsistency import get_inconsistency_engine
544
+ ie = get_inconsistency_engine()
545
+
546
+ for name, c in ie.active_conflicts.items():
547
+ result["active_conflicts"].append({
548
+ "id": name,
549
+ "type": "approach_avoidance",
550
+ "intensity": c.intensity,
551
+ "side_a": c.desire,
552
+ "side_b": c.fear,
553
+ "description": f"{c.desire} vs {c.fear}",
554
+ "tension_level": c.get_tension_level(),
555
+ "times_faced": c.times_faced,
556
+ "balance": c.current_balance
557
+ })
558
+
559
+ result["count"] = len(result["active_conflicts"])
560
+
561
+ # Get top conflicts by tension
562
+ sorted_conflicts = sorted(result["active_conflicts"], key=lambda x: x.get("tension_level", 0), reverse=True)
563
+ result["top_conflicts"] = sorted_conflicts[:3]
564
+
565
+ # Update tension description
566
+ if result["count"] > 0:
567
+ avg_tension = sum(c.get("tension_level", 0) for c in result["active_conflicts"]) / result["count"]
568
+ if avg_tension > 0.7:
569
+ result["tension_description"] = "feeling torn and conflicted"
570
+ elif avg_tension > 0.4:
571
+ result["tension_description"] = "feeling some internal tension"
572
+ else:
573
+ result["tension_description"] = "feeling mildly conflicted"
574
+
575
+ except Exception as e:
576
+ print(f"[WebUI] Error getting inconsistency conflicts: {e}")
577
+
578
+ return result
579
+
580
+
581
+ @app.get("/api/soul/somatic")
582
+ async def get_somatic_state():
583
+ """Get current somatic (bodily) sensations"""
584
+ if _soul_orchestrator and hasattr(_soul_orchestrator, 'somatic'):
585
+ try:
586
+ somatic = _soul_orchestrator.somatic
587
+ return {
588
+ "bodily_state": somatic.get_current_bodily_state(),
589
+ "sensation_summary": somatic.get_sensation_summary(),
590
+ "active_sensations": [
591
+ {
592
+ "region": s.region.value,
593
+ "quality": s.quality,
594
+ "intensity": s.intensity,
595
+ "emotion": s.associated_emotion
596
+ }
597
+ for s in somatic.active_sensations[-5:]
598
+ ]
599
+ }
600
+ except Exception as e:
601
+ print(f"[WebUI] Error getting somatic state: {e}")
602
+ return soul_state.get("somatic", {})
603
+
604
+
605
+ # ============================================================
606
+ # Aliveness API Endpoints
607
+ # ============================================================
608
+
609
+ def update_interoceptive_state(data: dict):
610
+ """Update interoceptive state from bridge"""
611
+ aliveness_state["interoceptive"].update(data)
612
+ aliveness_state["interoceptive"]["updated_at"] = datetime.now().isoformat()
613
+ # Notify clients
614
+ for client in clients:
615
+ client.set()
616
+
617
+
618
+ def update_idle_state(data: dict):
619
+ """Update idle/default mode state from bridge"""
620
+ aliveness_state["idle"].update(data)
621
+ aliveness_state["idle"]["updated_at"] = datetime.now().isoformat()
622
+ for client in clients:
623
+ client.set()
624
+
625
+
626
+ def update_bids_state(data: dict):
627
+ """Update emotional bids state from bridge"""
628
+ aliveness_state["bids"].update(data)
629
+ aliveness_state["bids"]["updated_at"] = datetime.now().isoformat()
630
+ for client in clients:
631
+ client.set()
632
+
633
+
634
+ def update_memory_state(data: dict):
635
+ """Update emotional memory state from bridge"""
636
+ aliveness_state["memory"].update(data)
637
+ aliveness_state["memory"]["updated_at"] = datetime.now().isoformat()
638
+ for client in clients:
639
+ client.set()
640
+
641
+
642
+ def update_inconsistency_state(data: dict):
643
+ """Update inconsistency state from bridge"""
644
+ aliveness_state["inconsistency"].update(data)
645
+ aliveness_state["inconsistency"]["updated_at"] = datetime.now().isoformat()
646
+ for client in clients:
647
+ client.set()
648
+
649
+
650
+ @app.get("/api/aliveness/interoceptive")
651
+ async def get_interoceptive_state():
652
+ """Get current interoceptive states (internal body)"""
653
+ # Try to get fresh data from the interoceptive system
654
+ try:
655
+ from heart.interoception import get_interoceptive_system
656
+ system = get_interoceptive_system()
657
+ states = system.get_state_values()
658
+ report = system.get_feeling_report()
659
+
660
+ return {
661
+ "states": {name: {"current_value": val} for name, val in states.items()},
662
+ "current_mood": aliveness_state["interoceptive"].get("current_mood", "content"),
663
+ "bodily_description": report.bodily_description if report else "feeling balanced",
664
+ "needs": report.needs if report else [],
665
+ "updated_at": aliveness_state["interoceptive"].get("updated_at")
666
+ }
667
+ except Exception as e:
668
+ print(f"[WebUI] Error getting interoceptive state: {e}")
669
+ return aliveness_state["interoceptive"]
670
+
671
+
672
+ @app.get("/api/aliveness/idle")
673
+ async def get_idle_state():
674
+ """Get current idle/default mode state"""
675
+ # Try to get fresh data from default mode processor
676
+ try:
677
+ from brain.default_mode import get_default_mode_processor
678
+ processor = get_default_mode_processor()
679
+ if processor:
680
+ status = processor.get_status()
681
+ thoughts = processor.get_recent_thoughts(limit=5)
682
+
683
+ return {
684
+ "running": status.get("running", False),
685
+ "recent_thoughts": [
686
+ {
687
+ "thought_type": t.thought_type,
688
+ "content": t.content,
689
+ "priority": t.priority
690
+ }
691
+ for t in thoughts
692
+ ],
693
+ "pending_initiations": status.get("pending_initiations", 0),
694
+ "last_processing": status.get("last_processing"),
695
+ "updated_at": aliveness_state["idle"].get("updated_at")
696
+ }
697
+ except Exception as e:
698
+ print(f"[WebUI] Error getting idle state: {e}")
699
+
700
+ return aliveness_state["idle"]
701
+
702
+
703
+ @app.get("/api/aliveness/bids")
704
+ async def get_bids_state():
705
+ """Get current emotional bids state"""
706
+ return aliveness_state["bids"]
707
+
708
+
709
+ @app.get("/api/aliveness/memory")
710
+ async def get_memory_state():
711
+ """Get current emotional memory stats"""
712
+ # Try to get fresh data from emotional memory system
713
+ try:
714
+ from brain.emotional_memory import get_emotional_memory_system
715
+ system = get_emotional_memory_system()
716
+ stats = system.get_stats()
717
+ recent_high = system.get_recent_high_emotion(hours=24, limit=1)
718
+
719
+ return {
720
+ "total_memories": stats.get("total_memories", 0),
721
+ "average_weight": stats.get("average_weight", 0),
722
+ "high_emotion_count": stats.get("high_emotion_count", 0),
723
+ "last_significant_memory": recent_high[0].content[:100] if recent_high else None,
724
+ "updated_at": aliveness_state["memory"].get("updated_at")
725
+ }
726
+ except Exception as e:
727
+ print(f"[WebUI] Error getting memory state: {e}")
728
+
729
+ return aliveness_state["memory"]
730
+
731
+
732
+ @app.get("/api/aliveness/inconsistency")
733
+ async def get_inconsistency_state():
734
+ """Get current inconsistency (human-like) state"""
735
+ # Try to get fresh data from inconsistency engine
736
+ try:
737
+ from heart.inconsistency import get_inconsistency_engine
738
+ engine = get_inconsistency_engine()
739
+ modifier = engine.get_inconsistency_modifier()
740
+
741
+ return {
742
+ "active_conflicts": modifier.get("active_conflicts", []),
743
+ "active_blind_spots": modifier.get("active_blind_spots", []),
744
+ "mood": modifier.get("mood", {"state": "content"}),
745
+ "behavioral_tendency": modifier.get("behavioral_tendency", "neutral"),
746
+ "growth_summary": modifier.get("growth_summary", {}),
747
+ "updated_at": aliveness_state["inconsistency"].get("updated_at")
748
+ }
749
+ except Exception as e:
750
+ print(f"[WebUI] Error getting inconsistency state: {e}")
751
+
752
+ return aliveness_state["inconsistency"]
753
+
754
+
755
+ @app.get("/api/aliveness")
756
+ async def get_full_aliveness():
757
+ """Get all aliveness data in one request"""
758
+ return {
759
+ "interoceptive": await get_interoceptive_state(),
760
+ "idle": await get_idle_state(),
761
+ "bids": await get_bids_state(),
762
+ "memory": await get_memory_state(),
763
+ "inconsistency": await get_inconsistency_state()
764
+ }
765
+
766
+
767
+ @app.get("/api/aliveness/new")
768
+ async def get_new_aliveness():
769
+ """Get all new aliveness module states"""
770
+ result = {}
771
+
772
+ # Afterglow
773
+ try:
774
+ from heart.afterglow import get_afterglow_engine
775
+ ag = get_afterglow_engine()
776
+ active = []
777
+ for a in ag.active_afterglows:
778
+ # Parse recorded_at ISO string to calculate hours ago
779
+ recorded_str = a.get("recorded_at", "")
780
+ hours_ago = 0.0
781
+ if recorded_str:
782
+ try:
783
+ from datetime import datetime as dt
784
+ recorded_dt = dt.fromisoformat(recorded_str)
785
+ hours_ago = round((dt.now().timestamp() - recorded_dt.timestamp()) / 3600, 1)
786
+ except:
787
+ pass
788
+ active.append({
789
+ "type": a["type"],
790
+ "intensity": round(a.get("intensity", 0), 2),
791
+ "hours_ago": hours_ago
792
+ })
793
+ result["afterglow"] = {"active": active, "count": len(active)}
794
+ except Exception:
795
+ result["afterglow"] = {"active": [], "count": 0}
796
+
797
+ # Circadian
798
+ try:
799
+ from heart.circadian import get_circadian_engine, _get_phase_for_hour
800
+ ce = get_circadian_engine()
801
+ phase_name, _ = _get_phase_for_hour(datetime.now().hour)
802
+ result["circadian"] = {
803
+ "phase": phase_name,
804
+ "sleeping": ce.is_sleeping(),
805
+ "sleep_debt": round(ce.sleep_debt, 2),
806
+ "modifiers": ce.get_personality_modifiers()
807
+ }
808
+ except Exception:
809
+ result["circadian"] = {"phase": "unknown", "sleeping": False, "sleep_debt": 0, "modifiers": {}}
810
+
811
+ # Attachment
812
+ try:
813
+ from heart.attachment import get_attachment_engine
814
+ ae = get_attachment_engine()
815
+ result["attachment"] = {
816
+ "style": ae.get_attachment_style(),
817
+ "security": round(ae.security_score, 2),
818
+ "trend": ae.get_recent_trend()
819
+ }
820
+ except Exception as e:
821
+ result["attachment"] = {"style": "unknown", "security": 0.5, "trend": "stable"}
822
+
823
+ # Phantom Somatic
824
+ try:
825
+ from heart.phantom_somatic import get_phantom_engine
826
+ pe = get_phantom_engine()
827
+ phantoms = [{"type": p["type"], "intensity": round(p["intensity"], 2),
828
+ "description": p.get("description", "")}
829
+ for p in pe.phantoms]
830
+ result["phantom_somatic"] = {"active": phantoms, "count": len(phantoms)}
831
+ except Exception:
832
+ result["phantom_somatic"] = {"active": [], "count": 0}
833
+
834
+ # Mood Shifts
835
+ try:
836
+ from heart.mood_shifts import get_mood_shift_tracker
837
+ ms = get_mood_shift_tracker()
838
+ result["mood_shift"] = {"last_shift": ms.last_shift, "shift_count": ms.shift_count if hasattr(ms, 'shift_count') else 0}
839
+ except Exception:
840
+ result["mood_shift"] = {"last_shift": None, "shift_count": 0}
841
+
842
+ # Narrative
843
+ try:
844
+ from brain.narrative import get_narrative_engine
845
+ ne = get_narrative_engine()
846
+ # Get owner's narrative
847
+ from core.settings import get as settings_get
848
+ owner_id = str(settings_get("TELEGRAM_OWNER_ID", ""))
849
+ if owner_id:
850
+ data = ne._get_data(owner_id)
851
+ msg_count = data.get("message_count", 0)
852
+
853
+ # If narrative has no count, count actual messages from episodic files
854
+ if msg_count == 0:
855
+ try:
856
+ from pathlib import Path
857
+ conv_dir = Path(f"./data/data/users/{owner_id}/conversations")
858
+ if conv_dir.exists():
859
+ total_lines = 0
860
+ for f in conv_dir.glob("*.jsonl"):
861
+ total_lines += sum(1 for _ in open(f))
862
+ msg_count = total_lines
863
+ except Exception:
864
+ pass
865
+
866
+ result["narrative"] = {
867
+ "phase": data.get("phase", "first_meeting"),
868
+ "message_count": msg_count,
869
+ "moments": len(data.get("key_moments", []))
870
+ }
871
+ else:
872
+ result["narrative"] = {"phase": "unknown", "message_count": 0, "moments": 0}
873
+ except Exception:
874
+ result["narrative"] = {"phase": "unknown", "message_count": 0, "moments": 0}
875
+
876
+ # Dreams
877
+ try:
878
+ from brain.dreams import get_dream_system
879
+ ds = get_dream_system()
880
+ last_dream = ds._dreams[-1] if ds._dreams else None
881
+ result["dreams"] = {
882
+ "total": len(ds._dreams),
883
+ "last_dream": last_dream.get("content", "") if last_dream else None,
884
+ "last_dream_time": last_dream.get("created_at", "") if last_dream else None
885
+ }
886
+ except Exception:
887
+ result["dreams"] = {"total": 0, "last_dream": None}
888
+
889
+ # Linguistic
890
+ try:
891
+ from brain.linguistic import get_linguistic_profile
892
+ from core.settings import get as settings_get
893
+ owner_id = str(settings_get("TELEGRAM_OWNER_ID", ""))
894
+ if owner_id:
895
+ lp = get_linguistic_profile(owner_id)
896
+ patterns = lp.get_absorbed_patterns() if hasattr(lp, 'get_absorbed_patterns') else {}
897
+ result["linguistic"] = {
898
+ "messages_analyzed": lp.total_messages,
899
+ "absorbed_words": patterns.get("words", [])[:5],
900
+ "abbreviations": patterns.get("abbreviations", [])[:5],
901
+ "emojis": patterns.get("emojis", [])[:5],
902
+ }
903
+ else:
904
+ result["linguistic"] = {"messages_analyzed": 0}
905
+ except Exception:
906
+ result["linguistic"] = {"messages_analyzed": 0}
907
+
908
+ # Curiosity
909
+ try:
910
+ from brain.curiosity import get_curiosity_drive
911
+ from core.settings import get as settings_get
912
+ owner_id = str(settings_get("TELEGRAM_OWNER_ID", ""))
913
+ if owner_id:
914
+ cd = get_curiosity_drive(owner_id)
915
+ topics = {t: round(v, 2) for t, v in cd.knowledge.items()} if hasattr(cd, 'knowledge') else {}
916
+ result["curiosity"] = {"topics": topics}
917
+ else:
918
+ result["curiosity"] = {"topics": {}}
919
+ except Exception:
920
+ result["curiosity"] = {"topics": {}}
921
+
922
+ # Almost-Said
923
+ try:
924
+ from brain.almost_said import get_almost_said_engine
925
+ ae2 = get_almost_said_engine()
926
+ result["almost_said"] = {"message_counter": ae2.message_counter if hasattr(ae2, 'message_counter') else 0}
927
+ except Exception:
928
+ result["almost_said"] = {"message_counter": 0}
929
+
930
+ return result
931
+
932
+
933
+ # Mount static files
934
+ static_path = Path(__file__).parent / "static"
935
+ if static_path.exists():
936
+ app.mount("/static", StaticFiles(directory=str(static_path)), name="static")