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
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")
|