diff --git a/lib/music_library/application.ex b/lib/music_library/application.ex index dabb4c8c..428a6586 100644 --- a/lib/music_library/application.ex +++ b/lib/music_library/application.ex @@ -23,6 +23,7 @@ defmodule MusicLibrary.Application do {Oban, Application.fetch_env!(:music_library, Oban)}, {Ecto.Migrator, repos: Application.fetch_env!(:music_library, :ecto_repos), skip: skip_migrations?()}, + {Task.Supervisor, name: MusicLibrary.TaskSupervisor}, {Phoenix.PubSub, name: MusicLibrary.PubSub}, {LastFm.Supervisor, LastFm.Config.resolve(:music_library)}, # Start a worker by calling: MusicLibrary.Worker.start_link(arg) diff --git a/lib/music_library/record_chat.ex b/lib/music_library/record_chat.ex index e362ed9a..8385db0a 100644 --- a/lib/music_library/record_chat.ex +++ b/lib/music_library/record_chat.ex @@ -1,5 +1,5 @@ defmodule MusicLibrary.RecordChat do - alias MusicLibrary.Records.{Record, Similarity} + alias MusicLibrary.Records.Record def stream_response(messages, record, embedding_text, callback) do system_prompt = build_system_prompt(record, embedding_text) @@ -11,11 +11,7 @@ defmodule MusicLibrary.RecordChat do OpenAI.chat_stream(full_messages, on_chunk: callback) end - def get_embedding_text(record_id) do - Similarity.get_embedding_text(record_id) - end - - def build_system_prompt(%Record{} = record, embedding_text) do + defp build_system_prompt(%Record{} = record, embedding_text) do context = case embedding_text do nil -> basic_context(record) diff --git a/lib/music_library_web/components/record_chat.ex b/lib/music_library_web/components/record_chat.ex index ca30da95..689eb908 100644 --- a/lib/music_library_web/components/record_chat.ex +++ b/lib/music_library_web/components/record_chat.ex @@ -1,6 +1,8 @@ defmodule MusicLibraryWeb.Components.RecordChat do use MusicLibraryWeb, :live_component + require Logger + alias MusicLibrary.RecordChat def open(id), do: Fluxon.open_dialog(id) @@ -12,7 +14,6 @@ defmodule MusicLibraryWeb.Components.RecordChat do |> assign(:messages, []) |> assign(:current_response, "") |> assign(:loading, false) - |> assign(:embedding_text, nil) |> assign(:error, nil)} end @@ -39,21 +40,7 @@ defmodule MusicLibraryWeb.Components.RecordChat do end def update(assigns, socket) do - {:ok, - socket - |> assign(assigns) - |> maybe_load_embedding_text()} - end - - defp maybe_load_embedding_text(socket) do - if socket.assigns.embedding_text == nil and socket.assigns[:record] do - case RecordChat.get_embedding_text(socket.assigns.record.id) do - {:ok, text} -> assign(socket, :embedding_text, text) - {:error, :not_found} -> assign(socket, :embedding_text, nil) - end - else - socket - end + {:ok, assign(socket, assigns)} end @impl true @@ -144,7 +131,12 @@ defmodule MusicLibraryWeb.Components.RecordChat do
-
+ <.input name="message" value="" @@ -188,51 +180,10 @@ defmodule MusicLibraryWeb.Components.RecordChat do end @impl true - def handle_event("send_message", %{"message" => ""}, socket) do - {:noreply, socket} - end + def handle_event("send_message", %{"message" => ""}, socket), do: {:noreply, socket} - def handle_event("send_message", %{"message" => text}, socket) do - parent_pid = self() - component_id = socket.assigns.id - user_message = %{role: "user", content: String.trim(text)} - - messages = socket.assigns.messages ++ [user_message] - - Task.start(fn -> - try do - RecordChat.stream_response( - messages, - socket.assigns.record, - socket.assigns.embedding_text, - fn chunk -> - Phoenix.LiveView.send_update(parent_pid, __MODULE__, - id: component_id, - chunk: chunk - ) - end - ) - - Phoenix.LiveView.send_update(parent_pid, __MODULE__, - id: component_id, - done: true - ) - rescue - e -> - Phoenix.LiveView.send_update(parent_pid, __MODULE__, - id: component_id, - error: Exception.message(e) - ) - end - end) - - {:noreply, - socket - |> assign(:messages, messages) - |> assign(:loading, true) - |> assign(:current_response, "") - |> assign(:error, nil)} - end + def handle_event("send_message", %{"message" => text}, socket), + do: do_send_message(socket, text) def handle_event("clear_chat", _params, socket) do {:noreply, @@ -245,17 +196,55 @@ defmodule MusicLibraryWeb.Components.RecordChat do def handle_event("retry", _params, socket) do case List.last(socket.assigns.messages) do %{role: "user"} = last_message -> - # Remove the last user message and re-send - messages = Enum.drop(socket.assigns.messages, -1) - - socket = assign(socket, :messages, messages) - handle_event("send_message", %{"message" => last_message.content}, socket) + socket + |> assign(:messages, Enum.drop(socket.assigns.messages, -1)) + |> do_send_message(last_message.content) _ -> {:noreply, assign(socket, :error, nil)} end end + defp do_send_message(socket, text) do + parent_pid = self() + component_id = socket.assigns.id + user_message = %{role: "user", content: String.trim(text)} + + messages = socket.assigns.messages ++ [user_message] + record = socket.assigns.record + embedding_text = socket.assigns.embedding_text + + Task.Supervisor.start_child(MusicLibrary.TaskSupervisor, fn -> + case RecordChat.stream_response(messages, record, embedding_text, fn chunk -> + Phoenix.LiveView.send_update(parent_pid, __MODULE__, + id: component_id, + chunk: chunk + ) + end) do + :ok -> + Phoenix.LiveView.send_update(parent_pid, __MODULE__, + id: component_id, + done: true + ) + + {:error, reason} -> + Logger.error("RecordChat streaming error: #{reason}") + + Phoenix.LiveView.send_update(parent_pid, __MODULE__, + id: component_id, + error: gettext("Something went wrong. Please try again.") + ) + end + end) + + {:noreply, + socket + |> assign(:messages, messages) + |> assign(:loading, true) + |> assign(:current_response, "") + |> assign(:error, nil)} + end + defp message_classes("user") do "ml-auto bg-blue-500 dark:bg-blue-600 text-white" end diff --git a/lib/music_library_web/live/collection_live/show.html.heex b/lib/music_library_web/live/collection_live/show.html.heex index 2fed29a7..a8253400 100644 --- a/lib/music_library_web/live/collection_live/show.html.heex +++ b/lib/music_library_web/live/collection_live/show.html.heex @@ -344,6 +344,7 @@ sheet_id="record-chat-sheet" module={MusicLibraryWeb.Components.RecordChat} record={@record} + embedding_text={@embedding_text} /> <.structured_modal diff --git a/lib/music_library_web/live/wishlist_live/show.html.heex b/lib/music_library_web/live/wishlist_live/show.html.heex index 3267f1c9..758d610f 100644 --- a/lib/music_library_web/live/wishlist_live/show.html.heex +++ b/lib/music_library_web/live/wishlist_live/show.html.heex @@ -308,6 +308,7 @@ sheet_id="record-chat-sheet" module={MusicLibraryWeb.Components.RecordChat} record={@record} + embedding_text={nil} /> <.structured_modal diff --git a/lib/open_ai.ex b/lib/open_ai.ex index 27f3f30a..67cb48d7 100644 --- a/lib/open_ai.ex +++ b/lib/open_ai.ex @@ -16,12 +16,15 @@ defmodule OpenAI do {:ok, result} end - def chat_stream(messages, opts \\ []) do + def chat_stream(messages, opts \\ []) when is_list(messages) do model = Keyword.get(opts, :model, "gpt-4o-mini") temperature = Keyword.get(opts, :temperature, 0.7) on_chunk = Keyword.fetch!(opts, :on_chunk) - API.chat_stream(messages, model, temperature, api_key(), on_chunk) + case API.chat_stream(messages, model, temperature, api_key(), on_chunk) do + :ok -> :ok + {:error, _reason} = error -> error + end end def embeddings(text) do diff --git a/lib/open_ai/api.ex b/lib/open_ai/api.ex index 0e20be31..5fba6237 100644 --- a/lib/open_ai/api.ex +++ b/lib/open_ai/api.ex @@ -75,20 +75,29 @@ defmodule OpenAI.API do end end - Req.post!("https://api.openai.com/v1/chat/completions", - receive_timeout: 30_000, - connect_options: [ - timeout: 5_000 - ], - json: %{ - model: model, - messages: messages, - stream: true, - temperature: temperature - }, - auth: {:bearer, api_key}, - finch_request: fun - ) + case Req.post("https://api.openai.com/v1/chat/completions", + receive_timeout: 30_000, + connect_options: [ + timeout: 5_000 + ], + json: %{ + model: model, + messages: messages, + stream: true, + temperature: temperature + }, + auth: {:bearer, api_key}, + finch_request: fun + ) do + {:ok, %{status: status}} when status in 200..299 -> + :ok + + {:ok, %{body: body}} -> + {:error, "OpenAI API error: #{inspect(body)}"} + + {:error, exception} -> + {:error, "Connection error: #{Exception.message(exception)}"} + end end def get_embeddings(text, api_key) do diff --git a/lib/open_ai/completion.ex b/lib/open_ai/completion.ex index e18a8434..842e7795 100644 --- a/lib/open_ai/completion.ex +++ b/lib/open_ai/completion.ex @@ -3,7 +3,5 @@ defmodule OpenAI.Completion do defstruct content: "", temperature: 0.2, role: "user", - model: "gpt-4o-mini", - messages: nil, - response_format: nil + model: "gpt-4o-mini" end diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index ec1595a7..b4d51f74 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -2000,3 +2000,8 @@ msgstr "" #, elixir-autogen, elixir-format msgid "e.g. \"What genre is this?\", \"Tell me about the artists\"" msgstr "" + +#: lib/music_library_web/components/record_chat.ex +#, elixir-autogen, elixir-format +msgid "Something went wrong. Please try again." +msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index 7587173c..f93674d0 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -2000,3 +2000,8 @@ msgstr "" #, elixir-autogen, elixir-format msgid "e.g. \"What genre is this?\", \"Tell me about the artists\"" msgstr "" + +#: lib/music_library_web/components/record_chat.ex +#, elixir-autogen, elixir-format +msgid "Something went wrong. Please try again." +msgstr ""