feat(admin): LLM tip quality dashboard — per-model/prompt/kind breakdowns

/admin/reward-analytics now surfaces served count, reaction rate, and avg
reward grouped by llm_model, prompt_version, and tip_kind — closing the
loop so model/prompt iterations in M2 are legible next to the bandit
policy view. Data comes from the tip_scores columns added in ffdf707 and
tip_feedback.reward_milli; bandit-only tips show as "(bandit-only)".

Closes #92.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-24 15:24:52 +00:00
parent 75d0e89906
commit aa4bdd8f09
7 changed files with 227 additions and 9 deletions

View File

@@ -2,11 +2,13 @@
* Creates an isolated in-memory SQLite DB with the full schema applied.
* Use this in tests instead of the shared `db` singleton.
*/
import Database from 'better-sqlite3';
import Database, { type Database as BetterSqlite3Database } from 'better-sqlite3';
import { drizzle } from 'drizzle-orm/better-sqlite3';
import * as schema from '../db/schema.js';
export function makeTestDb() {
type DrizzleDb = ReturnType<typeof drizzle<typeof schema>>;
export function makeTestDb(): DrizzleDb & { rawSqlite: BetterSqlite3Database } {
const sqlite = new Database(':memory:');
sqlite.pragma('foreign_keys = ON');
@@ -138,7 +140,10 @@ export function makeTestDb() {
);
`);
return drizzle(sqlite, { schema });
const db = drizzle(sqlite, { schema });
// `sqlite` is exposed as `rawSqlite` so tests that mock `../db/index.js`
// can provide the same `{ db, rawSqlite }` shape as the prod module.
return Object.assign(db, { rawSqlite: sqlite });
}
export type TestDb = ReturnType<typeof makeTestDb>;