Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

get_listening_history drops the DJ vibe and now playing when history is sparse

Aperta
#830 1 commento 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
3/5
Tempo stimato
Mezza giornata
Idoneità per principianti
85/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
rust
Ambito
cli

Direzione di ricerca

Start in history_outcome in src/infra/dj/tools.rs, where the early return for sparse history (around lines 1111-1117) throws away the vibe and now-playing fields that were already filled in. Move the result building into a small helper that always returns the same structured keys, and add the extra lines only when they are Some. Run cargo test --no-default-features --features telemetry,tui,mcp-server sparse_history and confirm the new tests run (non-zero count) and pass.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

bug good first issue

What

The MCP tool set_dj_vibe says the vibe "is stored and returned by get_listening_history". docs/mcp-setup.md says the same. Right now that's only true once you have at least 5 qualifying plays. On a fresh install, get_listening_history gives back one plain sentence. It has no vibe, no now playing and no structured data, so the vibe an agent just set seems to vanish.

How to reproduce it (you don't need Spotify, any build with mcp-server works):

  1. Build with the mcp-server feature and connect an MCP client, as described in docs/mcp-setup.md. Use a profile with little or no listening history.
  2. Call set_dj_vibe with {"vibe": "mellow piano"}.
  3. Call get_listening_history.
  4. You get back only: Very little listening history so far (0 qualifying plays in the last 30 days). Ask the user what they feel like instead of inferring it. The vibe isn't there, and the structured payload is missing too.

What should happen: the "ask the user" advice stays, and the answer also carries the vibe and the current track when they're set. The structured payload should have the same keys whether history is sparse or not.

Cause: history_outcome copies now_playing and vibe into the summary, then returns early for sparse history with a plain ToolOutcome::ok sentence that throws both away.

Where to look

  • src/infra/dj/tools.rs, line 192: the set_dj_vibe description makes the promise: "Either way it is stored and returned by get_listening_history, so you can honour it yourself when you queue."
  • src/infra/dj/tools.rs, fn vibe_effect, lines 1070-1089: the same promise again at lines 1076-1077 (ai-dj build, auto-queue off) and 1086-1087 (build without ai-dj).
  • src/infra/dj/tools.rs, async fn history_outcome, lines 1091-1130:
    • lines 1097-1102 load the listens file through spawn_blocking(crate::infra::history::load_listens). This reads the real listens.jsonl in your state dir.
    • lines 1104-1109 build the brief and fill in the steer:
      let mut summary = brief::build_brief(&listens, period);
      {
        let app = app.lock().await;
        summary.now_playing = super::current_track_label(&app);
        summary.vibe = app.dj.vibe.clone();
      }
      
    • lines 1111-1117 are the early return that drops them:
      if summary.is_sparse() {
        return ToolOutcome::ok(format!(
          "Very little listening history so far ({} qualifying plays in {}). Ask the user what they \
           feel like instead of inferring it.",
          summary.total_plays, summary.period_label
        ));
      }
      
    • lines 1119-1129 handle the full path: a json! with period, total_plays, top_artists, top_tracks, top_albums, recent, now_playing, vibe, returned as ToolOutcome::with_data(summary.to_prompt_block(), structured).
  • src/infra/dj/tools.rs, now_playing_outcome, lines 1160-1162: the comment there explains why every branch should use the same payload keys. A missing key gets read as "unknown". Follow the same rule here.
  • src/infra/dj/tools.rs, around line 502: ToolOutcome { text, structured: Option<Value>, is_error }, with the constructors ok and with_data just below it.
  • src/infra/dj/brief.rs, line 25: #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct TasteBrief. It derives Default, so you can build one by hand in a test.
  • src/infra/dj/brief.rs, lines 135-159: to_prompt_block. Lines 152-157 hold the "Currently playing" and "The listener asked for" wording. Reuse that wording.
  • src/infra/dj/brief.rs, line 164: is_sparse is self.total_plays < 5, so every new user takes the sparse branch.
  • docs/mcp-setup.md, lines 253-257: the user-facing promise (key sentence at 256-257). No doc change is needed once the code keeps that promise.

Possible solution

  • Move everything after the lock block into a small sync helper, for example:
    /// The tool answer for a filled-in brief, sparse or not.
    fn history_result(summary: &TasteBrief) -> ToolOutcome {
      let structured = json!({
        "period": summary.period_label,
        "total_plays": summary.total_plays,
        "top_artists": summary.top_artists,
        "top_tracks": summary.top_tracks,
        "top_albums": summary.top_albums,
        "recent": summary.recent,
        "now_playing": summary.now_playing,
        "vibe": summary.vibe,
      });
      if summary.is_sparse() {
        let mut text = format!(
          "Very little listening history so far ({} qualifying plays in {}). Ask the user what they \
           feel like instead of inferring it.",
          summary.total_plays, summary.period_label
        );
        // append "Currently playing: ..." and "The listener asked for: ..." when Some
        return ToolOutcome::with_data(text, structured);
      }
      ToolOutcome::with_data(summary.to_prompt_block(), structured)
    }
    
    Then history_outcome ends with history_result(&summary).
  • Keep the "Ask the user what they feel like" sentence. It's the reason the sparse branch exists: with no history the agent shouldn't guess a taste. Only add the vibe and now-playing lines, and only when they're Some.
  • Build the structured json once and use it in both branches, so a client always sees the same keys. On the sparse path the lists are just empty, which is fine.
  • Use the same wording as to_prompt_block (brief.rs lines 152-157) for the two extra lines so the model sees one consistent phrasing. You can either copy the format strings or pull them into a tiny shared function in brief.rs. Pick whichever keeps the diff smaller.
  • Traps:
    • Don't call history_outcome from a test. It reads the real listens.jsonl from your state dir and needs an Arc<Mutex<App>>. Test the new helper instead.
    • The helper must be called from history_outcome, not only from tests, or the non-test build warns about dead code.
    • period_label is &'static str, so a hand-built brief uses a literal such as "the last 30 days".
    • tools.rs only compiles under dj-core (src/infra/mod.rs gates pub mod dj on it), and mcp-server pulls that in. You don't need an extra #[cfg] on the tests.
    • The in-TUI DJ can call this tool too (its spec row has dj: true). The extra lines then reach that model's prompt as well, which is harmless: it's the listener's own steer.
    • Don't change is_sparse or the threshold of 5.
  • Tests to add, in the existing #[cfg(test)] mod tests at the bottom of src/infra/dj/tools.rs (it already has use super::*;):
    • sparse_history_still_returns_the_vibe: build TasteBrief { period_label: "the last 30 days", total_plays: 0, vibe: Some("mellow piano".into()), ..Default::default() }, call the helper, and assert outcome.text.contains("mellow piano"), outcome.text.contains("Ask the user") and outcome.structured.unwrap()["vibe"] == json!("mellow piano").
    • sparse_history_still_returns_now_playing: same idea with now_playing: Some("Song by Artist".into()).
    • sparse_history_payload_has_the_same_keys_as_full_history: with vibe: None, assert structured["vibe"].is_null() and that top_artists, total_plays and period are present.
    • Optionally full_history_still_uses_the_prompt_block: total_plays: 10 with one top artist, and assert the text equals summary.to_prompt_block().
    • None of these need a Spotify account, network or audio device. The brief is plain data.
  • CI runs clippy on the bin target only, so run cargo test to compile test code (when tests are added).
  • Leave tools/gates.count alone, new tests need no counter change.

Done when

  • cargo test --no-default-features --features telemetry,tui,mcp-server sparse_history runs your new tests and they pass. A filter that matches nothing still exits 0, so check the count.
  • Optional manual check (no Premium needed): run an mcp-server build with a fresh profile, call set_dj_vibe with {"vibe": "mellow piano"}, then get_listening_history, and see the vibe in both the text and the structured result.
  • cargo fmt --all -- --check, cargo clippy --no-default-features --features telemetry,tui -- -D warnings and cargo test --no-default-features --features telemetry,tui pass, plus cargo clippy --no-default-features --features telemetry,tui,mcp-server -- -D warnings and the mcp-server test run above.
Lingua principale
Rust
Stelle
1.4k
Fork
238
Merge medio
9h 21m
PR unite (30g)
193

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di LargeModGames/spotatui

Tutte le issue di LargeModGames/spotatui

Issue simili

Altre issue su Rust

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.