feat: persona chat loop, web UI, and local (Ollama) embeddings
Phase 1 — persona + persistent memory chat loop: - lyra/persona.py + personas/lyra.md: editable identity/voice (friend-first, honest, never invents poker math) - lyra/chat.py: turn loop assembling persona + cross-session recall + recent context, persisting both sides to SQLite - lyra/session.py, lyra/__main__.py: session lifecycle + `lyra` REPL Phase 1.25 — reuse the old web UI: - vendored the prior single-page UI into lyra/web/static, repointed to same-origin - lyra/web/server.py (FastAPI): serves the UI and backs its endpoint contract (/v1/chat/completions, session CRUD, health, inert thinking-stream) with the new chat loop + memory; SQLite stays the single source of truth - `lyra-web` console script Local backends — test for free, no OpenAI key: - llm.embed routes via EMBED_BACKEND (cloud=OpenAI, local=Ollama /api/embed) - simplified UI backend selector to Local (Ollama) / Cloud (OpenAI), default local - memory connection opened check_same_thread=False for the threaded server Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
"""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 llm, 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
|
||||
]
|
||||
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."""
|
||||
messages = build_messages(session_id, user_msg)
|
||||
reply = llm.complete(messages, backend=backend)
|
||||
|
||||
memory.remember(session_id, "user", user_msg)
|
||||
memory.remember(session_id, "assistant", reply)
|
||||
return reply
|
||||
Reference in New Issue
Block a user