feat: behind-the-scenes 👍/👎 rating system (fine-tune data collection)

Brian can rate Lyra's outputs as he uses her; each rating is stored as a
(context, content, rating) triple — the shape a future fine-tune / preference
dataset wants, collected passively during real use.

- memory: ratings table + add_rating (upsert: one row per item, re-rating
  replaces), list_ratings, rating_counts
- server: POST /rate, GET /ratings/counts, GET /ratings/export (JSONL download)
- chat UI: subtle 👍/👎 on each assistant reply, captures the prompting message
  as context
- journal/reflection UI: 👍/👎 on each thought
- tests: counts + upsert-replace behavior

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 19:32:27 +00:00
parent 9befe4d403
commit 4f770f2e43
6 changed files with 173 additions and 1 deletions
+50
View File
@@ -92,6 +92,21 @@ CREATE TABLE IF NOT EXISTS journal (
source TEXT
);
CREATE INDEX IF NOT EXISTS idx_journal_created ON journal(created_at);
-- Brian's behind-the-scenes feedback on Lyra's outputs (chat replies, reflections,
-- journal/metacognition). Stored as (context, content, rating) — the shape a future
-- fine-tune / preference dataset wants. One row per rated item (re-rating updates it).
CREATE TABLE IF NOT EXISTS ratings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at TEXT NOT NULL,
kind TEXT NOT NULL, -- chat | reflection | metacognition | journal
rating INTEGER NOT NULL, -- +1 (good / want more) or -1 (off / want less)
content TEXT NOT NULL, -- the rated output
context TEXT, -- what prompted it (e.g. the user message for a chat reply)
ref TEXT, -- optional source id (journal id, session id, ...)
note TEXT
);
CREATE INDEX IF NOT EXISTS idx_ratings_created ON ratings(created_at);
"""
_conn: sqlite3.Connection | None = None
@@ -542,6 +557,41 @@ def add_journal_entry(kind: str, content: str, source: str | None = None) -> int
return int(cur.lastrowid)
def add_rating(kind: str, rating: int, content: str, context: str | None = None,
ref: str | None = None, note: str | None = None) -> int:
"""Record (or replace) Brian's feedback on one Lyra output. One row per item:
re-rating the same content updates it. Returns row id."""
now = datetime.now(timezone.utc).isoformat()
conn = _connection()
with conn:
conn.execute("DELETE FROM ratings WHERE kind = ? AND content = ?", (kind, content))
cur = conn.execute(
"INSERT INTO ratings (created_at, kind, rating, content, context, ref, note) "
"VALUES (?, ?, ?, ?, ?, ?, ?)",
(now, kind, 1 if rating >= 0 else -1, content, context,
str(ref) if ref is not None else None, note),
)
return int(cur.lastrowid)
def list_ratings(limit: int | None = None) -> list[dict]:
conn = _connection()
sql = "SELECT id, created_at, kind, rating, content, context, ref, note FROM ratings ORDER BY id DESC"
if limit is not None:
sql += f" LIMIT {int(limit)}"
return [dict(r) for r in conn.execute(sql).fetchall()]
def rating_counts() -> dict:
conn = _connection()
r = conn.execute(
"SELECT COUNT(*) AS total, "
"COALESCE(SUM(CASE WHEN rating > 0 THEN 1 ELSE 0 END), 0) AS up, "
"COALESCE(SUM(CASE WHEN rating < 0 THEN 1 ELSE 0 END), 0) AS down FROM ratings"
).fetchone()
return {"total": r["total"], "up": r["up"], "down": r["down"]}
def list_journal(limit: int | None = None, kinds: tuple[str, ...] | None = None) -> list[dict]:
"""Journal entries, newest first. Optionally filter by kind."""
conn = _connection()