Skip to content

Commit 2bee235

Browse files
feat(agent): add decade/genre filters, tune model params, update backlog
Add genre, decade, year_from, year_to filters to SearchLibrary tool and LibraryQuery. Parse decade strings ("90s" -> 1990-1999) programmatically. Tune build_agent() params: temperature 0.3, max_tokens 2048, repeat_penalty 1.1. Add playlist naming guidance and decade strategy to system prompt. Update task-277 ACs and notes with migration details. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a203129 commit 2bee235

File tree

9 files changed

+257
-47
lines changed

9 files changed

+257
-47
lines changed

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@ Always use Context7 MCP when I need library/API documentation, code generation,
2525
- microsoft/playwright
2626
- mrlesk/backlog.md
2727
- nextest-rs/nextest
28+
- proptest-rs/proptest
2829
- serial-ata/lofty-rs
2930
- sharkdp/hyperfine
3031
- taiko2k/tauon
3132
- tailwindlabs/tailwindcss
3233
- tranxuanthang/lrclib
3334
- websites/deno
3435
- websites/last_fm_api
36+
- websites/rs_rig-core
3537
- websites/rs_tauri_2_9_5
3638
- websites/taskfile_dev
3739

backlog/tasks/task-277 - Genius-playlist-creator.md

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ title: Genius playlist creator
44
status: In Progress
55
assignee: []
66
created_date: '2026-02-18 05:58'
7-
updated_date: '2026-04-02 22:34'
7+
updated_date: '2026-04-04 03:49'
88
labels:
99
- feature
1010
- playlists
@@ -14,6 +14,7 @@ labels:
1414
- lastfm
1515
dependencies:
1616
- TASK-308
17+
- TASK-277.1
1718
references:
1819
- docs/genius.md
1920
priority: high
@@ -32,13 +33,13 @@ Design document: `~/.claude/plans/steady-juggling-scroll.md`
3233

3334
## Acceptance Criteria
3435
<!-- AC:BEGIN -->
35-
- [ ] #1 User can enter a natural language prompt to generate a playlist
36-
- [ ] #2 System uses local LLM (Ollama + llama3.2:1b) to interpret prompts and call appropriate tools
37-
- [ ] #3 8 tools available: GetRecentlyPlayed, GetTopArtists, SearchLibrary, GetSimilarTracks, GetSimilarArtists, GetTrackTags, GetTopArtistsByTag, GetTopTracksByCountry
38-
- [ ] #4 Generated playlist only contains tracks that exist in user's local library
39-
- [ ] #5 Graceful degradation: works without Last.fm (local-only tools), works without Ollama (returns setup instructions)
36+
- [x] #1 User can enter a natural language prompt to generate a playlist
37+
- [x] #2 System uses local LLM (Ollama + qwen3.5:9b) to interpret prompts and call appropriate tools
38+
- [x] #3 8 tools available: GetRecentlyPlayed, GetTopArtists, SearchLibrary, GetSimilarTracks, GetSimilarArtists, GetTrackTags, GetTopArtistsByTag, GetTopTracksByCountry
39+
- [x] #4 Generated playlist only contains tracks that exist in user's local library
40+
- [x] #5 Graceful degradation: works without Last.fm (local-only tools), works without Ollama (returns setup instructions)
4041
- [x] #6 Feature-flagged behind `agent` — zero overhead when disabled
41-
- [ ] #7 Onboarding wizard: Ollama check → model download → ready
42+
- [x] #7 Onboarding wizard: Ollama check → model download → ready
4243
- [x] #8 Agent evals pass (tool selection, output format, degradation)
4344
<!-- AC:END -->
4445

@@ -214,6 +215,34 @@ Total: 764 tests pass (762 existing + 2 new)
214215
- Mixed-history request (`make me a chill playlist like what I listened to last Friday`): default prompt used 4 turns; override prompt used 2 turns and treated weak recent-history results as a weighting signal instead of spending extra turns matching them exactly.
215216

216217
2026-04-02: Conclusion from prompt experiments: tighter stop rules materially reduce turn count, but prompt-only business-rule enforcement remains unreliable for cases like seed-artist caps. The most promising direction is to keep LLM-driven discovery/tool routing while moving playlist compilation and policy enforcement into deterministic business logic that scores and filters candidates using empirical evidence (tool source overlap, local genre, Last.fm tags/similarity, last played date, play history, and explicit duplicate-artist caps).
218+
219+
## 2026-04-03: Python-to-Rust migration finalized
220+
221+
Swapped AC#2 model from llama3.2:1b to qwen3.5:9b (matches Python agent). Ported remaining Python logic to Rust via rig:
222+
223+
### prompt.rs
224+
- Added PLAYLIST NAMING section (creative synonyms, not parroting user's words)
225+
- Added decade/era strategy (search_library with decade+genre, parallel get_top_artists_by_tag)
226+
- Documented all search_library filters (keyword, artist, album, genre, decade, year range)
227+
- Clarified search_library(query=...) vs search_library(genre=...) in CRITICAL section
228+
229+
### tools.rs — SearchLibrary expanded
230+
- Added genre, decade, year_from, year_to args + tool definition
231+
- Programmatic `parse_decade()` handles any century ("90s" -> 1990-1999, "1780s" -> 1780-1789)
232+
- 6 new tests: parse_decade (4) + genre/decade filter integration (2)
233+
234+
### db/library.rs — LibraryQuery expanded
235+
- Added genre (LIKE), year_from, year_to fields to LibraryQuery struct
236+
- Added filtering logic in get_all_tracks() SQL builder
237+
238+
### mod.rs — build_agent() tuned
239+
- Temperature: 0.2 -> 0.3 (matches Python)
240+
- max_tokens: 1024 -> 2048 (prevents response truncation)
241+
- Added repeat_penalty: 1.1 (prevents token repetition/gibberish)
242+
- Preamble set via rig's `.preamble()` builder method
243+
244+
### Test coverage
245+
770 tests pass (764 existing + 6 new). No regressions with or without agent feature.
217246
<!-- SECTION:NOTES:END -->
218247

219248
## Definition of Done

backlog/tasks/task-277.1 - Refresh-Genius-chat-style-UI.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
---
22
id: TASK-277.1
33
title: Refresh Genius chat-style UI
4-
status: To Do
4+
status: In Progress
55
assignee: []
66
created_date: '2026-04-04 03:38'
7+
updated_date: '2026-04-04 03:43'
78
labels:
89
- feature
910
- frontend
@@ -14,6 +15,7 @@ documentation:
1415
- docs/genius.md
1516
parent_task_id: TASK-277
1617
priority: medium
18+
ordinal: 2500
1719
---
1820

1921
## Description

backlog/tasks/task-301 - Global-playback-control-shortcuts-stop-after-current-etc.-from-all-views.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
---
22
id: TASK-301
33
title: 'Global playback control shortcuts (stop after current, etc.) from all views'
4-
status: In Progress
4+
status: To Do
55
assignee: []
66
created_date: '2026-03-31 03:36'
7-
updated_date: '2026-03-31 03:38'
7+
updated_date: '2026-04-04 03:43'
88
labels:
99
- enhancement
1010
- ux
1111
- playback
1212
dependencies: []
1313
priority: medium
14-
ordinal: 1500
14+
ordinal: 2859.375
1515
---
1616

1717
## Description

crates/mt-tauri/src/agent/mod.rs

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,21 @@ use crate::lastfm::LastFmClient;
3131
/// Generate a unique playlist name by appending a number if needed.
3232
///
3333
/// If the base name exists, tries "Name (2)", "Name (3)", etc.
34-
fn generate_unique_playlist_name(conn: &rusqlite::Connection, base_name: &str) -> crate::db::DbResult<String> {
34+
fn generate_unique_playlist_name(
35+
conn: &rusqlite::Connection,
36+
base_name: &str,
37+
) -> crate::db::DbResult<String> {
3538
// Check if base name is available
3639
let exists: bool = conn.query_row(
3740
"SELECT EXISTS(SELECT 1 FROM playlists WHERE name = ? LIMIT 1)",
3841
[base_name],
3942
|row| row.get(0),
4043
)?;
41-
44+
4245
if !exists {
4346
return Ok(base_name.to_string());
4447
}
45-
48+
4649
// Find an available suffix
4750
for i in 2..=100 {
4851
let candidate = format!("{} ({})", base_name, i);
@@ -55,7 +58,7 @@ fn generate_unique_playlist_name(conn: &rusqlite::Connection, base_name: &str) -
5558
return Ok(candidate);
5659
}
5760
}
58-
61+
5962
// Fallback: append timestamp if all numbers are taken
6063
use std::time::{SystemTime, UNIX_EPOCH};
6164
let timestamp = SystemTime::now()
@@ -79,10 +82,7 @@ fn shuffle_spread_artists(tracks: &[(i64, String)]) -> Vec<i64> {
7982
let mut artist_keys: HashMap<i64, &str> = HashMap::new();
8083

8184
for (id, artist) in tracks {
82-
by_artist
83-
.entry(artist.as_str())
84-
.or_default()
85-
.push(*id);
85+
by_artist.entry(artist.as_str()).or_default().push(*id);
8686
artist_keys.insert(*id, artist.as_str());
8787
}
8888

@@ -189,10 +189,11 @@ pub(crate) fn build_agent(
189189
client
190190
.agent(DEFAULT_MODEL)
191191
.preamble(&system_prompt)
192-
.temperature(0.2)
193-
.max_tokens(1024)
192+
.temperature(0.3)
193+
.max_tokens(2048)
194194
.additional_params(serde_json::json!({
195195
"top_p": 0.9,
196+
"repeat_penalty": 1.1,
196197
}))
197198
.tool(GetRecentlyPlayed {
198199
ctx: Arc::clone(&ctx),
@@ -323,7 +324,9 @@ pub async fn agent_generate_playlist(
323324
.with_conn(|conn| {
324325
let mut results = Vec::new();
325326
for id in &parsed.track_ids {
326-
let mut stmt = conn.prepare("SELECT id, artist FROM library WHERE id = ? AND missing = 0 LIMIT 1")?;
327+
let mut stmt = conn.prepare(
328+
"SELECT id, artist FROM library WHERE id = ? AND missing = 0 LIMIT 1",
329+
)?;
327330
let mut rows = stmt.query([id])?;
328331
if let Some(row) = rows.next()? {
329332
let track_id: i64 = row.get(0)?;
@@ -337,7 +340,7 @@ pub async fn agent_generate_playlist(
337340

338341
if track_details.is_empty() {
339342
return Ok(AgentResponse::error(
340-
"None of the selected tracks exist in your library".to_string()
343+
"None of the selected tracks exist in your library".to_string(),
341344
));
342345
}
343346

@@ -355,7 +358,7 @@ pub async fn agent_generate_playlist(
355358
let playlist_name: String = db
356359
.with_conn(|conn| generate_unique_playlist_name(conn, &parsed.name))
357360
.map_err(|e: crate::db::DbError| format!("Failed to generate playlist name: {e}"))?;
358-
361+
359362
let playlist = db
360363
.with_conn(|conn| playlists::create_playlist(conn, &playlist_name))
361364
.map_err(|e| format!("Failed to create playlist: {e}"))?;
@@ -371,9 +374,7 @@ pub async fn agent_generate_playlist(
371374
};
372375

373376
let added = db
374-
.with_conn(|conn| {
375-
playlists::add_tracks_to_playlist(conn, playlist.id, &shuffled_ids, None)
376-
})
377+
.with_conn(|conn| playlists::add_tracks_to_playlist(conn, playlist.id, &shuffled_ids, None))
377378
.map_err(|e| format!("Failed to add tracks to playlist: {e}"))?;
378379

379380
info!(
@@ -694,23 +695,23 @@ mod tests {
694695
#[test]
695696
fn unique_name_appends_number_when_exists() {
696697
let db = Database::new_in_memory().expect("in-memory db");
697-
698+
698699
// Create first playlist
699700
db.with_conn(|conn| playlists::create_playlist(conn, "Chill Vibes"))
700701
.expect("create first")
701702
.expect("first playlist created");
702-
703+
703704
// Second should get (2)
704705
let name2 = db
705706
.with_conn(|conn| generate_unique_playlist_name(conn, "Chill Vibes"))
706707
.expect("generate name2");
707708
assert_eq!(name2, "Chill Vibes (2)");
708-
709+
709710
// Create second playlist
710711
db.with_conn(|conn| playlists::create_playlist(conn, &name2))
711712
.expect("create second")
712713
.expect("second playlist created");
713-
714+
714715
// Third should get (3)
715716
let name3 = db
716717
.with_conn(|conn| generate_unique_playlist_name(conn, "Chill Vibes"))

crates/mt-tauri/src/agent/prompt.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,23 +35,35 @@ STRATEGY — pick the approach that fits the request:
3535
- Artist-based requests ("similar to Radiohead", "like Bjork"):
3636
Call get_similar_artists AND search_library(artist=...) in parallel on the first turn.
3737
Then use get_similar_tracks on seed tracks to expand.
38+
- Decade/era requests ("90s rock", "80s pop", "2000s indie"):
39+
Call search_library with decade="90s" AND genre="rock" (or relevant genre) with limit=50.
40+
Also call get_top_artists_by_tag with era-appropriate genre tags IN PARALLEL.
41+
The library has year metadata on most tracks — USE IT instead of guessing artist names.
3842
- General/mixed requests:
3943
Use get_recently_played or get_top_artists to understand listening habits, then combine
4044
with get_similar_tracks, get_similar_artists, or get_top_artists_by_tag.
4145
- Regional requests ("Japanese music", "Brazilian"):
4246
Use get_top_tracks_by_country with limit=50.
43-
- search_library is ONLY for: exact artist names, exact album names, or specific song titles. NEVER for mood keywords.
47+
- search_library supports keyword, artist, album, genre, decade, and year range filters.
48+
- search_library is ONLY for: exact artist names, exact album names, specific song titles, genre/decade filtering. NEVER for mood keywords.
4449
- Use get_track_tags to understand a track's mood/genre before expanding with get_top_artists_by_tag.
4550
4651
CRITICAL: Avoid these common mistakes:
4752
- NEVER call search_library with mood words like "chill", "relax", "calm", "soft", "dream", "slow" — this matches titles containing those words, not actual chill music
48-
- NEVER call search_library with genre words like "ambient", "electronic", "indie" — use get_top_artists_by_tag instead
53+
- NEVER call search_library(query=...) with genre words like "ambient", "electronic", "indie" — use get_top_artists_by_tag or search_library(genre=...) instead
4954
- If get_top_artists_by_tag returns 0 matches, try related tags (e.g., "ambient" -> "chillout", "electronic" -> "electronica") rather than falling back to search_library
5055
5156
RESPONSE FORMAT (final answer only):
5257
Playlist: [descriptive name]
5358
Tracks: [comma-separated track IDs]
5459
60+
PLAYLIST NAMING:
61+
- Use a creative synonym or evocative phrase, not the user's exact words
62+
- "chill" -> "Midnight Drift", "Velvet Haze", "Slow Burn Frequencies"
63+
- "upbeat" -> "Solar Flare", "Electric Momentum", "Daybreak Drive"
64+
- "sad" -> "Rain on Glass", "Quiet Ache", "Blue Hour Confessions"
65+
- Capture the FEELING, don't parrot the request
66+
5567
Only include track IDs you received from tool results. Never invent IDs."#
5668
)
5769
}

0 commit comments

Comments
 (0)