diff --git a/lib/music_library/chats/collection_chat.ex b/lib/music_library/chats/collection_chat.ex index 181c2b7b..819c5db5 100644 --- a/lib/music_library/chats/collection_chat.ex +++ b/lib/music_library/chats/collection_chat.ex @@ -8,9 +8,10 @@ defmodule MusicLibrary.Chats.CollectionChat do alias MusicLibrary.Chats.Prompt @impl true - @spec stream_response([map()], String.t(), (String.t() -> any())) :: :ok | {:error, term()} - def stream_response(messages, collection_summary, callback) do - instructions = build_instructions(collection_summary) + @spec stream_response([map()], {String.t(), non_neg_integer()}, (String.t() -> any())) :: + :ok | {:error, term()} + def stream_response(messages, {summary, record_count}, callback) do + instructions = build_instructions(summary, record_count) OpenAI.chat_stream(messages, on_chunk: callback, @@ -19,9 +20,7 @@ defmodule MusicLibrary.Chats.CollectionChat do ) end - defp build_instructions(collection_summary) do - record_count = count_records(collection_summary) - + defp build_instructions(collection_summary, record_count) do Prompt.build(""" Answer questions about the user's music collection. \ Use the provided collection catalog as your primary reference. \ @@ -31,12 +30,4 @@ defmodule MusicLibrary.Chats.CollectionChat do #{collection_summary}\ """) end - - defp count_records(""), do: 0 - - defp count_records(summary) do - summary - |> String.split("\n") - |> length() - end end diff --git a/lib/music_library/collection.ex b/lib/music_library/collection.ex index fa48763c..3e4327f1 100644 --- a/lib/music_library/collection.ex +++ b/lib/music_library/collection.ex @@ -191,7 +191,9 @@ defmodule MusicLibrary.Collection do |> MapSet.new() end - @spec collection_summary() :: String.t() + @max_genres_per_record 3 + + @spec collection_summary() :: {String.t(), non_neg_integer()} def collection_summary do records = from(r in Record, @@ -201,16 +203,31 @@ defmodule MusicLibrary.Collection do ) |> Repo.all() - records - |> Enum.map_join("\n", &format_record_line/1) + record_count = length(records) + + summary = + records + |> Enum.group_by(& &1.musicbrainz_id) + |> Enum.map(fn {_id, group} -> format_group(group) end) + |> Enum.sort() + |> Enum.join("\n") + + {summary, record_count} end - defp format_record_line(record) do + defp format_group(records) do + record = hd(records) artist_names = Record.artist_names(record) - genres = record.genres || [] + formats = records |> Enum.map(& &1.format) |> Enum.uniq() |> Enum.join("/") + + genres = + records + |> Enum.flat_map(&(&1.genres || [])) + |> Enum.uniq() + |> Enum.take(@max_genres_per_record) base = - "#{artist_names} - #{record.title} (#{record.release_date || "Unknown"}, #{record.format}, #{record.type})" + "#{artist_names} - #{record.title} (#{record.release_date || "Unknown"}, #{formats}, #{record.type})" if genres == [] do base diff --git a/test/music_library/chats/collection_chat_test.exs b/test/music_library/chats/collection_chat_test.exs index 26037381..e40e4980 100644 --- a/test/music_library/chats/collection_chat_test.exs +++ b/test/music_library/chats/collection_chat_test.exs @@ -31,7 +31,7 @@ defmodule MusicLibrary.Chats.CollectionChatTest do summary = "Radiohead - OK Computer (1997-06-16, cd, album) [alternative rock, art rock]\nPink Floyd - The Wall (1979-11-30, vinyl, album) [progressive rock]" - assert :ok = CollectionChat.stream_response([], summary, fn _chunk -> :ok end) + assert :ok = CollectionChat.stream_response([], {summary, 2}, fn _chunk -> :ok end) assert_receive {:captured_instructions, instructions} assert instructions =~ summary @@ -43,7 +43,7 @@ defmodule MusicLibrary.Chats.CollectionChatTest do test "stream_response handles empty collection" do stub_and_capture_instructions(self()) - assert :ok = CollectionChat.stream_response([], "", fn _chunk -> :ok end) + assert :ok = CollectionChat.stream_response([], {"", 0}, fn _chunk -> :ok end) assert_receive {:captured_instructions, instructions} assert instructions =~ "0 records" diff --git a/test/music_library/collection_test.exs b/test/music_library/collection_test.exs index 5eb12aaf..f34712e4 100644 --- a/test/music_library/collection_test.exs +++ b/test/music_library/collection_test.exs @@ -298,11 +298,11 @@ defmodule MusicLibrary.CollectionTest do end describe "collection_summary/0" do - test "returns empty string when collection is empty" do - assert Collection.collection_summary() == "" + test "returns empty string and zero count when collection is empty" do + assert Collection.collection_summary() == {"", 0} end - test "returns one line per collected record" do + test "returns one line per collected record with record count" do record_with_artist("Radiohead", %{ title: "OK Computer", format: :cd, @@ -321,9 +321,10 @@ defmodule MusicLibrary.CollectionTest do purchased_at: ~U[2024-01-02 00:00:00Z] }) - summary = Collection.collection_summary() + {summary, record_count} = Collection.collection_summary() lines = String.split(summary, "\n") + assert record_count == 2 assert length(lines) == 2 assert Enum.any?(lines, fn line -> @@ -339,6 +340,54 @@ defmodule MusicLibrary.CollectionTest do end) end + test "deduplicates records with same musicbrainz_id and merges formats" do + mbid = Ecto.UUID.generate() + + record_with_artist("AC/DC", %{ + title: "Highway to Hell", + format: :cd, + type: :album, + genres: ["hard rock", "rock"], + release_date: "1979-07-27", + musicbrainz_id: mbid, + purchased_at: ~U[2024-01-01 00:00:00Z] + }) + + record_with_artist("AC/DC", %{ + title: "Highway to Hell", + format: :vinyl, + type: :album, + genres: ["hard rock", "rock"], + release_date: "1979-07-27", + musicbrainz_id: mbid, + purchased_at: ~U[2024-01-02 00:00:00Z] + }) + + {summary, record_count} = Collection.collection_summary() + lines = String.split(summary, "\n") + + assert record_count == 2 + assert length(lines) == 1 + assert hd(lines) =~ "cd/vinyl" + end + + test "caps genres to 3 per record" do + record_with_artist("ABC", %{ + title: "The Lexicon of Love", + format: :vinyl, + type: :album, + genres: ~w[dance dance-pop disco electronic new-wave pop synth-pop], + release_date: "1982-06-21", + purchased_at: ~U[2024-01-01 00:00:00Z] + }) + + {summary, _count} = Collection.collection_summary() + [genres_str] = Regex.run(~r/\[(.+)\]/, summary, capture: :all_but_first) + genre_count = genres_str |> String.split(", ") |> length() + + assert genre_count == 3 + end + test "omits genre brackets when genres are empty" do record_with_artist("Radiohead", %{ title: "OK Computer", @@ -349,7 +398,7 @@ defmodule MusicLibrary.CollectionTest do purchased_at: ~U[2024-01-01 00:00:00Z] }) - summary = Collection.collection_summary() + {summary, _count} = Collection.collection_summary() refute summary =~ "[" refute summary =~ "]" end @@ -365,7 +414,8 @@ defmodule MusicLibrary.CollectionTest do purchased_at: nil }) - summary = Collection.collection_summary() + {summary, record_count} = Collection.collection_summary() + assert record_count == 1 assert summary =~ "OK Computer" refute summary =~ "Wish You Were Here" end