84c4f75e03
Turn the inert "Show Work" thinking panel into a real live activity log: - lyra/logbus.py: thread-safe in-memory ring buffer other modules publish to - chat.respond logs backend/model/embed per turn, recall counts, reply size; web layer logs chat errors - server: replace the keep-alive /stream/thinking stub with /stream/logs, an SSE endpoint that replays the recent buffer then streams new events - UI: repurpose the panel as a global "Live Log" — connects on load, renders level/time/msg/fields, drops the old per-session localStorage + dead popup Every turn now shows its backend + model in-app, so local-vs-cloud (free vs paid) is visible at a glance. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
64 lines
2.3 KiB
Python
64 lines
2.3 KiB
Python
"""The chat turn loop: persona + recalled memory + recent context -> reply.
|
|
|
|
Each turn assembles the persona system prompt, semantically-relevant memories
|
|
recalled from across all past sessions, and the recent turns of the current
|
|
session, then asks the model for a reply and persists both sides.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from lyra import config, llm, logbus, memory, persona
|
|
from lyra.llm import Backend, Message
|
|
|
|
RECALL_K = 5
|
|
RECENT_N = 10
|
|
|
|
|
|
def _memory_note(exchanges: list[memory.Exchange]) -> Message:
|
|
"""Format recalled memories as a system note Lyra can draw on."""
|
|
lines = []
|
|
for ex in exchanges:
|
|
when = ex.created_at[:10] # YYYY-MM-DD
|
|
lines.append(f"- ({when}, {ex.role}) {ex.content}")
|
|
body = "Relevant things you remember from past conversations:\n" + "\n".join(lines)
|
|
return {"role": "system", "content": body}
|
|
|
|
|
|
def build_messages(session_id: str, user_msg: str) -> list[Message]:
|
|
"""Assemble the full message list for one turn."""
|
|
messages: list[Message] = [{"role": "system", "content": persona.system_prompt()}]
|
|
|
|
recent = memory.recent(session_id, n=RECENT_N)
|
|
recent_ids = {ex.id for ex in recent}
|
|
|
|
# Cross-session recall, minus anything already shown in the recent window.
|
|
recalled = [
|
|
ex for ex in memory.recall(user_msg, k=RECALL_K) if ex.id not in recent_ids
|
|
]
|
|
logbus.log("debug", "context built", recent=len(recent), recalled=len(recalled))
|
|
if recalled:
|
|
messages.append(_memory_note(recalled))
|
|
|
|
for ex in recent:
|
|
messages.append({"role": ex.role, "content": ex.content})
|
|
|
|
messages.append({"role": "user", "content": user_msg})
|
|
return messages
|
|
|
|
|
|
def respond(session_id: str, user_msg: str, backend: Backend = "cloud") -> str:
|
|
"""Produce Lyra's reply to a single user message and persist the exchange."""
|
|
cfg = config.load()
|
|
model = cfg.local_model if backend == "local" else cfg.cloud_model
|
|
logbus.log(
|
|
"info", "chat request", session=session_id, backend=backend,
|
|
model=model, embed=cfg.embed_backend,
|
|
)
|
|
|
|
messages = build_messages(session_id, user_msg)
|
|
reply = llm.complete(messages, backend=backend)
|
|
logbus.log("info", "reply", session=session_id, chars=len(reply))
|
|
|
|
memory.remember(session_id, "user", user_msg)
|
|
memory.remember(session_id, "assistant", reply)
|
|
return reply
|