diff --git a/lib/elixir_ai/chat_runner.ex b/lib/elixir_ai/chat_runner.ex index fe26e99..8b2b9f9 100644 --- a/lib/elixir_ai/chat_runner.ex +++ b/lib/elixir_ai/chat_runner.ex @@ -3,10 +3,9 @@ defmodule ElixirAi.ChatRunner do use GenServer import ElixirAi.ChatUtils, only: [ai_tool: 1] alias ElixirAi.{Conversation, Message} + import ElixirAi.PubsubTopics defp via(name), do: {:via, Horde.Registry, {ElixirAi.ChatRegistry, name}} - defp topic(name), do: "ai_chat:#{name}" - def message_topic(name), do: "conversation_messages:#{name}" def new_user_message(name, text_content) do GenServer.cast(via(name), {:user_message, text_content}) @@ -28,7 +27,7 @@ defmodule ElixirAi.ChatRunner do def init(name) do messages = case Conversation.find_id(name) do - {:ok, conv_id} -> Message.load_for_conversation(conv_id, topic: message_topic(name)) + {:ok, conv_id} -> Message.load_for_conversation(conv_id, topic: conversation_message_topic(name)) _ -> [] end @@ -78,7 +77,7 @@ defmodule ElixirAi.ChatRunner do function: fn %{"color" => color} -> Phoenix.PubSub.broadcast( ElixirAi.PubSub, - "ai_chat:#{name}", + chat_topic(name), {:set_background_color, color} ) end, @@ -309,7 +308,7 @@ defmodule ElixirAi.ChatRunner do {:reply, state.streaming_response, state} end - defp broadcast_ui(name, msg), do: Phoenix.PubSub.broadcast(ElixirAi.PubSub, topic(name), msg) + defp broadcast_ui(name, msg), do: Phoenix.PubSub.broadcast(ElixirAi.PubSub, chat_topic(name), msg) defp store_message(name, messages) when is_list(messages) do Enum.each(messages, &store_message(name, &1)) @@ -319,7 +318,7 @@ defmodule ElixirAi.ChatRunner do defp store_message(name, message) do Phoenix.PubSub.broadcast( ElixirAi.PubSub, - message_topic(name), + conversation_message_topic(name), {:store_message, name, message} ) diff --git a/lib/elixir_ai/conversation_manager.ex b/lib/elixir_ai/conversation_manager.ex index 995eb11..8ae6f02 100644 --- a/lib/elixir_ai/conversation_manager.ex +++ b/lib/elixir_ai/conversation_manager.ex @@ -1,6 +1,7 @@ defmodule ElixirAi.ConversationManager do use GenServer alias ElixirAi.{Conversation, Message} + import ElixirAi.PubsubTopics, only: [conversation_message_topic: 1] require Logger @name {:via, Horde.Registry, {ElixirAi.ChatRegistry, __MODULE__}} @@ -117,7 +118,7 @@ defmodule ElixirAi.ConversationManager do def handle_info({:store_message, name, message}, %{conversations: conversations} = state) do case Conversation.find_id(name) do {:ok, conv_id} -> - Message.insert(conv_id, message, topic: ElixirAi.ChatRunner.message_topic(name)) + Message.insert(conv_id, message, topic: conversation_message_topic(name)) _ -> :ok @@ -164,7 +165,7 @@ defmodule ElixirAi.ConversationManager do if MapSet.member?(subscriptions, name) do subscriptions else - Phoenix.PubSub.subscribe(ElixirAi.PubSub, ElixirAi.ChatRunner.message_topic(name)) + Phoenix.PubSub.subscribe(ElixirAi.PubSub, conversation_message_topic(name)) MapSet.put(subscriptions, name) end diff --git a/lib/elixir_ai/data/ai_provider.ex b/lib/elixir_ai/data/ai_provider.ex index 6526a86..f9a5248 100644 --- a/lib/elixir_ai/data/ai_provider.ex +++ b/lib/elixir_ai/data/ai_provider.ex @@ -1,6 +1,7 @@ defmodule ElixirAi.AiProvider do alias ElixirAi.Data.DbHelpers require Logger + import ElixirAi.PubsubTopics defmodule AiProviderSchema do defstruct [:id, :name, :model_name, :api_token, :completions_url] @@ -77,14 +78,14 @@ defmodule ElixirAi.AiProvider do "updated_at" => now } - case DbHelpers.run_sql(sql, params, "ai_providers") do + case DbHelpers.run_sql(sql, params, providers_topic()) do {:error, :db_error} -> {:error, :db_error} _result -> Phoenix.PubSub.broadcast( ElixirAi.PubSub, - "ai_providers", + providers_topic(), {:provider_added, attrs} ) @@ -102,7 +103,7 @@ defmodule ElixirAi.AiProvider do params = %{"name" => name} - case DbHelpers.run_sql(sql, params, "ai_providers", AiProviderSchema.schema()) do + case DbHelpers.run_sql(sql, params, providers_topic(), AiProviderSchema.schema()) do {:error, _} -> {:error, :db_error} [] -> {:error, :not_found} [row | _] -> {:ok, row |> convert_uuid_to_string() |> then(&struct(AiProviderSchema, &1))} @@ -113,7 +114,7 @@ defmodule ElixirAi.AiProvider do sql = "SELECT COUNT(*) FROM ai_providers" params = %{} - case DbHelpers.run_sql(sql, params, "ai_providers") do + case DbHelpers.run_sql(sql, params, providers_topic()) do {:error, :db_error} -> {:error, :db_error} diff --git a/lib/elixir_ai/pubsub_topics.ex b/lib/elixir_ai/pubsub_topics.ex new file mode 100644 index 0000000..1943030 --- /dev/null +++ b/lib/elixir_ai/pubsub_topics.ex @@ -0,0 +1,6 @@ +defmodule ElixirAi.PubsubTopics do + + def conversation_message_topic(name), do: "conversation_messages:#{name}" + def chat_topic(name), do: "ai_chat:#{name}" + def providers_topic, do: "providers" +end diff --git a/lib/elixir_ai_web/live/ai_providers_live.ex b/lib/elixir_ai_web/live/ai_providers_live.ex index 615f64f..8b06f4c 100644 --- a/lib/elixir_ai_web/live/ai_providers_live.ex +++ b/lib/elixir_ai_web/live/ai_providers_live.ex @@ -2,10 +2,11 @@ defmodule ElixirAiWeb.AiProvidersLive do use ElixirAiWeb, :live_component import ElixirAiWeb.FormComponents alias ElixirAi.AiProvider + import ElixirAi.PubsubTopics def update(assigns, socket) do if connected?(socket) do - Phoenix.PubSub.subscribe(ElixirAi.PubSub, "ai_providers") + Phoenix.PubSub.subscribe(ElixirAi.PubSub, providers_topic()) end {:ok, diff --git a/lib/elixir_ai_web/live/chat_live.ex b/lib/elixir_ai_web/live/chat_live.ex index 15a1a08..a3c54e0 100644 --- a/lib/elixir_ai_web/live/chat_live.ex +++ b/lib/elixir_ai_web/live/chat_live.ex @@ -5,6 +5,7 @@ defmodule ElixirAiWeb.ChatLive do import ElixirAiWeb.ChatMessage alias ElixirAi.ChatRunner alias ElixirAi.ConversationManager + import ElixirAi.PubsubTopics def valid_background_colors do [ @@ -21,8 +22,10 @@ defmodule ElixirAiWeb.ChatLive do def mount(%{"name" => name}, _session, socket) do case ConversationManager.open_conversation(name) do {:ok, _pid} -> - if connected?(socket), - do: Phoenix.PubSub.subscribe(ElixirAi.PubSub, "ai_chat:#{name}") + if connected?(socket) do + Phoenix.PubSub.subscribe(ElixirAi.PubSub, chat_topic(name)) + Phoenix.PubSub.subscribe(ElixirAi.PubSub, conversation_message_topic(name)) + end conversation = ChatRunner.get_conversation(name) @@ -32,7 +35,8 @@ defmodule ElixirAiWeb.ChatLive do |> assign(user_input: "") |> assign(messages: conversation.messages) |> assign(streaming_response: conversation.streaming_response) - |> assign(background_color: "bg-cyan-950/30")} + |> assign(background_color: "bg-cyan-950/30") + |> assign(db_error: nil)} {:error, :not_found} -> {:ok, push_navigate(socket, to: "/")} @@ -48,6 +52,11 @@ defmodule ElixirAiWeb.ChatLive do {@conversation_name} + <%= if @db_error do %> + + <% end %>
assign(streaming_response: nil)} end + def handle_info({:db_error, reason}, socket) do + {:noreply, assign(socket, db_error: reason)} + end + def handle_info({:set_background_color, color}, socket) do Logger.info("setting background color to #{color}") {:noreply, assign(socket, background_color: color)} diff --git a/lib/elixir_ai_web/live/home_live.ex b/lib/elixir_ai_web/live/home_live.ex index 237fb20..64a283b 100644 --- a/lib/elixir_ai_web/live/home_live.ex +++ b/lib/elixir_ai_web/live/home_live.ex @@ -3,10 +3,11 @@ defmodule ElixirAiWeb.HomeLive do import ElixirAiWeb.FormComponents alias ElixirAi.{ConversationManager, AiProvider} require Logger + import ElixirAi.PubsubTopics def mount(_params, _session, socket) do if connected?(socket) do - Phoenix.PubSub.subscribe(ElixirAi.PubSub, "ai_providers") + Phoenix.PubSub.subscribe(ElixirAi.PubSub, providers_topic()) send(self(), :load_data) end diff --git a/test/elixir_ai_web/live/chat_live_test.exs b/test/elixir_ai_web/live/chat_live_test.exs new file mode 100644 index 0000000..59b3520 --- /dev/null +++ b/test/elixir_ai_web/live/chat_live_test.exs @@ -0,0 +1,26 @@ +defmodule ElixirAiWeb.ChatLiveTest do + use ElixirAiWeb.ConnCase, async: false + import ElixirAi.PubsubTopics, only: [conversation_message_topic: 1] + + setup do + stub(ElixirAi.ConversationManager, :open_conversation, fn _name -> {:ok, self()} end) + + stub(ElixirAi.ChatRunner, :get_conversation, fn _name -> + %{messages: [], streaming_response: nil} + end) + + :ok + end + + test "displays a db error when a db_error message is broadcast", %{conn: conn} do + {:ok, view, _html} = live(conn, ~p"/chat/test_conv") + + Phoenix.PubSub.broadcast( + ElixirAi.PubSub, + conversation_message_topic("test_conv"), + {:db_error, "unique constraint violated"} + ) + + assert render(view) =~ "unique constraint violated" + end +end diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 3087079..a621f33 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -16,6 +16,7 @@ defmodule ElixirAiWeb.ConnCase do """ use ExUnit.CaseTemplate + use Mimic using do quote do @@ -27,11 +28,18 @@ defmodule ElixirAiWeb.ConnCase do # Import conveniences for testing with connections import Plug.Conn import Phoenix.ConnTest + import Phoenix.LiveViewTest import ElixirAiWeb.ConnCase + use Mimic end end + setup :set_mimic_global + setup _tags do + stub(ElixirAi.Data.DbHelpers, :run_sql, fn _sql, _params, _topic -> [] end) + stub(ElixirAi.Data.DbHelpers, :run_sql, fn _sql, _params, _topic, _schema -> [] end) + stub(ElixirAi.ChatUtils, :request_ai_response, fn _server, _messages, _tools -> :ok end) {:ok, conn: Phoenix.ConnTest.build_conn()} end end diff --git a/test/test_helper.exs b/test/test_helper.exs index 6aaf218..91cb7e0 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,3 +1,5 @@ ExUnit.start() Mimic.copy(ElixirAi.Data.DbHelpers) Mimic.copy(ElixirAi.ChatUtils) +Mimic.copy(ElixirAi.ConversationManager) +Mimic.copy(ElixirAi.ChatRunner)