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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user