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
+26
View File
@@ -142,6 +142,32 @@ def create_app() -> FastAPI:
async def journal_data(limit: int = 300) -> dict:
return {"entries": memory.list_journal(limit=limit)}
@app.post("/rate")
async def rate(request: Request) -> dict:
"""Record Brian's 👍/👎 on a Lyra output (chat reply, reflection, journal)."""
b = await request.json()
rating = int(b.get("rating", 0))
content = (b.get("content") or "").strip()
if not content or rating == 0:
return {"ok": False}
memory.add_rating(
kind=b.get("kind") or "chat", rating=rating, content=content,
context=(b.get("context") or None), ref=b.get("ref"), note=b.get("note"),
)
logbus.log("info", "rating", kind=b.get("kind"), rating=1 if rating >= 0 else -1)
return {"ok": True, "counts": memory.rating_counts()}
@app.get("/ratings/counts")
async def ratings_counts() -> dict:
return memory.rating_counts()
@app.get("/ratings/export")
async def ratings_export() -> Response:
"""All ratings as JSONL — the seed for a future fine-tune / preference set."""
lines = "\n".join(json.dumps(r) for r in memory.list_ratings())
return Response(content=lines + ("\n" if lines else ""), media_type="application/x-ndjson",
headers={"Content-Disposition": 'attachment; filename="lyra_ratings.jsonl"'})
@app.get("/hand/{hand_id}")
async def hand_page(hand_id: int) -> FileResponse:
"""Replayable hand-history viewer."""