diff --git a/lib/music_library/chats/collection_chat.ex b/lib/music_library/chats/collection_chat.ex new file mode 100644 index 00000000..638765d2 --- /dev/null +++ b/lib/music_library/chats/collection_chat.ex @@ -0,0 +1,38 @@ +defmodule MusicLibrary.Chats.CollectionChat do + @moduledoc """ + Chat implementation for the music collection using OpenAI streaming with web search. + """ + + @behaviour MusicLibrary.Chats.StreamProvider + + 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) + + OpenAI.chat_stream(messages, on_chunk: callback, instructions: instructions) + end + + defp build_instructions(collection_summary) do + record_count = count_records(collection_summary) + + Prompt.build(""" + Answer questions about the user's music collection. \ + Use the provided collection catalog as your primary reference. \ + The collection contains #{record_count} records. + + Collection catalog: + #{collection_summary}\ + """) + end + + defp count_records(""), do: 0 + + defp count_records(summary) do + summary + |> String.split("\n") + |> length() + end +end diff --git a/test/music_library/chats/collection_chat_test.exs b/test/music_library/chats/collection_chat_test.exs new file mode 100644 index 00000000..26037381 --- /dev/null +++ b/test/music_library/chats/collection_chat_test.exs @@ -0,0 +1,51 @@ +defmodule MusicLibrary.Chats.CollectionChatTest do + use ExUnit.Case + + alias MusicLibrary.Chats.CollectionChat + + defp sse_event(type, data) do + json = JSON.encode!(%{type: type, delta: data}) + "event: #{type}\ndata: #{json}\n\n" + end + + defp completed_response do + sse_event("response.output_text.delta", "Hello") <> + "event: response.completed\ndata: {\"type\":\"response.completed\"}\n\n" + end + + defp stub_and_capture_instructions(test_pid) do + Req.Test.stub(OpenAI.API, fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + request = JSON.decode!(body) + send(test_pid, {:captured_instructions, request["instructions"]}) + + conn + |> Plug.Conn.put_resp_content_type("text/event-stream") + |> Plug.Conn.send_resp(200, completed_response()) + end) + end + + test "stream_response includes collection summary in instructions" do + stub_and_capture_instructions(self()) + + 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_receive {:captured_instructions, instructions} + assert instructions =~ summary + assert instructions =~ "music collection" + assert instructions =~ "collection catalog" + assert instructions =~ "2 records" + end + + test "stream_response handles empty collection" do + stub_and_capture_instructions(self()) + + assert :ok = CollectionChat.stream_response([], "", fn _chunk -> :ok end) + + assert_receive {:captured_instructions, instructions} + assert instructions =~ "0 records" + end +end