Files
jeremy b3bef75981 feat: VoiceCoach v2 — livekit-agents 1.5 personal voice assistant
Replaces pipecat 0.0.48 (6 manual patches, DuplicateIdentity reconnect storms)
with livekit-agents 1.5 SDK. New features:
- SvelteKit UI with live transcript, VAD ring, mode bar, vocal meter, history view
- SQLite session store with WAL mode (cross-session memory)
- Async SpeechBrain vocal/emotion analysis injected per turn
- Homelab tools: SSH/docker, Prometheus alerts, Home Assistant, SearXNG search
- OpenClaw LLM gateway preserved (qwen2.5:14b via Ollama)
- 4 modes: coaching, vent, practice, briefing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 18:05:30 -05:00

141 lines
4.5 KiB
Python

"""SQLite session store — WAL mode, aiosqlite."""
import json
import os
import time
from typing import Optional
import aiosqlite
DB_PATH = os.getenv("DB_PATH", "/data/sessions.db")
async def init_db():
async with aiosqlite.connect(DB_PATH) as db:
await db.execute("PRAGMA journal_mode=WAL")
await db.executescript("""
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
started_at REAL NOT NULL,
ended_at REAL,
mode TEXT DEFAULT 'coaching',
summary TEXT
);
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
vocal_energy REAL,
vocal_emotion TEXT,
ts REAL NOT NULL,
FOREIGN KEY(session_id) REFERENCES sessions(id)
);
CREATE TABLE IF NOT EXISTS profile (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at REAL NOT NULL
);
""")
await db.commit()
async def create_session(session_id: str, mode: str = "coaching"):
async with aiosqlite.connect(DB_PATH) as db:
await db.execute(
"INSERT OR IGNORE INTO sessions (id, started_at, mode) VALUES (?, ?, ?)",
(session_id, time.time(), mode),
)
await db.commit()
async def end_session(session_id: str, summary: Optional[str] = None):
async with aiosqlite.connect(DB_PATH) as db:
await db.execute(
"UPDATE sessions SET ended_at=?, summary=? WHERE id=?",
(time.time(), summary, session_id),
)
await db.commit()
async def add_message(
session_id: str,
role: str,
content: str,
vocal_energy: Optional[float] = None,
vocal_emotion: Optional[str] = None,
):
async with aiosqlite.connect(DB_PATH) as db:
await db.execute(
"INSERT INTO messages (session_id, role, content, vocal_energy, vocal_emotion, ts) VALUES (?,?,?,?,?,?)",
(session_id, role, content, vocal_energy, vocal_emotion, time.time()),
)
await db.commit()
async def get_recent_context(limit: int = 3) -> str:
"""Return a brief context string from the last N sessions for injection into system prompt."""
async with aiosqlite.connect(DB_PATH) as db:
async with db.execute(
"SELECT id, started_at, mode, summary FROM sessions WHERE ended_at IS NOT NULL ORDER BY ended_at DESC LIMIT ?",
(limit,),
) as cur:
rows = await cur.fetchall()
if not rows:
return ""
parts = ["Recent session context:"]
for session_id, started_at, mode, summary in rows:
import datetime
dt = datetime.datetime.fromtimestamp(started_at).strftime("%Y-%m-%d")
if summary:
parts.append(f"- {dt} ({mode}): {summary}")
return "\n".join(parts)
async def get_sessions(limit: int = 20) -> list[dict]:
async with aiosqlite.connect(DB_PATH) as db:
async with db.execute(
"SELECT id, started_at, ended_at, mode, summary FROM sessions ORDER BY started_at DESC LIMIT ?",
(limit,),
) as cur:
rows = await cur.fetchall()
return [
{
"id": r[0],
"started_at": r[1],
"ended_at": r[2],
"mode": r[3],
"summary": r[4],
}
for r in rows
]
async def get_session_messages(session_id: str) -> list[dict]:
async with aiosqlite.connect(DB_PATH) as db:
async with db.execute(
"SELECT role, content, vocal_emotion, ts FROM messages WHERE session_id=? ORDER BY ts",
(session_id,),
) as cur:
rows = await cur.fetchall()
return [{"role": r[0], "content": r[1], "vocal_emotion": r[2], "ts": r[3]} for r in rows]
async def get_profile(key: str) -> Optional[str]:
async with aiosqlite.connect(DB_PATH) as db:
async with db.execute("SELECT value FROM profile WHERE key=?", (key,)) as cur:
row = await cur.fetchone()
return row[0] if row else None
async def set_profile(key: str, value: str):
async with aiosqlite.connect(DB_PATH) as db:
await db.execute(
"INSERT OR REPLACE INTO profile (key, value, updated_at) VALUES (?,?,?)",
(key, value, time.time()),
)
await db.commit()