alive-ai 0.1.13 → 0.1.15
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/README.md +8 -2
- package/brain/default_mode.py +2 -1
- package/cli/check_webui_static.js +16 -0
- package/core/message_handler.py +19 -6
- package/heart/circadian.py +27 -3
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/webui/app.py +161 -37
- package/webui/bridge.py +14 -4
- package/webui/persistence.py +268 -0
- package/webui/static/index.html +164 -82
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ The emotional layer now has real runtime consequences:
|
|
|
31
31
|
| Complex emotions | Guilt, pride, jealousy, embarrassment, and anticipation. | They do not just label the dashboard. They push fear, sadness, anger, dominance, trust, arousal, joy, and future-facing behavior in different directions. |
|
|
32
32
|
| Hormones | Oxytocin, dopamine, serotonin, cortisol, melatonin, plus residual metabolites. | Hormones modulate perception, soul valence/arousal, emotional deltas, somatic body state, interoception, impulse probability, and prompt guidance. Stress makes her more vigilant; bonding increases trust; dopamine increases pursuit; serotonin stabilizes; melatonin slows her down. |
|
|
33
33
|
| Internal body state | Energy, arousal, certainty, social satiety, cognitive load, connection craving, body sensations, and somatic memories. | The body state is persisted and feeds prompt tone, sleep/rest behavior, and whether she feels steady, overloaded, touchy, open, or withdrawn. |
|
|
34
|
-
| Circadian rhythm | Phase, sleep pressure, sleep debt, forced-awake windows, sleep cycle ID, wake time, and sleepiness. | She becomes sleepy, slows down, falls asleep, stops outward proactive behavior while asleep, can be woken by a message, recovers sleep debt, and wakes with lower or higher energy depending on rest. |
|
|
34
|
+
| Circadian rhythm | Phase, sleep pressure, sleep debt in hours, forced-awake windows, sleep cycle ID, wake time, and sleepiness. | She becomes sleepy, slows down, falls asleep, stops outward proactive behavior while asleep, can be woken by a message, recovers sleep debt, and wakes with lower or higher energy depending on rest. After 2am, high sleep pressure shortens user wake-up windows so she can drift back to sleep instead of staying pinned awake. |
|
|
35
35
|
| Dreams | One normalized dream per sleep cycle, generated from memory fragments and emotion tags. | Dreams are saved, can appear in morning context, and are exposed in the dashboard and static Pages demo. |
|
|
36
36
|
| Narrative | Relationship phase (first_meeting → bonded) tracked per user. Key moments are detected from message content. | Phase and moment count are injected into the LLM prompt each turn. The dashboard shows the current phase and total key moments. |
|
|
37
37
|
| Curiosity | Per-user knowledge map across topics detected in messages. Topics range from 0 (unknown) to 1 (well-understood). | When knowledge on a topic is below 0.3 she asks a direct question. At 40% probability otherwise she surfaces curiosity as a prompt hint. Dashboard shows topics sorted by curiosity level. |
|
|
@@ -277,7 +277,13 @@ The real WebUI streams local runtime state over Server-Sent Events and shows:
|
|
|
277
277
|
- attachment, circadian rhythm, sleepiness, body memory, dreams, curiosity, and conflicts,
|
|
278
278
|
- runtime health through local endpoints.
|
|
279
279
|
|
|
280
|
-
The WebUI
|
|
280
|
+
The WebUI hydrates from durable runtime stores instead of only the current browser session. It resolves the active dashboard user from explicit WebUI input, live Telegram activity, configured owner ID, runtime state, and finally the most active user folder on disk. Chat rows are journaled per active user under `data/users/<user>/webui_chat.jsonl` and merged with episodic Telegram conversation history, including legacy flat `data/conversations` history after upgrades. `/state` and the SSE stream now use the same composed snapshot: visible chat, runtime state, soul state, aliveness state, current thoughts, memory counters, and the active dashboard user.
|
|
281
|
+
|
|
282
|
+
Sleep debt is stored and shown as hours on a 0-8h pressure scale. The UI no longer reports it as a misleading capped percentage, so a persisted `5.6h` debt displays as `5.6h` with the matching pressure bar.
|
|
283
|
+
|
|
284
|
+
Settings edits validate JSON before saving and write atomically, so a bad edit cannot corrupt the existing config file. The Settings tab also protects unsaved edits while switching tabs.
|
|
285
|
+
|
|
286
|
+
The WebUI script is intentionally shipped as a single static file because the npm package has to run locally without a frontend build step. `npm run smoke` compiles the Python modules and checks the CLI; release validation also parses the embedded dashboard script and verifies required dashboard hooks so tab navigation, chat, settings, and thought rendering cannot be broken by a syntax error.
|
|
281
287
|
|
|
282
288
|
GitHub Pages cannot run the Python/FastAPI backend, so the public page includes a static export of the actual WebUI with mocked state:
|
|
283
289
|
|
package/brain/default_mode.py
CHANGED
|
@@ -251,7 +251,8 @@ class DefaultModeProcessor:
|
|
|
251
251
|
if data_path:
|
|
252
252
|
self.data_path = data_path
|
|
253
253
|
else:
|
|
254
|
-
|
|
254
|
+
from core.paths import data_dir
|
|
255
|
+
self.data_path = data_dir()
|
|
255
256
|
|
|
256
257
|
self.data_path.mkdir(parents=True, exist_ok=True)
|
|
257
258
|
|
|
@@ -26,4 +26,20 @@ if (count === 0) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
29
|
+
for (const page of ["home", "chat", "settings"]) {
|
|
30
|
+
if (!html.includes(`data-page="${page}"`)) {
|
|
31
|
+
console.error(`Missing bottom-nav data-page="${page}" hook.`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
if (!html.includes(`id="page-${page}"`)) {
|
|
35
|
+
console.error(`Missing page container #page-${page}.`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
for (const required of ["/static/manifest.json", "/static/icon.svg"]) {
|
|
40
|
+
if (!html.includes(required)) {
|
|
41
|
+
console.error(`Missing static asset reference: ${required}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
29
45
|
console.log(`WebUI static check passed (${count} inline script parsed).`);
|
package/core/message_handler.py
CHANGED
|
@@ -264,11 +264,13 @@ def _get_or_create_user_memory(self, user_id: str):
|
|
|
264
264
|
if cache_key in _user_memories:
|
|
265
265
|
return _user_memories[cache_key]
|
|
266
266
|
|
|
267
|
-
# Create new memory instance for this user using
|
|
267
|
+
# Create new memory instance for this user using the canonical per-user path.
|
|
268
268
|
from brain.memory import Memory
|
|
269
|
+
from core.user_manager import UserManager
|
|
269
270
|
|
|
270
|
-
|
|
271
|
-
|
|
271
|
+
user_manager = UserManager()
|
|
272
|
+
user_manager.migrate_legacy_data(user_id)
|
|
273
|
+
instance_data_path = user_manager.get_user_paths(user_id)["base"]
|
|
272
274
|
|
|
273
275
|
memory = Memory(
|
|
274
276
|
nervous=self.nervous,
|
|
@@ -462,6 +464,7 @@ async def _process_single_message(self, data: dict):
|
|
|
462
464
|
if self._subconscious: self._subconscious.register_interaction()
|
|
463
465
|
if chat_id: self._default_chat_id = chat_id
|
|
464
466
|
text = data.get("text", "")
|
|
467
|
+
message_id = data.get("message_id")
|
|
465
468
|
|
|
466
469
|
circadian_interaction = {}
|
|
467
470
|
if CIRCADIAN_AVAILABLE:
|
|
@@ -786,7 +789,7 @@ async def _process_single_message(self, data: dict):
|
|
|
786
789
|
# Track if we asked a question (for follow-ups)
|
|
787
790
|
_follow_up.record_message_sent(response)
|
|
788
791
|
|
|
789
|
-
await _send_response(self, response, emotion, chat_id, text, user_id)
|
|
792
|
+
await _send_response(self, response, emotion, chat_id, text, user_id, message_id=message_id)
|
|
790
793
|
if self._subconscious: _feed_learning(self._subconscious, text)
|
|
791
794
|
|
|
792
795
|
# Actually send the media (we already decided what to send)
|
|
@@ -1094,7 +1097,7 @@ IMPORTANT: You are sending this media ALONG with your message. Reference it natu
|
|
|
1094
1097
|
return fallback_response(emotion, msg)
|
|
1095
1098
|
|
|
1096
1099
|
|
|
1097
|
-
async def _send_response(self, response, emotion, chat_id, text, user_id="default"):
|
|
1100
|
+
async def _send_response(self, response, emotion, chat_id, text, user_id="default", message_id=None):
|
|
1098
1101
|
mood = emotion.get("mood", "neutral")
|
|
1099
1102
|
|
|
1100
1103
|
# Process any action tags in the response (pass instance config path)
|
|
@@ -1123,9 +1126,19 @@ async def _send_response(self, response, emotion, chat_id, text, user_id="defaul
|
|
|
1123
1126
|
"chat_id": chat_id,
|
|
1124
1127
|
"fallback_text": response,
|
|
1125
1128
|
"mood": mood,
|
|
1129
|
+
"user_id": user_id,
|
|
1130
|
+
"reply_to_message_id": message_id,
|
|
1131
|
+
"source": "runtime",
|
|
1126
1132
|
})
|
|
1127
1133
|
return
|
|
1128
|
-
await self.nervous.emit("send_text", {
|
|
1134
|
+
await self.nervous.emit("send_text", {
|
|
1135
|
+
"text": response,
|
|
1136
|
+
"mood": mood,
|
|
1137
|
+
"chat_id": chat_id,
|
|
1138
|
+
"user_id": user_id,
|
|
1139
|
+
"reply_to_message_id": message_id,
|
|
1140
|
+
"source": "runtime",
|
|
1141
|
+
})
|
|
1129
1142
|
|
|
1130
1143
|
|
|
1131
1144
|
def _process_self_authorship_actions(response: str, user_id: str = "default", self_path: Path = None) -> tuple:
|
package/heart/circadian.py
CHANGED
|
@@ -140,9 +140,21 @@ class CircadianEngine:
|
|
|
140
140
|
return round(self._clamp(sleepiness), 2)
|
|
141
141
|
|
|
142
142
|
def _should_auto_sleep(self, now: datetime) -> bool:
|
|
143
|
-
|
|
143
|
+
sleepiness = self.get_sleepiness()
|
|
144
|
+
if not (2 <= now.hour < 6 and sleepiness >= 0.85):
|
|
144
145
|
return False
|
|
145
|
-
|
|
146
|
+
if not self._is_forced_awake(now):
|
|
147
|
+
return True
|
|
148
|
+
# After 2am, sleep pressure can override a user wake-up. Messages can
|
|
149
|
+
# briefly rouse her, but they should not pin her awake for another hour.
|
|
150
|
+
if sleepiness >= 0.95:
|
|
151
|
+
return True
|
|
152
|
+
try:
|
|
153
|
+
until = datetime.fromisoformat(self.forced_awake_until) if self.forced_awake_until else now
|
|
154
|
+
forced_started = until - timedelta(minutes=self._forced_awake_duration_minutes(now))
|
|
155
|
+
return (now - forced_started).total_seconds() >= 10 * 60
|
|
156
|
+
except Exception:
|
|
157
|
+
return sleepiness >= 0.9
|
|
146
158
|
|
|
147
159
|
def _should_auto_wake(self, now: datetime) -> bool:
|
|
148
160
|
slept = self._hours_asleep(now)
|
|
@@ -231,9 +243,21 @@ class CircadianEngine:
|
|
|
231
243
|
self._save()
|
|
232
244
|
return True
|
|
233
245
|
|
|
234
|
-
def
|
|
246
|
+
def _forced_awake_duration_minutes(self, now: datetime = None) -> int:
|
|
247
|
+
now = now or self._now()
|
|
248
|
+
sleepiness = self.get_sleepiness()
|
|
249
|
+
if 2 <= now.hour < 6 and sleepiness >= 0.9:
|
|
250
|
+
return 10
|
|
251
|
+
if 0 <= now.hour < 6 and sleepiness >= 0.85:
|
|
252
|
+
return 20
|
|
253
|
+
if now.hour >= 23 and sleepiness >= 0.75:
|
|
254
|
+
return 30
|
|
255
|
+
return 45
|
|
256
|
+
|
|
257
|
+
def stay_up_for_user(self, duration_minutes: int = None):
|
|
235
258
|
"""User is keeping her awake past bedtime."""
|
|
236
259
|
now = self._now()
|
|
260
|
+
duration_minutes = duration_minutes or self._forced_awake_duration_minutes(now)
|
|
237
261
|
self.forced_awake = True
|
|
238
262
|
self.forced_awake_until = (now + timedelta(minutes=duration_minutes)).isoformat()
|
|
239
263
|
self.last_transition_reason = "staying_up_for_user"
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/webui/app.py
CHANGED
|
@@ -13,6 +13,13 @@ from fastapi import FastAPI, Request, BackgroundTasks
|
|
|
13
13
|
from fastapi.responses import HTMLResponse, StreamingResponse, FileResponse, JSONResponse
|
|
14
14
|
from fastapi.staticfiles import StaticFiles
|
|
15
15
|
from core.paths import data_dir, media_dir
|
|
16
|
+
from .persistence import (
|
|
17
|
+
append_chat_message,
|
|
18
|
+
count_visible_messages,
|
|
19
|
+
load_chat_messages,
|
|
20
|
+
new_message_id,
|
|
21
|
+
resolve_active_user_id,
|
|
22
|
+
)
|
|
16
23
|
|
|
17
24
|
|
|
18
25
|
app = FastAPI(title="Alive-AI Dashboard")
|
|
@@ -21,26 +28,45 @@ app = FastAPI(title="Alive-AI Dashboard")
|
|
|
21
28
|
_start_time = datetime.now()
|
|
22
29
|
|
|
23
30
|
|
|
24
|
-
def load_persistent_stats() -> dict:
|
|
31
|
+
def load_persistent_stats(active_user: str = None) -> dict:
|
|
25
32
|
"""Load stats from actual data sources on startup"""
|
|
26
33
|
stats = {"messages": 0, "memories": 0, "evaluations": 0}
|
|
27
34
|
|
|
28
35
|
# Try different base paths
|
|
29
36
|
base_paths = [data_dir()]
|
|
30
37
|
|
|
31
|
-
|
|
38
|
+
if active_user:
|
|
39
|
+
try:
|
|
40
|
+
stats["messages"] = count_visible_messages(active_user)
|
|
41
|
+
except Exception:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
# Count actual per-user conversation rows and WebUI journal rows.
|
|
32
45
|
for base_path in base_paths:
|
|
33
46
|
try:
|
|
34
|
-
# Look for summaries in users/*/summaries/
|
|
35
47
|
users_path = base_path / "users"
|
|
36
48
|
if users_path.exists():
|
|
37
49
|
count = 0
|
|
38
50
|
for user_dir in users_path.iterdir():
|
|
39
|
-
|
|
40
|
-
if
|
|
41
|
-
|
|
51
|
+
conv_path = user_dir / "conversations"
|
|
52
|
+
if conv_path.exists():
|
|
53
|
+
for conv_file in conv_path.glob("*.jsonl"):
|
|
54
|
+
with conv_file.open() as fh:
|
|
55
|
+
for line in fh:
|
|
56
|
+
try:
|
|
57
|
+
row = json.loads(line)
|
|
58
|
+
if row.get("user"):
|
|
59
|
+
count += 1
|
|
60
|
+
if row.get("ai"):
|
|
61
|
+
count += 1
|
|
62
|
+
except Exception:
|
|
63
|
+
continue
|
|
64
|
+
journal_path = user_dir / "webui_chat.jsonl"
|
|
65
|
+
if journal_path.exists():
|
|
66
|
+
with journal_path.open() as fh:
|
|
67
|
+
count += sum(1 for _ in fh)
|
|
42
68
|
if count > 0:
|
|
43
|
-
stats["messages"] = count
|
|
69
|
+
stats["messages"] = max(stats["messages"], count)
|
|
44
70
|
break
|
|
45
71
|
except Exception:
|
|
46
72
|
pass
|
|
@@ -245,6 +271,79 @@ aliveness_state = {
|
|
|
245
271
|
}
|
|
246
272
|
|
|
247
273
|
|
|
274
|
+
def _active_user_id(explicit=None) -> str:
|
|
275
|
+
return resolve_active_user_id(explicit, self_ref=_self_ref, dashboard_state=alive_ai_state)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _runtime_state_dict() -> dict:
|
|
279
|
+
runtime_state = getattr(_self_ref, "state", None)
|
|
280
|
+
if runtime_state and hasattr(runtime_state, "to_dict"):
|
|
281
|
+
try:
|
|
282
|
+
return runtime_state.to_dict()
|
|
283
|
+
except Exception:
|
|
284
|
+
return {}
|
|
285
|
+
return {}
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _runtime_chat_ready() -> bool:
|
|
289
|
+
nervous = getattr(_self_ref, "nervous", None)
|
|
290
|
+
listeners = getattr(nervous, "listeners", {}) if nervous else {}
|
|
291
|
+
# Bridge registers one listener; the runtime handler is attached during Self.start().
|
|
292
|
+
return len(listeners.get("message_received", [])) > 1
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _subconscious_thoughts(limit: int = 10) -> list:
|
|
296
|
+
thoughts = []
|
|
297
|
+
sub = getattr(_self_ref, "_subconscious", None)
|
|
298
|
+
wm = getattr(sub, "working_memory", None)
|
|
299
|
+
if wm and hasattr(wm, "get_recent_thoughts"):
|
|
300
|
+
try:
|
|
301
|
+
for thought in wm.get_recent_thoughts(limit):
|
|
302
|
+
thoughts.append({
|
|
303
|
+
"thought": getattr(thought, "content", ""),
|
|
304
|
+
"type": getattr(thought, "type", "reflection"),
|
|
305
|
+
"emotion": getattr(thought, "emotion", {}) or {},
|
|
306
|
+
"time": _format_time(getattr(thought, "created_at", None)),
|
|
307
|
+
})
|
|
308
|
+
except Exception:
|
|
309
|
+
thoughts = []
|
|
310
|
+
if thoughts:
|
|
311
|
+
return thoughts[-limit:]
|
|
312
|
+
return alive_ai_state.get("recent_thoughts", [])[-limit:]
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _format_time(value) -> str:
|
|
316
|
+
if not value:
|
|
317
|
+
return datetime.now().strftime("%H:%M:%S")
|
|
318
|
+
try:
|
|
319
|
+
if isinstance(value, datetime):
|
|
320
|
+
return value.strftime("%H:%M:%S")
|
|
321
|
+
return datetime.fromisoformat(str(value)).strftime("%H:%M:%S")
|
|
322
|
+
except Exception:
|
|
323
|
+
text = str(value)
|
|
324
|
+
return text[11:19] if len(text) >= 19 else text
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def build_snapshot(user_id: str = None) -> dict:
|
|
328
|
+
"""Compose the dashboard state from live and durable runtime stores."""
|
|
329
|
+
active_user = _active_user_id(user_id)
|
|
330
|
+
snapshot = dict(alive_ai_state)
|
|
331
|
+
snapshot["active_user"] = active_user
|
|
332
|
+
snapshot["runtime"] = _runtime_state_dict()
|
|
333
|
+
snapshot["soul"] = soul_state
|
|
334
|
+
snapshot["aliveness"] = aliveness_state
|
|
335
|
+
snapshot["conversation"] = load_chat_messages(active_user)
|
|
336
|
+
snapshot["stats"] = {
|
|
337
|
+
**snapshot.get("stats", {}),
|
|
338
|
+
**load_persistent_stats(active_user),
|
|
339
|
+
}
|
|
340
|
+
thoughts = _subconscious_thoughts()
|
|
341
|
+
snapshot["recent_thoughts"] = thoughts
|
|
342
|
+
snapshot["current_thought"] = thoughts[-1]["thought"] if thoughts else alive_ai_state.get("current_thought")
|
|
343
|
+
snapshot["updated_at"] = datetime.now().isoformat()
|
|
344
|
+
return snapshot
|
|
345
|
+
|
|
346
|
+
|
|
248
347
|
def update_state(data: dict):
|
|
249
348
|
"""Called by nervous system to update state"""
|
|
250
349
|
global alive_ai_state
|
|
@@ -255,15 +354,24 @@ def update_state(data: dict):
|
|
|
255
354
|
client.set()
|
|
256
355
|
|
|
257
356
|
|
|
258
|
-
def add_conversation(role: str, content: str
|
|
357
|
+
def add_conversation(role: str, content: str, message_id: str = None,
|
|
358
|
+
status: str = "sent", user_id: str = None,
|
|
359
|
+
source: str = "runtime"):
|
|
259
360
|
"""Add a message to conversation history"""
|
|
361
|
+
if message_id and any(m.get("message_id") == message_id for m in alive_ai_state["conversation"]):
|
|
362
|
+
return
|
|
260
363
|
alive_ai_state["conversation"].append({
|
|
364
|
+
"message_id": message_id or new_message_id(role),
|
|
261
365
|
"role": role,
|
|
262
366
|
"content": content,
|
|
263
|
-
"time": datetime.now().strftime("%H:%M:%S")
|
|
367
|
+
"time": datetime.now().strftime("%H:%M:%S"),
|
|
368
|
+
"status": status,
|
|
369
|
+
"source": source,
|
|
264
370
|
})
|
|
265
371
|
# Keep last 20 messages
|
|
266
372
|
alive_ai_state["conversation"] = alive_ai_state["conversation"][-20:]
|
|
373
|
+
if user_id:
|
|
374
|
+
alive_ai_state["active_user"] = user_id
|
|
267
375
|
if role == "user":
|
|
268
376
|
alive_ai_state["last_user_message"] = content
|
|
269
377
|
else:
|
|
@@ -316,7 +424,7 @@ async def event_generator(request: Request):
|
|
|
316
424
|
|
|
317
425
|
try:
|
|
318
426
|
# Send initial state
|
|
319
|
-
yield f"event: state\ndata: {json.dumps(
|
|
427
|
+
yield f"event: state\ndata: {json.dumps(build_snapshot())}\n\n"
|
|
320
428
|
|
|
321
429
|
while True:
|
|
322
430
|
if await request.is_disconnected():
|
|
@@ -331,7 +439,7 @@ async def event_generator(request: Request):
|
|
|
331
439
|
continue
|
|
332
440
|
|
|
333
441
|
# Send updated state
|
|
334
|
-
yield f"event: state\ndata: {json.dumps(
|
|
442
|
+
yield f"event: state\ndata: {json.dumps(build_snapshot())}\n\n"
|
|
335
443
|
except asyncio.CancelledError:
|
|
336
444
|
pass # Client disconnected normally
|
|
337
445
|
except Exception as e:
|
|
@@ -372,7 +480,7 @@ async def sse_events(request: Request):
|
|
|
372
480
|
@app.get("/state")
|
|
373
481
|
async def get_state():
|
|
374
482
|
"""Get current state (for polling fallback)"""
|
|
375
|
-
return
|
|
483
|
+
return build_snapshot()
|
|
376
484
|
|
|
377
485
|
|
|
378
486
|
@app.get("/avatar")
|
|
@@ -407,7 +515,7 @@ async def health():
|
|
|
407
515
|
@app.get("/api/stats")
|
|
408
516
|
async def get_persistent_stats():
|
|
409
517
|
"""Get stats refreshed from actual data sources"""
|
|
410
|
-
stats = load_persistent_stats()
|
|
518
|
+
stats = load_persistent_stats(_active_user_id())
|
|
411
519
|
|
|
412
520
|
# Update global state with fresh stats
|
|
413
521
|
alive_ai_state["stats"] = stats
|
|
@@ -456,9 +564,10 @@ async def get_memory_status():
|
|
|
456
564
|
@app.get("/thoughts")
|
|
457
565
|
async def get_thoughts():
|
|
458
566
|
"""Get recent thoughts from subconscious"""
|
|
567
|
+
thoughts = _subconscious_thoughts()
|
|
459
568
|
return {
|
|
460
|
-
"current_thought": alive_ai_state.get("current_thought"),
|
|
461
|
-
"recent_thoughts":
|
|
569
|
+
"current_thought": thoughts[-1]["thought"] if thoughts else alive_ai_state.get("current_thought"),
|
|
570
|
+
"recent_thoughts": thoughts
|
|
462
571
|
}
|
|
463
572
|
|
|
464
573
|
|
|
@@ -726,7 +835,7 @@ async def get_memory_state():
|
|
|
726
835
|
# Try to get fresh data from emotional memory system
|
|
727
836
|
try:
|
|
728
837
|
from brain.emotional_memory import get_emotional_memory_system
|
|
729
|
-
system = get_emotional_memory_system()
|
|
838
|
+
system = get_emotional_memory_system(_active_user_id())
|
|
730
839
|
stats = system.get_stats()
|
|
731
840
|
recent_high = system.get_recent_high_emotion(hours=24, limit=1)
|
|
732
841
|
|
|
@@ -851,8 +960,7 @@ async def get_new_aliveness():
|
|
|
851
960
|
try:
|
|
852
961
|
from brain.narrative import get_narrative_engine
|
|
853
962
|
ne = get_narrative_engine()
|
|
854
|
-
|
|
855
|
-
owner_id = str(settings_get("TELEGRAM_OWNER_ID", ""))
|
|
963
|
+
owner_id = _active_user_id()
|
|
856
964
|
|
|
857
965
|
# Fallback: when owner_id is empty (terminal mode), find the most active user
|
|
858
966
|
if not owner_id:
|
|
@@ -887,7 +995,7 @@ async def get_new_aliveness():
|
|
|
887
995
|
|
|
888
996
|
result["narrative"] = {
|
|
889
997
|
"phase": data.get("phase", "first_meeting"),
|
|
890
|
-
"message_count": msg_count,
|
|
998
|
+
"message_count": max(msg_count, count_visible_messages(owner_id)),
|
|
891
999
|
"moments": len(data.get("key_moments", []))
|
|
892
1000
|
}
|
|
893
1001
|
else:
|
|
@@ -906,8 +1014,7 @@ async def get_new_aliveness():
|
|
|
906
1014
|
# Linguistic
|
|
907
1015
|
try:
|
|
908
1016
|
from brain.linguistic import get_linguistic_profile
|
|
909
|
-
|
|
910
|
-
owner_id = str(settings_get("TELEGRAM_OWNER_ID", ""))
|
|
1017
|
+
owner_id = _active_user_id()
|
|
911
1018
|
if owner_id:
|
|
912
1019
|
lp = get_linguistic_profile(owner_id)
|
|
913
1020
|
patterns = lp.get_absorbed_patterns() if hasattr(lp, 'get_absorbed_patterns') else {}
|
|
@@ -925,8 +1032,7 @@ async def get_new_aliveness():
|
|
|
925
1032
|
# Curiosity
|
|
926
1033
|
try:
|
|
927
1034
|
from brain.curiosity import get_curiosity_drive
|
|
928
|
-
|
|
929
|
-
owner_id = str(settings_get("TELEGRAM_OWNER_ID", ""))
|
|
1035
|
+
owner_id = _active_user_id()
|
|
930
1036
|
|
|
931
1037
|
# Fallback: when owner_id is empty (terminal mode), find the most active user
|
|
932
1038
|
if not owner_id:
|
|
@@ -966,22 +1072,35 @@ async def get_new_aliveness():
|
|
|
966
1072
|
async def chat_endpoint(request: Request, background_tasks: BackgroundTasks):
|
|
967
1073
|
data = await request.json()
|
|
968
1074
|
text = data.get("text", "").strip()
|
|
969
|
-
if not text or not _self_ref:
|
|
1075
|
+
if not text or not _self_ref or not _runtime_chat_ready():
|
|
970
1076
|
return JSONResponse({"status": "error", "message": "No text or AI not ready"}, 400)
|
|
971
|
-
|
|
972
|
-
|
|
1077
|
+
user_id = _active_user_id(data.get("user_id"))
|
|
1078
|
+
message_id = data.get("message_id") or new_message_id("webui_user")
|
|
1079
|
+
append_chat_message(user_id, "user", text, message_id=message_id, status="pending", source="webui")
|
|
1080
|
+
add_conversation("user", text, message_id=message_id, status="pending", user_id=user_id, source="webui")
|
|
973
1081
|
update_state({})
|
|
974
|
-
|
|
1082
|
+
|
|
975
1083
|
async def _send():
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1084
|
+
try:
|
|
1085
|
+
await _self_ref.nervous.emit("message_received", {
|
|
1086
|
+
"message_id": message_id,
|
|
1087
|
+
"user_id": user_id,
|
|
1088
|
+
"webui_user_id": user_id,
|
|
1089
|
+
"text": text,
|
|
1090
|
+
"chat_id": "webui",
|
|
1091
|
+
"source": "webui"
|
|
1092
|
+
})
|
|
1093
|
+
except Exception as e:
|
|
1094
|
+
append_chat_message(
|
|
1095
|
+
user_id,
|
|
1096
|
+
"alive_ai",
|
|
1097
|
+
f"Something went wrong while processing that message: {e}",
|
|
1098
|
+
status="error",
|
|
1099
|
+
source="webui",
|
|
1100
|
+
)
|
|
1101
|
+
update_state({"thinking": False})
|
|
983
1102
|
background_tasks.add_task(_send)
|
|
984
|
-
return JSONResponse({"status": "sent"})
|
|
1103
|
+
return JSONResponse({"status": "sent", "message_id": message_id, "user_id": user_id})
|
|
985
1104
|
|
|
986
1105
|
|
|
987
1106
|
@app.get("/api/settings")
|
|
@@ -1013,13 +1132,18 @@ async def save_settings(request: Request):
|
|
|
1013
1132
|
if fname not in allowed:
|
|
1014
1133
|
return JSONResponse({"status": "error", "message": "Invalid file"}, 400)
|
|
1015
1134
|
config_dir = Path(os.environ.get("ALIVE_AI_ROOT", ".")) / "config"
|
|
1135
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
1016
1136
|
p = config_dir / fname
|
|
1017
1137
|
content = data.get("content")
|
|
1018
1138
|
try:
|
|
1019
1139
|
if fname.endswith(".json"):
|
|
1020
|
-
|
|
1140
|
+
text = json.dumps(content, indent=2, ensure_ascii=False) + "\n"
|
|
1141
|
+
json.loads(text)
|
|
1021
1142
|
else:
|
|
1022
|
-
|
|
1143
|
+
text = str(content or "")
|
|
1144
|
+
tmp = p.with_suffix(p.suffix + ".tmp")
|
|
1145
|
+
tmp.write_text(text)
|
|
1146
|
+
tmp.replace(p)
|
|
1023
1147
|
return {"status": "saved"}
|
|
1024
1148
|
except Exception as e:
|
|
1025
1149
|
return JSONResponse({"status": "error", "message": str(e)}, 500)
|
package/webui/bridge.py
CHANGED
|
@@ -10,6 +10,7 @@ from .app import (
|
|
|
10
10
|
update_interoceptive_state, update_idle_state, update_bids_state,
|
|
11
11
|
update_memory_state, update_inconsistency_state
|
|
12
12
|
)
|
|
13
|
+
from .persistence import append_chat_message, new_message_id, resolve_active_user_id
|
|
13
14
|
|
|
14
15
|
_webui_server = None
|
|
15
16
|
|
|
@@ -46,16 +47,25 @@ def init_bridge(nervous, ai=None):
|
|
|
46
47
|
|
|
47
48
|
async def on_message_sent(data):
|
|
48
49
|
"""Track outgoing messages"""
|
|
49
|
-
text = data.get("text", "")
|
|
50
|
-
|
|
50
|
+
text = data.get("text") or data.get("fallback_text", "")
|
|
51
|
+
user_id = resolve_active_user_id(data.get("user_id"), dashboard_state=alive_ai_state)
|
|
52
|
+
message_id = data.get("message_id") or new_message_id("alive_ai")
|
|
53
|
+
append_chat_message(user_id, "alive_ai", text, message_id=message_id,
|
|
54
|
+
status="sent", source=data.get("source", "runtime"))
|
|
55
|
+
add_conversation("alive_ai", text, message_id=message_id, user_id=user_id,
|
|
56
|
+
source=data.get("source", "runtime"))
|
|
51
57
|
alive_ai_state["stats"]["messages"] = alive_ai_state["stats"].get("messages", 0) + 1
|
|
52
58
|
update_state({})
|
|
53
59
|
|
|
54
60
|
async def on_message_received(data):
|
|
55
61
|
"""Track incoming messages"""
|
|
56
62
|
text = data.get("text", "")
|
|
57
|
-
user_id = data.get("
|
|
58
|
-
|
|
63
|
+
user_id = resolve_active_user_id(data.get("webui_user_id") or data.get("user_id"), dashboard_state=alive_ai_state)
|
|
64
|
+
message_id = data.get("message_id") or new_message_id("user")
|
|
65
|
+
append_chat_message(user_id, "user", text, message_id=message_id,
|
|
66
|
+
status="sent", source=data.get("source", "runtime"))
|
|
67
|
+
add_conversation("user", text, message_id=message_id, user_id=user_id,
|
|
68
|
+
source=data.get("source", "runtime"))
|
|
59
69
|
# Track active user
|
|
60
70
|
alive_ai_state["active_user"] = user_id
|
|
61
71
|
|