feat(api): radio uses similarity pool with exclude param + M3 fallback
Wire LoadCandidatesFromSimilarity as the primary candidate loader with an ?exclude= query param for comma-separated UUID filtering; fall back to LoadCandidates on error. Thread SimilarityWeight into ScoringWeights and update testHandlers recCfg accordingly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+47
-10
@@ -82,19 +82,33 @@ func (h *handlers) handleRadio(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
currentVec := loadCurrentSessionVector(r, q, user.ID, h.logger)
|
||||
|
||||
candidates, err := recommendation.LoadCandidates(r.Context(), q, user.ID, seedID, h.recCfg.RecentlyPlayedHours, currentVec)
|
||||
exclude := parseExcludeParam(r.URL.Query().Get("exclude"))
|
||||
limits := recommendation.DefaultCandidateSourceLimits()
|
||||
candidates, err := recommendation.LoadCandidatesFromSimilarity(
|
||||
r.Context(), q, user.ID, seedID,
|
||||
h.recCfg.RecentlyPlayedHours, currentVec, exclude, limits,
|
||||
)
|
||||
if err != nil {
|
||||
h.logger.Error("api: radio: load candidates", "err", err)
|
||||
writeErr(w, http.StatusInternalServerError, "server_error", "candidate load failed")
|
||||
return
|
||||
h.logger.Warn("api: radio: similarity-pool failed; falling back to whole-library", "err", err)
|
||||
candidates, err = recommendation.LoadCandidates(
|
||||
r.Context(), q, user.ID, seedID,
|
||||
h.recCfg.RecentlyPlayedHours, currentVec,
|
||||
)
|
||||
if err != nil {
|
||||
h.logger.Error("api: radio: load candidates fallback failed", "err", err)
|
||||
writeErr(w, http.StatusInternalServerError, "server_error", "candidate load failed")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
weights := recommendation.ScoringWeights{
|
||||
BaseWeight: h.recCfg.BaseWeight,
|
||||
LikeBoost: h.recCfg.LikeBoost,
|
||||
RecencyWeight: h.recCfg.RecencyWeight,
|
||||
SkipPenalty: h.recCfg.SkipPenalty,
|
||||
JitterMagnitude: h.recCfg.JitterMagnitude,
|
||||
ContextWeight: h.recCfg.ContextWeight,
|
||||
BaseWeight: h.recCfg.BaseWeight,
|
||||
LikeBoost: h.recCfg.LikeBoost,
|
||||
RecencyWeight: h.recCfg.RecencyWeight,
|
||||
SkipPenalty: h.recCfg.SkipPenalty,
|
||||
JitterMagnitude: h.recCfg.JitterMagnitude,
|
||||
ContextWeight: h.recCfg.ContextWeight,
|
||||
SimilarityWeight: h.recCfg.SimilarityWeight,
|
||||
}
|
||||
picks := recommendation.Shuffle(candidates, weights, time.Now().UTC(), h.rng, limit-1)
|
||||
|
||||
@@ -141,3 +155,26 @@ func loadCurrentSessionVector(r *http.Request, q *dbq.Queries, userID pgtype.UUI
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// parseExcludeParam parses a comma-separated list of UUIDs from the
|
||||
// `exclude` query string, silently dropping malformed entries. Returns
|
||||
// nil for empty or all-malformed input.
|
||||
func parseExcludeParam(raw string) []pgtype.UUID {
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(raw, ",")
|
||||
out := make([]pgtype.UUID, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
id, ok := parseUUID(p)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
out = append(out, id)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user