feat(tuning): scoring weights → DB-backed admin tuning lab
The recommendation scoring knobs move out of YAML (radio profile) and out of the systemMixWeights hard-code (daily_mix profile) into DB-backed settings with live effect (#1250) — the defaults-discovery lab per decision #1247: the operator turns knobs to find good values, which then get baked back into shipped defaults; end users and other operators should never need the card. - Migration 0040: recommendation_weight_profiles (radio / daily_mix, 8 weight columns), taste_tuning singleton (engagement half-life + completion-curve points), recommendation_tuning_audit (one row per change with a {field, old, new} diff — the trend view's markers, #1251). - internal/recsettings: boot reconcile seeds shipped defaults without clobbering tuned rows (coverart SettingsService pattern), validates patches (bounds, curve ordering), writes audit rows, and pushes daily_mix weights + taste config into package playlists. No-op patches write no audit row. - playlists gains SetSystemMixWeights / SetTasteConfig swap points under a RWMutex — no signature threading through the producers; the scheduler's taste rebuild reads the pushed config. - Radio reads its weight profile from the service per request; the 8 weight fields leave config.RecommendationConfig (YAML keeps only RecentlyPlayedHours / RadioSize / RadioSizeMax). - Admin API: GET/PATCH/reset under /api/admin/recommendation-tuning, echoing current + shipped values. - Web: new admin Tuning tab — two weight profiles side by side, taste card, per-scope save (changed fields only) + reset, deviation dots against shipped defaults. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TsF3cNoKrqCYsU78cXC8U6
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { api } from './client';
|
||||
|
||||
// Mirrors internal/api/admin_recommendation_tuning.go (#1250): the
|
||||
// recommendation tuning lab. Field names are the server's snake_case
|
||||
// keys, used verbatim in PATCH bodies.
|
||||
|
||||
export type WeightProfile = {
|
||||
base_weight: number;
|
||||
like_boost: number;
|
||||
recency_weight: number;
|
||||
skip_penalty: number;
|
||||
jitter_magnitude: number;
|
||||
context_weight: number;
|
||||
similarity_weight: number;
|
||||
taste_weight: number;
|
||||
};
|
||||
|
||||
export type TasteTuning = {
|
||||
half_life_days: number;
|
||||
engagement_hard_skip: number;
|
||||
engagement_neutral: number;
|
||||
engagement_full: number;
|
||||
};
|
||||
|
||||
export type TuningScope = 'radio' | 'daily_mix' | 'taste';
|
||||
|
||||
export type TuningSnapshot = {
|
||||
profiles: Record<'radio' | 'daily_mix', WeightProfile>;
|
||||
taste: TasteTuning;
|
||||
shipped: {
|
||||
profiles: Record<'radio' | 'daily_mix', WeightProfile>;
|
||||
taste: TasteTuning;
|
||||
};
|
||||
};
|
||||
|
||||
export function getTuning(): Promise<TuningSnapshot> {
|
||||
return api.get<TuningSnapshot>('/api/admin/recommendation-tuning');
|
||||
}
|
||||
|
||||
export function patchTuning(
|
||||
scope: TuningScope,
|
||||
values: Record<string, number>
|
||||
): Promise<TuningSnapshot> {
|
||||
return api.patch<TuningSnapshot>(`/api/admin/recommendation-tuning/${scope}`, { values });
|
||||
}
|
||||
|
||||
export function resetTuning(scope: TuningScope): Promise<TuningSnapshot> {
|
||||
return api.post<TuningSnapshot>(`/api/admin/recommendation-tuning/${scope}/reset`, {});
|
||||
}
|
||||
Reference in New Issue
Block a user