From df8e44da146ac93c7bb1a7ab8deaf3dda2da3aa3 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Mon, 4 Mar 2024 09:40:15 -0500 Subject: [PATCH 01/99] Parser module and Bubble parsing --- lib/chat_bots/chats/bubble.ex | 3 +++ lib/chat_bots/parser.ex | 19 +++++++++++++++++++ test/chat_bots/parser_test.exs | 25 +++++++++++++++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 lib/chat_bots/chats/bubble.ex create mode 100644 lib/chat_bots/parser.ex create mode 100644 test/chat_bots/parser_test.exs diff --git a/lib/chat_bots/chats/bubble.ex b/lib/chat_bots/chats/bubble.ex new file mode 100644 index 0000000..8a06465 --- /dev/null +++ b/lib/chat_bots/chats/bubble.ex @@ -0,0 +1,3 @@ +defmodule ChatBots.Chats.Bubble do + defstruct [:type, :text] +end diff --git a/lib/chat_bots/parser.ex b/lib/chat_bots/parser.ex new file mode 100644 index 0000000..6e98921 --- /dev/null +++ b/lib/chat_bots/parser.ex @@ -0,0 +1,19 @@ +defmodule ChatBots.Parser do + @moduledoc """ + Parses messages from the chat API into chat items to be displayed in the chat window. + """ + alias ChatBots.Chats.Bubble + + @doc """ + """ + def parse(%{content: content}) do + # if valid JSON, parse the JSON + content = + case Jason.decode(content) do + {:ok, decoded} -> decoded["response"] + {:error, _} -> content + end + + %Bubble{type: "bot", text: content} + end +end diff --git a/test/chat_bots/parser_test.exs b/test/chat_bots/parser_test.exs new file mode 100644 index 0000000..0d7c5d3 --- /dev/null +++ b/test/chat_bots/parser_test.exs @@ -0,0 +1,25 @@ +defmodule ChatBots.ParserTest do + use ChatBots.DataCase, async: true + + alias ChatBots.Chats.Bubble + alias ChatBots.Chats.Message + alias ChatBots.Parser + + test "parses a message from a text response" do + response = %{ + role: "assistant", + content: "Hello, world!" + } + + assert %Bubble{type: "bot", text: "Hello, world!"} = Parser.parse(response) + end + + test "parses a message from a JSON response" do + response = %Message{ + role: "assistant", + content: "{\n \"response\": \"Hello, world!\"\n}" + } + + assert %Bubble{type: "bot", text: "Hello, world!"} = Parser.parse(response) + end +end From 14c0ed0638697f14770836a2ce9a9beaca19d2cf Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Tue, 5 Mar 2024 09:43:46 -0500 Subject: [PATCH 02/99] Refactor ChatLive to use Bubbles --- lib/chat_bots/application.ex | 2 -- lib/chat_bots/parser.ex | 4 +-- lib/chat_bots_web/live/chat_live.ex | 48 ++++++++++++++++------------- 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/lib/chat_bots/application.ex b/lib/chat_bots/application.ex index 28035cb..a7e01a5 100644 --- a/lib/chat_bots/application.ex +++ b/lib/chat_bots/application.ex @@ -28,8 +28,6 @@ defmodule ChatBots.Application do opts = [strategy: :one_for_one, name: ChatBots.Supervisor] result = Supervisor.start_link(children, opts) - # Temporarily reset the database on start during development - ChatBots.Seeder.reset() result end diff --git a/lib/chat_bots/parser.ex b/lib/chat_bots/parser.ex index 6e98921..4ff7324 100644 --- a/lib/chat_bots/parser.ex +++ b/lib/chat_bots/parser.ex @@ -10,8 +10,8 @@ defmodule ChatBots.Parser do # if valid JSON, parse the JSON content = case Jason.decode(content) do - {:ok, decoded} -> decoded["response"] - {:error, _} -> content + {:ok, %{"response" => response}} -> response + {_, _} -> content end %Bubble{type: "bot", text: content} diff --git a/lib/chat_bots_web/live/chat_live.ex b/lib/chat_bots_web/live/chat_live.ex index 3cec008..700e786 100644 --- a/lib/chat_bots_web/live/chat_live.ex +++ b/lib/chat_bots_web/live/chat_live.ex @@ -2,19 +2,21 @@ defmodule ChatBotsWeb.ChatLive do use ChatBotsWeb, :live_view alias ChatBots.Bots alias ChatBots.Chats + alias ChatBots.Chats.Bubble + alias ChatBots.Parser def mount(_params, _session, socket) do bots = Bots.list_bots() bot = hd(bots) chat = Chats.new_chat(bot.id) - messages = [%{role: "info", content: "#{bot.name} has entered the chat"}] + chat_items = [%Bubble{type: "info", text: "#{bot.name} has entered the chat"}] socket = socket |> assign(:bots, bots) |> assign(:bot, bot) |> assign(:chat, chat) - |> assign(:messages, messages) + |> assign(:chat_items, chat_items) |> assign(:loading, false) {:ok, socket} @@ -23,13 +25,13 @@ defmodule ChatBotsWeb.ChatLive do def handle_event("select_bot", %{"bot_id" => bot_id}, socket) do bot = Bots.get_bot(bot_id) chat = Chats.new_chat(bot.id) - messages = [%{role: "info", content: "#{bot.name} has entered the chat"}] + chat_items = [%{type: "info", text: "#{bot.name} has entered the chat"}] socket = socket |> assign(:bot, bot) |> assign(:chat, chat) - |> assign(:messages, messages) + |> assign(:chat_items, chat_items) {:noreply, socket} end @@ -38,11 +40,11 @@ defmodule ChatBotsWeb.ChatLive do # send a message to self to trigger the API call in the background send(self(), {:send_message, message_text}) - # add user message to messages - user_message = %{role: "user", content: message_text} - messages = socket.assigns.messages ++ [user_message] + # add user message to chat_items + user_message = %Bubble{type: "user", text: message_text} + chat_items = socket.assigns.chat_items ++ [user_message] - socket = assign(socket, messages: messages, loading: true) + socket = assign(socket, chat_items: chat_items, loading: true) {:noreply, socket} end @@ -50,13 +52,15 @@ defmodule ChatBotsWeb.ChatLive do socket = case ChatBots.ChatApi.send_message(socket.assigns.chat, message_text) do {:ok, chat} -> - new_message = chat.messages |> List.last() - messages = socket.assigns.messages ++ [new_message] - assign(socket, chat: chat, messages: messages, loading: false) + bubble = chat.messages |> List.last() |> Parser.parse() + chat_items = socket.assigns.chat_items ++ [bubble] + assign(socket, chat: chat, chat_items: chat_items, loading: false) {:error, error} -> - messages = socket.assigns.messages ++ [%{role: "error", content: error["message"]}] - assign(socket, messages: messages, loading: false) + chat_items = + socket.assigns.chat_items ++ [%Bubble{type: "error", text: error["message"]}] + + assign(socket, chat_items: chat_items, loading: false) end {:noreply, socket} @@ -76,11 +80,11 @@ defmodule ChatBotsWeb.ChatLive do <%= options_for_select(bot_options(@bots), @bot.id) %> - +
- <%= for message <- @messages do %> - <%= for line <- String.split(message.content, "\n\n") do %> - <.message_bubble role={message.role} message_text={line} /> + <%= for chat_item <- @chat_items do %> + <%= for line <- String.split(chat_item.text, "\n\n") do %> + <.message_bubble type={chat_item.type} text={line} /> <% end %> <% end %>
@@ -110,22 +114,22 @@ defmodule ChatBotsWeb.ChatLive do """ end - defp message_bubble(%{role: "error"} = assigns) do + defp message_bubble(%{type: "error"} = assigns) do ~H""" -

Error: <%= @message_text %>

+

Error: <%= @text %>

""" end defp message_bubble(assigns) do ~H""" -

<%= @message_text %>

+

<%= @text %>

""" end - defp get_message_classes(role) do + defp get_message_classes(type) do base_classes = "p-2 my-2 rounded-lg text-sm w-auto max-w-md" - case role do + case type do "user" -> "#{base_classes} user-bubble text-white bg-blue-500 self-end" From 2c9492df796442ceb227e840d8ac71300ebea81f Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Thu, 7 Mar 2024 09:29:49 -0500 Subject: [PATCH 03/99] Parser parses Images, and improved Bubbles --- lib/chat_bots/parser.ex | 27 ++++++++++++++++++++------- test/chat_bots/parser_test.exs | 27 ++++++++++++++++++++++----- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/lib/chat_bots/parser.ex b/lib/chat_bots/parser.ex index 4ff7324..f574022 100644 --- a/lib/chat_bots/parser.ex +++ b/lib/chat_bots/parser.ex @@ -3,17 +3,30 @@ defmodule ChatBots.Parser do Parses messages from the chat API into chat items to be displayed in the chat window. """ alias ChatBots.Chats.Bubble + alias ChatBots.Chats.Image @doc """ """ def parse(%{content: content}) do - # if valid JSON, parse the JSON - content = - case Jason.decode(content) do - {:ok, %{"response" => response}} -> response - {_, _} -> content - end + case Jason.decode(content) do + {:ok, content_map} -> gather_chat_items(content_map) + {_, _} -> [parse_chat_item(content)] + end + end + + defp gather_chat_items(content_map) do + content_map + |> Map.to_list() + |> Enum.map(&parse_chat_item/1) + end + + defp parse_chat_item({"text", response}), do: parse_chat_item(response) + + defp parse_chat_item({"image_prompt", prompt}) do + %Image{prompt: prompt} + end - %Bubble{type: "bot", text: content} + defp parse_chat_item(response) do + %Bubble{type: "bot", text: response} end end diff --git a/test/chat_bots/parser_test.exs b/test/chat_bots/parser_test.exs index 0d7c5d3..dc95edf 100644 --- a/test/chat_bots/parser_test.exs +++ b/test/chat_bots/parser_test.exs @@ -2,6 +2,7 @@ defmodule ChatBots.ParserTest do use ChatBots.DataCase, async: true alias ChatBots.Chats.Bubble + alias ChatBots.Chats.Image alias ChatBots.Chats.Message alias ChatBots.Parser @@ -11,15 +12,31 @@ defmodule ChatBots.ParserTest do content: "Hello, world!" } - assert %Bubble{type: "bot", text: "Hello, world!"} = Parser.parse(response) + assert [%Bubble{type: "bot", text: "Hello, world!"}] = Parser.parse(response) end test "parses a message from a JSON response" do - response = %Message{ + response = make_json_message(%{text: "Hello, world!"}) + + assert [%Bubble{type: "bot", text: "Hello, world!"}] = Parser.parse(response) + end + + test "parses an Image from a JSON response" do + response = + make_json_message(%{ + text: "Hello, world!", + image_prompt: "An image of a duck wearing a hat" + }) + + assert [%Image{prompt: "An image of a duck wearing a hat"}, _bubble] = Parser.parse(response) + end + + defp make_json_message(response_json) do + json = Jason.encode!(response_json) + + %Message{ role: "assistant", - content: "{\n \"response\": \"Hello, world!\"\n}" + content: json } - - assert %Bubble{type: "bot", text: "Hello, world!"} = Parser.parse(response) end end From 081bce170b9a2532c3ebf6983d6a75ca59d63afa Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Fri, 8 Mar 2024 10:25:56 -0500 Subject: [PATCH 04/99] Add Req --- mix.exs | 3 ++- mix.lock | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index ad0d86f..79dce76 100644 --- a/mix.exs +++ b/mix.exs @@ -49,7 +49,8 @@ defmodule ChatBots.MixProject do {:ecto_sql, "~> 3.6"}, {:ecto_sqlite3, "~> 0.9.1"}, {:phoenix_ecto, "~> 4.4"}, - {:dotenvy, "~> 0.7.0"} + {:dotenvy, "~> 0.7.0"}, + {:req, "~> 0.4.0"} ] end diff --git a/mix.lock b/mix.lock index 36542d6..233919b 100644 --- a/mix.lock +++ b/mix.lock @@ -17,8 +17,10 @@ "esbuild": {:hex, :esbuild, "0.7.0", "ce3afb13cd2c5fd63e13c0e2d0e0831487a97a7696cfa563707342bb825d122a", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "4ae9f4f237c5ebcb001390b8ada65a12fb2bb04f3fe3d1f1692b7a06fbfe8752"}, "exqlite": {:hex, :exqlite, "0.13.10", "9cddd9b8764b77232d15d83752832cf96ea1066308547346e1e474a93b026810", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "8c688793efbfabd463a2af5b6f4c8ddd35278e402c07e165ee5a52a788aa67ea"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, + "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, "floki": {:hex, :floki, "0.34.2", "5fad07ef153b3b8ec110b6b155ec3780c4b2c4906297d0b4be1a7162d04a7e02", [:mix], [], "hexpm", "26b9d50f0f01796bc6be611ca815c5e0de034d2128e39cc9702eee6b66a4d1c8"}, "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, + "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, @@ -27,9 +29,13 @@ "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, + "mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"}, "mix_test_watch": {:hex, :mix_test_watch, "1.1.0", "330bb91c8ed271fe408c42d07e0773340a7938d8a0d281d57a14243eae9dc8c3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "52b6b1c476cbb70fd899ca5394506482f12e5f6b0d6acff9df95c7f1e0812ec3"}, "mock": {:hex, :mock, "0.3.7", "75b3bbf1466d7e486ea2052a73c6e062c6256fb429d6797999ab02fa32f29e03", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "4da49a4609e41fd99b7836945c26f373623ea968cfb6282742bcb94440cf7e5c"}, "mox": {:hex, :mox, "1.0.2", "dc2057289ac478b35760ba74165b4b3f402f68803dd5aecd3bfd19c183815d64", [:mix], [], "hexpm", "f9864921b3aaf763c8741b5b8e6f908f44566f1e427b2630e89e9a73b981fef2"}, + "nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"}, + "nimble_ownership": {:hex, :nimble_ownership, "0.2.1", "3e44c72ebe8dd213db4e13aff4090aaa331d158e72ce1891d02e0ffb05a1eb2d", [:mix], [], "hexpm", "bf38d2ef4fb990521a4ecf112843063c1f58a5c602484af4c7977324042badee"}, + "nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"}, "openai": {:hex, :openai, "0.3.1", "0736a9816f71c48b54bf9432aac35a9d03f1f8044990cb75dd4e8723ab0a00fe", [:mix], [{:httpoison, "~> 1.8", [hex: :httpoison, repo: "hexpm", optional: false]}, {:json, "~> 1.4", [hex: :json, repo: "hexpm", optional: false]}, {:mix_test_watch, "~> 1.0", [hex: :mix_test_watch, repo: "hexpm", optional: false]}, {:mock, "~> 0.3.6", [hex: :mock, repo: "hexpm", optional: false]}], "hexpm", "d4806a1f2eac2e0b2b35bb2d3cdb0305c4f8f4e4cc187a1695e5654aa698646e"}, "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, "phoenix": {:hex, :phoenix, "1.7.1", "a029bde19d9c3b559e5c3d06c78b76e81396bedd456a6acedb42f9c7b2e535a9", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.4", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "ea9d4a85c3592e37efa07d0dc013254fda445885facaefddcbf646375c116457"}, @@ -44,6 +50,7 @@ "plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"}, "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, + "req": {:hex, :req, "0.4.13", "6fde45b78e606e2a46fc3a7e4a74828c220cd0b16951f4321c1214f955402d72", [:mix], [{:aws_signature, "~> 0.3.2", [hex: :aws_signature, repo: "hexpm", optional: true]}, {:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:nimble_ownership, "~> 0.2.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "e01a596b74272de799bc57191883e5d4d3875be63f0480223edc5a0086bfe31b"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, "tailwind": {:hex, :tailwind, "0.1.10", "21ed80ae1f411f747ee513470578acaaa1d0eb40170005350c5b0b6d07e2d624", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "e0fc474dfa8ed7a4573851ac69c5fd3ca70fbb0a5bada574d1d657ebc6f2f1f1"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, From 1c87037ccaee48a4c0417ae305851e34a7d63ed1 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Fri, 8 Mar 2024 10:28:00 -0500 Subject: [PATCH 05/99] Add StabilityAi client --- config/dev.exs | 5 +- config/runtime.exs | 6 ++ config/test.exs | 3 + lib/chat_bots/stability_ai/api.ex | 69 +++++++++++++++++++ lib/chat_bots/stability_ai/client.ex | 14 ++++ lib/chat_bots/stability_ai/http_client.ex | 6 ++ test/chat_bots/stability_ai/api_test.exs | 45 ++++++++++++ .../support/fixtures/stability_ai_fixtures.ex | 52 ++++++++++++++ test/test_helper.exs | 3 + 9 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 lib/chat_bots/stability_ai/api.ex create mode 100644 lib/chat_bots/stability_ai/client.ex create mode 100644 lib/chat_bots/stability_ai/http_client.ex create mode 100644 test/chat_bots/stability_ai/api_test.exs create mode 100644 test/support/fixtures/stability_ai_fixtures.ex diff --git a/config/dev.exs b/config/dev.exs index e765358..4001045 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -9,7 +9,7 @@ import Config config :chat_bots, ChatBotsWeb.Endpoint, # Binding to loopback ipv4 address prevents access from other machines. # Change to `ip: {0, 0, 0, 0}` to allow access from other machines. - http: [ip: {127, 0, 0, 1}, port: 4000], + http: [ip: {127, 0, 0, 1}, port: 4001], check_origin: false, code_reloader: true, debug_errors: true, @@ -70,3 +70,6 @@ config :phoenix, :stacktrace_depth, 20 # Initialize plugs at runtime for faster development compilation config :phoenix, :plug_init_mode, :runtime + +# path for downloading images +config :chat_bots, download_path: "priv/static/images/quiz" diff --git a/config/runtime.exs b/config/runtime.exs index c03baf1..a44a58d 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -33,6 +33,9 @@ config :openai, organization_key: env!("OPENAI_ORG_KEY"), http_options: [recv_timeout: 30_000] +# Stability.ai configuration +config :chat_bots, stability_ai_api_key: env!("STABILITY_AI_API_KEY") + if config_env() == :prod do # The secret key base is used to sign/encrypt cookies and other secrets. # A default value is used in config/dev.exs and config/test.exs but you @@ -69,6 +72,9 @@ if config_env() == :prod do # get password from env (will raise if not set) config :chat_bots, :auth, password: System.fetch_env!("USER_PASSWORD") + # path for downloading images + config :chat_bots, download_path: "/data/images" + # ## SSL Support # # To get SSL working, you will need to add the `https` key diff --git a/config/test.exs b/config/test.exs index ad7af91..25fd3c2 100644 --- a/config/test.exs +++ b/config/test.exs @@ -18,3 +18,6 @@ config :logger, level: :warning # Initialize plugs at runtime for faster test compilation config :phoenix, :plug_init_mode, :runtime + +# set download path for generated images to /tmp +config :chat_bots, :download_path, "/tmp" diff --git a/lib/chat_bots/stability_ai/api.ex b/lib/chat_bots/stability_ai/api.ex new file mode 100644 index 0000000..7d8edbe --- /dev/null +++ b/lib/chat_bots/stability_ai/api.ex @@ -0,0 +1,69 @@ +defmodule ChatBots.StabilityAi.Api do + alias ChatBots.StabilityAi.Client + + @doc """ + Generates and saves an image for the given prompt. + Returns the filename. + + Possible params: + + - style_preset: 3d-model, analog-film, anime, cinematic, comic-book, digital-art, + enhance, fantasy-art, isometric, line-art, low-poly, modeling-compound, neon-punk, + origami, photographic, pixel-art, tile-texture + """ + def generate_image(prompt, params \\ %{}) do + model = "stable-diffusion-xl-1024-v1-0" + url = "https://api.stability.ai/v1/generation/#{model}/text-to-image" + download_path = Application.get_env(:chat_bots, :download_path) |> IO.inspect() + + headers = + [ + {"Authorization", "Bearer #{get_api_key()}"}, + {"Content-Type", "application/json"}, + {"Accept", "application/json"} + ] + + body = + %{ + steps: 40, + width: 1024, + height: 1024, + seed: 0, + cfg_scale: 6, + samples: 1, + style_preset: "enhance", + text_prompts: [ + %{text: prompt, weight: 1}, + %{text: "blurry, bad", weight: -1} + ] + } + |> Map.merge(params) + + {:ok, %Req.Response{body: resp_body}} = + Client.post(url, json: body, headers: headers, receive_timeout: 120_000) + + %{"artifacts" => [%{"base64" => base64, "seed" => seed}]} = resp_body + image_data = Base.decode64!(base64) + filename = make_filename(seed) + File.write!("#{download_path}/#{filename}", image_data) + {:ok, filename} + end + + defp make_filename(seed) do + date = Date.utc_today() |> Date.to_iso8601() |> String.replace("-", "") + "img-#{date}-#{seed}.png" + end + + # defp make_filename(style_preset, seed, prompt) do + # replace non-alphanumeric characters with underscores + # prompt = prompt |> String.replace(~r/[^a-zA-Z0-9]/, "_") + + # base_name = "SD-#{style_preset}-#{seed}-#{prompt}" + # truncate to 125 characters total + # (base_name |> String.slice(0..120)) <> ".png" + # end + + defp get_api_key() do + Application.get_env(:chat_bots, :stability_ai_api_key) + end +end diff --git a/lib/chat_bots/stability_ai/client.ex b/lib/chat_bots/stability_ai/client.ex new file mode 100644 index 0000000..c9a0084 --- /dev/null +++ b/lib/chat_bots/stability_ai/client.ex @@ -0,0 +1,14 @@ +defmodule ChatBots.StabilityAi.Client do + @moduledoc """ + Behaviour for a client that communicates with the Stability AI API + """ + + def impl do + Application.get_env(:chat_bots, :stability_ai_client, ChatBots.StabilityAi.HttpClient) + end + + @callback post(url :: String.t(), options :: keyword()) :: + {:ok, Req.Response.t()} | {:error, Exception.t()} + + def post(url, options), do: impl().post(url, options) +end diff --git a/lib/chat_bots/stability_ai/http_client.ex b/lib/chat_bots/stability_ai/http_client.ex new file mode 100644 index 0000000..835190d --- /dev/null +++ b/lib/chat_bots/stability_ai/http_client.ex @@ -0,0 +1,6 @@ +defmodule ChatBots.StabilityAi.HttpClient do + @behaviour ChatBots.StabilityAi.Client + + @impl true + def post(url, options), do: Req.post(url, options) +end diff --git a/test/chat_bots/stability_ai/api_test.exs b/test/chat_bots/stability_ai/api_test.exs new file mode 100644 index 0000000..032689c --- /dev/null +++ b/test/chat_bots/stability_ai/api_test.exs @@ -0,0 +1,45 @@ +defmodule ChatBots.StabilityAi.ApiTest do + use ChatBotsWeb.ConnCase, async: true + + import Mox + alias ChatBots.StabilityAi.Api + alias ChatBots.StabilityAi.MockClient + + setup [:verify_on_exit!, :setup_environment] + + describe "generate_image/1" do + test "sends a post request with the expected params to the API" do + MockClient + |> expect(:post, fn url, options -> + assert url == + "https://api.stability.ai/v1/generation/stable-diffusion-xl-1024-v1-0/text-to-image" + + assert {"Authorization", "Bearer StaB1l1tyA1"} in options[:headers] + assert {"Content-Type", "application/json"} in options[:headers] + assert {"Accept", "application/json"} in options[:headers] + + {:ok, + %Req.Response{ + body: %{ + "artifacts" => [ + %{ + "base64" => "Zm9vYmFy", + "seed" => 12345 + } + ] + } + }} + end) + + date = Date.utc_today() |> Date.to_iso8601() |> String.replace("-", "") + expected_filename = "img-#{date}-12345.png" + + assert {:ok, ^expected_filename} = + Api.generate_image("a cute fluffy cat", %{style_preset: "awesome"}) + end + end + + defp setup_environment(_) do + Application.put_env(:chat_bots, :stability_ai_api_key, "StaB1l1tyA1") + end +end diff --git a/test/support/fixtures/stability_ai_fixtures.ex b/test/support/fixtures/stability_ai_fixtures.ex new file mode 100644 index 0000000..19fa1fe --- /dev/null +++ b/test/support/fixtures/stability_ai_fixtures.ex @@ -0,0 +1,52 @@ +defmodule ChatBots.Fixtures.StabilityAiFixtures do + use ChatBots.DataCase, only: [assert: 2] + import Mox + alias ChatBots.StabilityAi.MockClient + + def text_to_image_response do + {:ok, + %Req.Response{ + status: 200, + headers: %{ + "alt-svc" => ["h3=\":443\"; ma=86400"], + "cf-cache-status" => ["DYNAMIC"], + "cf-ray" => ["835ac104dd2382b6-IAD"], + "connection" => ["keep-alive"], + "content-type" => ["application/json"], + "date" => ["Fri, 15 Dec 2023 01:01:19 GMT"], + "server" => ["cloudflare"], + "set-cookie" => [ + "__cf_bm=Sfa_zHOhmcUMezrGYBwpzlw9pCjjjDU8c7Gg2oEYtAo-1702602079-1-AfXGGd4AYd6fxGG6V51lBV0nAjAVNrMobxBi8LsT39X4jX798MlXiMu7+/cAld85V1XwerdifqIIKYFDwvN94g4=; path=/; expires=Fri, 15-Dec-23 01:31:19 GMT; domain=.stability.ai; HttpOnly; Secure; SameSite=None" + ], + "transfer-encoding" => ["chunked"], + "vary" => ["Origin"], + "x-envoy-upstream-service-time" => ["7385"] + }, + body: %{ + "artifacts" => [ + %{ + "base64" => + "iVBORw0KGgoAAAANSUhEUgAABAAAAAQACAIAAADwf7zUAAOKcWNhQlgAA4pxanVtYgAAAB5qdW1kYzJwYQARABCAAACqADibcQNjMnBhAAADiktqdW1iAAAAR2p1bWRjMm1hABEAEIAAAKoAOJtxA3Vybjp1dWlkOjA1MGViZDYxLWE2ZWItNDNmNi05MzQ5LTkzYjI0M2Y4MDgxNwAAA0ujanVtYgAAAClqdW1kYzJhcwARABCAAACqADibcQNjMnBhLmFzc2VydGlvbnMAAANJPGp1bWIAAAAzanVtZEDLDDK7ikidpwsq1vR/Q2kDYzJwYS50aHVtYm5haWwuY2xhaW0uanBlZwAAAAAUYmZkYgBpbWFnZS9qcGVnAAADSO1iaWRi/9j/4AAQSkZJRgABAgAAAQABAAD/wAARCAQABAADAREAAhEBAxEB/9sAQwAGBAUGBQQGBgUGBwcGCAoQCgoJCQoUDg8MEBcUGBgXFBYWGh0lHxobIxwWFiAsICMmJykqKRkfLTAtKDAlKCko/9sAQwEHBwcKCAoTCgoTKBoWGigoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgo/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwCpmtznEzSEJmm1YYZosAZFIABoAXigAyaADmi4BRcAouAUAJmmAmaAFpALQAUwFpAFABmi9gDIoAM0AGaADIoATNABkUXADRcAyKLgJTAChatBotsBmnsAmTQAoJoYC5NIABNDAXNCVwEoAChatBotsBTAOKLgGaLgFABxQAhNO1gEoAUUMBe1K4BmgAouAUAFCVwDNDAM0AFACGi4BRcBc0bgGaLgGaEAUAJTuAZoAWkAmaAAGmAuaQCcUAHFFwF4oAKADigAyKACkAhp2AM0wF5pAHNO4BzSAM0MBKAF/Gi4CUAGeKYCg8UNWAM0gDIoAOKAF4oasAcUXAKADigBOKLgITTQCZoYDgaTAM0AGaLgLmgBM0AGaLgGaQBQAhpoAzTasAUmrAGaLgLQwCgAouAUXAKAEoAKYBmkAc0AKDTAXJ9aQBzQAZNIAyaYCZoAXNF7agITQnYBpNO4BmgAzxSAXJpgKDRcBKACkAhpgJQAUALSADQAUwCi4DqQDTQAU7gFACUAKKGA6kAUWsAUAJTQBSYBQAUAKKGAUJ2AKLgFDVgChOwCdqAEp3AKAEpAFACimAtIAoAM07AFIA4oAKACgAoAMU7gJQAUXAKAFpAJQAUAFMApAFNAFDAChatBotsC0AFAB9ad7bAJSYC0AFABQAlOwBQAUMBRSAMUXsAEUAFACUwDFIAoAMD0p3AMUAKKLgHai9gDFFwFpAFC8wEFDAWgBKAD8KACgBaAEp2sAlIBaACgBRQAUAFCAKGAUJ2AaaYBSAKYC0mACi4BSAKdwChAFACGgAoASmAtDAChatBotsBQA6hqwBQAUJXASncAFIBTQnYBKACmtACi4C0gCgANCAShqwBTQBxRYAosAlIBKYBQAUgHCgBKdwFNIBtMAoAKACgAoAdSASgApgLSuAUwEoAChatBotsC9qFqAUwFpXASgAp3AWklcAoASgBaAEzTYBmhqwB3pABNACCmwCi4BQAlFwFpAFPYBRSAKadgE6UXAP1pAFABTYBQAUAGaGAdaFoAhIBpALTAXNFrgIaQBz3oAKdwCkAUAFOwBSAKACgBaAA0ANpgLii4BQwAmgApAFMAzQwDJodgFpAH402AhoAM0gCgA5oAKYDqTVgCgApAJVALSuAlACE00AZpAGabAM0gHUMBKYB2pAIaAEpgOBoaAWklcAosAmfeiwBQAlNaABouAUgFoABRcBTSAQUALTQDaAAUAGKACgAzQAlMBaAFFJqwATQlcBM0AApsB1JqwBQnYBKAFoYBQnbUBKAAUALQAlMAoAQ0gCgApgHagAoAKAAUgFp3ADSASmAlADu1ACUAJQAooAKACi4CikwA0AJT2AUUXAChatBotsCZoAWmwEpAFMAFAAetIBRQAtACYoAQ0AFO4CUAKKLgLRewCe1IAp3AD1oAMUAOpAIaYCUAFIBKYCgEkAdaQCkc4HSncBKAAUAL3pAZF1qC/wBq21qvLSOU/ADcx/kPzquhfLoa1K5AH0oWmoAMZGelAD5WDyOyggE8A0gGCmwFNIBM00AUXAChatBotsBQAlMBaACgAouAUXAChatBotsAadwCi4BSAMUAFMAxSABQAUwA0gD8KACgBaADvQAE00AmaGA6kwEzTsAlIANMBKLgOxQnYBChatBotsCj0psBaQCUAJQAUALTuAUABpAJQAU7gFFwA0XASgBaQC0ABoAKADNFgEoAKACmACgAoAKACgApAFNaAJQAtIBaAAUNWAWgBtAC02AhpAFABmgBaACgBKYBigB7ptbG9W46qeKGITHHUUAJQAlABRcY6hOwCUgExTACKACkAUwFoADQIKAEzSGFAADTAM0AFDAKLgPjid0lZVysa7mOegzikAyncApAFMQUDCkAooAWhAFCAKGAhpp2ASi4BSAUUMBaadgOx8BaxoOl298NatRLNJ9xmi8zcuPuD0Oe9Q0xpnITsjTSNEuyMsSq9dozwKoRHTuAtIAoATFO4BikAUAFACUAFAC0wK+o3S2dlLO38C5A9T2H50X0KiuZnDtK0XiuYvJ81nEUVuuZCAP/Q3P4Clc6GrRPQD94gcgHin5nKFIBKYC0AWpXtpLOHZGY7mPKuRyJR2b2I6H1496L2Aq0gDvTuAo6UMBMUgEoAWmAUALmgQlIYtG4CUwCgAouAYJouAUgCmAUgCmAlAC0gD8aYAKAFoAKBBRcBOKBhSAKYCUALSASgB2adrABpAJQAooYBQAlAAaADmgBaAAjjOOM4p3ASi4AKLgAFFwA0gHIAT8xwPXGaaAaenT8aGAUAFIApgFABSAKAEoAcgBZd3C55x1xTuBv8AjC40ee5thoSyCGKFUdnQLuI74xyfUnrSSe42c9QIKYBQAtDASgBwobAWpADTSuAhosAhpp2AWi4CGkAUwEoAUUXAO9ACigA7UCAikMDTAQ0CCgYCkA401oAlABQAUCEpDCmAUgHIrO6qgLMTgAdSaLgSXVvNaXDwXUTxTJ95HGCPwoCxDTuAUXAMUXAMUXAChatBotsB3pgLQwEFAC0gOi1Hwdq9hoceqTwr5DAFlU5eNT0LCjmTeg+U5yncQUgFoAKADtTQC0rAFACUAGKLgFFwChK4CZpgFIBTQtAEoAUU7gFAC0gGnrQAlMBaACgAoAwtU33+t6bYIMxPcojD1I+Y/kNv51nOeljqoU+pzEun3mop4lvbVWK20bXMxUchDMq/h6/QVLmka2vudxpN0L3TbW5XnzIwT9eh/XNbXucMlysuUCEoAKAChgLSAKAFpgFIBDQAlMBaACgBaACgQYoGIetAB1pAFMAoWgCUAFABRcBaACkAo6GgAFMQZoAVVZ2VVBLMQAB1JPahjL+s6PfaLPHDqUHkySJvUZByPwpbhYzqAA0wEoAKACgAoAKAFpAAp3AWi4AChatBotsAaAENNOwC", + "finishReason" => "SUCCESS", + "seed" => 953_806_256 + } + ] + }, + trailers: %{}, + private: %{} + }} + end + + @doc """ + Set up mock client to expect a text to image request with the given prompt + """ + def expect_text_to_image_request(prompt) do + MockClient + |> expect(:post, fn _url, options -> + json = Keyword.get(options, :json) + assert %{text_prompts: prompts} = json + assert Enum.any?(prompts, &(&1[:text] == prompt)) + text_to_image_response() + end) + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs index c6a49e7..f901a43 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -3,3 +3,6 @@ Ecto.Adapters.SQL.Sandbox.mode(ChatBots.Repo, :manual) Mox.defmock(ChatBots.OpenAi.MockClient, for: ChatBots.OpenAi.Client) Application.put_env(:chat_bots, :open_ai_client, ChatBots.OpenAi.MockClient) + +Mox.defmock(ChatBots.StabilityAi.MockClient, for: ChatBots.StabilityAi.Client) +Application.put_env(:chat_bots, :stability_ai_client, ChatBots.StabilityAi.MockClient) From 524a11307af14996285e5cbb86496176412d68f4 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Sat, 9 Mar 2024 09:49:42 -0500 Subject: [PATCH 06/99] update download_path for prod to /priv/static --- config/runtime.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/runtime.exs b/config/runtime.exs index a44a58d..95d96c1 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -73,7 +73,7 @@ if config_env() == :prod do config :chat_bots, :auth, password: System.fetch_env!("USER_PASSWORD") # path for downloading images - config :chat_bots, download_path: "/data/images" + config :chat_bots, download_path: Application.app_dir(:quiz) <> "/priv/static/images/quiz" # ## SSL Support # From 8c41994506c33da51388aea0228817a3b725d07d Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Sat, 9 Mar 2024 09:51:04 -0500 Subject: [PATCH 07/99] Image struct --- lib/chat_bots/chats/image.ex | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 lib/chat_bots/chats/image.ex diff --git a/lib/chat_bots/chats/image.ex b/lib/chat_bots/chats/image.ex new file mode 100644 index 0000000..ab68acd --- /dev/null +++ b/lib/chat_bots/chats/image.ex @@ -0,0 +1,3 @@ +defmodule ChatBots.Chats.Image do + defstruct [:prompt, :file] +end From 3fc482e8e444d8a2b56d165bccc4536642490073 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Sat, 9 Mar 2024 10:16:06 -0500 Subject: [PATCH 08/99] Rename ChatApi to OpenAi.Api --- lib/chat_bots/{chat_api.ex => open_ai/api.ex} | 2 +- lib/chat_bots_web/live/chat_live.ex | 8 +++++--- .../{chat_api_test.exs => open_ai/api_test.exs} | 10 +++++----- 3 files changed, 11 insertions(+), 9 deletions(-) rename lib/chat_bots/{chat_api.ex => open_ai/api.ex} (97%) rename test/chat_bots/{chat_api_test.exs => open_ai/api_test.exs} (87%) diff --git a/lib/chat_bots/chat_api.ex b/lib/chat_bots/open_ai/api.ex similarity index 97% rename from lib/chat_bots/chat_api.ex rename to lib/chat_bots/open_ai/api.ex index 1afe829..d14e9d8 100644 --- a/lib/chat_bots/chat_api.ex +++ b/lib/chat_bots/open_ai/api.ex @@ -1,4 +1,4 @@ -defmodule ChatBots.ChatApi do +defmodule ChatBots.OpenAi.Api do alias ChatBots.OpenAi.Client alias ChatBots.Chats alias ChatBots.Chats.{Chat, Message} diff --git a/lib/chat_bots_web/live/chat_live.ex b/lib/chat_bots_web/live/chat_live.ex index 700e786..50acdaf 100644 --- a/lib/chat_bots_web/live/chat_live.ex +++ b/lib/chat_bots_web/live/chat_live.ex @@ -3,6 +3,7 @@ defmodule ChatBotsWeb.ChatLive do alias ChatBots.Bots alias ChatBots.Chats alias ChatBots.Chats.Bubble + alias ChatBots.OpenAi.Api alias ChatBots.Parser def mount(_params, _session, socket) do @@ -50,10 +51,11 @@ defmodule ChatBotsWeb.ChatLive do def handle_info({:send_message, message_text}, socket) do socket = - case ChatBots.ChatApi.send_message(socket.assigns.chat, message_text) do + case Api.send_message(socket.assigns.chat, message_text) do {:ok, chat} -> - bubble = chat.messages |> List.last() |> Parser.parse() - chat_items = socket.assigns.chat_items ++ [bubble] + # parse the latest message into chat items + new_chat_items = chat.messages |> List.last() |> Parser.parse() + chat_items = socket.assigns.chat_items ++ new_chat_items assign(socket, chat: chat, chat_items: chat_items, loading: false) {:error, error} -> diff --git a/test/chat_bots/chat_api_test.exs b/test/chat_bots/open_ai/api_test.exs similarity index 87% rename from test/chat_bots/chat_api_test.exs rename to test/chat_bots/open_ai/api_test.exs index 2042562..4dcaf0c 100644 --- a/test/chat_bots/chat_api_test.exs +++ b/test/chat_bots/open_ai/api_test.exs @@ -1,10 +1,10 @@ -defmodule ChatBots.ChatApiTest do +defmodule ChatBots.OpenAi.ApiTest do use ChatBots.DataCase import Mox import ChatBots.Fixtures alias ChatBots.OpenAi.MockClient - alias ChatBots.ChatApi + alias ChatBots.OpenAi.Api alias ChatBots.Chats alias ChatBots.Chats.Message import ChatBots.Fixtures @@ -26,7 +26,7 @@ defmodule ChatBots.ChatApiTest do api_success_fixture("42") end) - {:ok, updated_chat} = ChatApi.send_message(chat, message_text) + {:ok, updated_chat} = Api.send_message(chat, message_text) # assert the last message in the updated_chat is "42 assert %Message{role: "user", content: ^message_text} = updated_chat.messages |> Enum.at(-2) @@ -41,7 +41,7 @@ defmodule ChatBots.ChatApiTest do # Set up the mock and assert the message is sent to the client as a map MockClient |> expect(:chat_completion, fn _ -> api_error_fixture() end) - assert {:error, error} = ChatApi.send_message(chat, message_text) + assert {:error, error} = Api.send_message(chat, message_text) assert error["message"] == "Invalid request" end @@ -53,7 +53,7 @@ defmodule ChatBots.ChatApiTest do # Set up the mock and assert the message is sent to the client as a map MockClient |> expect(:chat_completion, fn _ -> api_timeout_fixture() end) - assert {:error, error} = ChatApi.send_message(chat, message_text) + assert {:error, error} = Api.send_message(chat, message_text) assert error["message"] == "Your request timed out" end end From c6993f1e994322d80bbffc6ab8cae96a94cf1dd7 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Sat, 9 Mar 2024 10:16:30 -0500 Subject: [PATCH 09/99] Fix broken tests --- lib/chat_bots/parser.ex | 2 +- test/chat_bots/parser_test.exs | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/chat_bots/parser.ex b/lib/chat_bots/parser.ex index f574022..abd5fdd 100644 --- a/lib/chat_bots/parser.ex +++ b/lib/chat_bots/parser.ex @@ -9,7 +9,7 @@ defmodule ChatBots.Parser do """ def parse(%{content: content}) do case Jason.decode(content) do - {:ok, content_map} -> gather_chat_items(content_map) + {:ok, %{"text" => _} = content_map} -> gather_chat_items(content_map) {_, _} -> [parse_chat_item(content)] end end diff --git a/test/chat_bots/parser_test.exs b/test/chat_bots/parser_test.exs index dc95edf..c27b420 100644 --- a/test/chat_bots/parser_test.exs +++ b/test/chat_bots/parser_test.exs @@ -15,6 +15,15 @@ defmodule ChatBots.ParserTest do assert [%Bubble{type: "bot", text: "Hello, world!"}] = Parser.parse(response) end + test "parses a message from a text repsonse containing only a number" do + response = %{ + role: "assistant", + content: "42" + } + + assert [%Bubble{type: "bot", text: "42"}] = Parser.parse(response) + end + test "parses a message from a JSON response" do response = make_json_message(%{text: "Hello, world!"}) From 26674c6f09cde11701f3695e2f22d98939adffe8 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Mon, 11 Mar 2024 09:55:39 -0400 Subject: [PATCH 10/99] ChatLive displays new images as loading --- lib/chat_bots_web/live/chat_live.ex | 27 +++++++++++++++------- test/chat_bots_web/live/chat_live_test.exs | 18 +++++++++++++++ 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/lib/chat_bots_web/live/chat_live.ex b/lib/chat_bots_web/live/chat_live.ex index 50acdaf..16e21fd 100644 --- a/lib/chat_bots_web/live/chat_live.ex +++ b/lib/chat_bots_web/live/chat_live.ex @@ -3,6 +3,7 @@ defmodule ChatBotsWeb.ChatLive do alias ChatBots.Bots alias ChatBots.Chats alias ChatBots.Chats.Bubble + alias ChatBots.Chats.Image alias ChatBots.OpenAi.Api alias ChatBots.Parser @@ -26,7 +27,7 @@ defmodule ChatBotsWeb.ChatLive do def handle_event("select_bot", %{"bot_id" => bot_id}, socket) do bot = Bots.get_bot(bot_id) chat = Chats.new_chat(bot.id) - chat_items = [%{type: "info", text: "#{bot.name} has entered the chat"}] + chat_items = [%Bubble{type: "info", text: "#{bot.name} has entered the chat"}] socket = socket @@ -85,9 +86,7 @@ defmodule ChatBotsWeb.ChatLive do
<%= for chat_item <- @chat_items do %> - <%= for line <- String.split(chat_item.text, "\n\n") do %> - <.message_bubble type={chat_item.type} text={line} /> - <% end %> + <.render_chat_item item={chat_item} /> <% end %>
@@ -116,15 +115,27 @@ defmodule ChatBotsWeb.ChatLive do """ end - defp message_bubble(%{type: "error"} = assigns) do + defp render_chat_item(%{item: %Bubble{type: "error"}} = assigns) do ~H""" -

Error: <%= @text %>

+

Error: <%= @item.text %>

""" end - defp message_bubble(assigns) do + defp render_chat_item(%{item: %Bubble{}} = assigns) do ~H""" -

<%= @text %>

+

<%= @item.text %>

+ """ + end + + defp render_chat_item(%{item: %Image{}} = assigns) do + ~H""" +
+ <%= if is_nil(@item.file) do %> + loading... + <% else %> + + <% end %> +
""" end diff --git a/test/chat_bots_web/live/chat_live_test.exs b/test/chat_bots_web/live/chat_live_test.exs index c1361a4..38e7855 100644 --- a/test/chat_bots_web/live/chat_live_test.exs +++ b/test/chat_bots_web/live/chat_live_test.exs @@ -169,6 +169,24 @@ defmodule ChatBotsWeb.ChatLiveTest do assert has_element?(view, "p.bot-bubble", ~r"\Asecond line\z") end + test "displays an Image response as loading", %{conn: conn} do + bot_fixture() + {:ok, view, _html} = live(conn, "/") + + message_text = "Make a picture of a cat" + + expect_api_success(message_text, %{ + text: "here is your picture", + image_prompt: "A picture of a cat" + }) + + view + |> form("#chat-form", %{"message" => message_text}) + |> render_submit() + + assert has_element?(view, ".chat-image", "loading") + end + # Set up the mock and assert the message is sent to the client with message_text defp expect_api_success(message_sent, message_received \\ "42") do MockClient From 7f248a53de13dd0f3111ba54ed359da830e0b317 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Mon, 11 Mar 2024 10:13:44 -0400 Subject: [PATCH 11/99] Parser splits responses on newline --- lib/chat_bots/parser.ex | 7 +++++-- test/chat_bots/parser_test.exs | 23 ++++++++++++++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/lib/chat_bots/parser.ex b/lib/chat_bots/parser.ex index abd5fdd..3b74e17 100644 --- a/lib/chat_bots/parser.ex +++ b/lib/chat_bots/parser.ex @@ -10,7 +10,7 @@ defmodule ChatBots.Parser do def parse(%{content: content}) do case Jason.decode(content) do {:ok, %{"text" => _} = content_map} -> gather_chat_items(content_map) - {_, _} -> [parse_chat_item(content)] + {_, _} -> parse_chat_item(content) end end @@ -18,6 +18,7 @@ defmodule ChatBots.Parser do content_map |> Map.to_list() |> Enum.map(&parse_chat_item/1) + |> List.flatten() end defp parse_chat_item({"text", response}), do: parse_chat_item(response) @@ -27,6 +28,8 @@ defmodule ChatBots.Parser do end defp parse_chat_item(response) do - %Bubble{type: "bot", text: response} + response + |> String.split("\n\n") + |> Enum.map(&%Bubble{type: "bot", text: &1}) end end diff --git a/test/chat_bots/parser_test.exs b/test/chat_bots/parser_test.exs index c27b420..c2c0d80 100644 --- a/test/chat_bots/parser_test.exs +++ b/test/chat_bots/parser_test.exs @@ -6,7 +6,7 @@ defmodule ChatBots.ParserTest do alias ChatBots.Chats.Message alias ChatBots.Parser - test "parses a message from a text response" do + test "parses a Bubble from a text response" do response = %{ role: "assistant", content: "Hello, world!" @@ -15,6 +15,18 @@ defmodule ChatBots.ParserTest do assert [%Bubble{type: "bot", text: "Hello, world!"}] = Parser.parse(response) end + test "splits mult-line content into multiple Bubbles from a text response" do + response = %{ + role: "assistant", + content: "Hello, world!\n\nHow are you?" + } + + assert [ + %Bubble{type: "bot", text: "Hello, world!"}, + %Bubble{type: "bot", text: "How are you?"} + ] = Parser.parse(response) + end + test "parses a message from a text repsonse containing only a number" do response = %{ role: "assistant", @@ -30,6 +42,15 @@ defmodule ChatBots.ParserTest do assert [%Bubble{type: "bot", text: "Hello, world!"}] = Parser.parse(response) end + test "splits multi-line content into multiple Bubbles from a JSON response" do + response = make_json_message(%{text: "Hello, world!\n\nHow are you?"}) + + assert [ + %Bubble{type: "bot", text: "Hello, world!"}, + %Bubble{type: "bot", text: "How are you?"} + ] = Parser.parse(response) + end + test "parses an Image from a JSON response" do response = make_json_message(%{ From 58bf7a1e53a98eedd16f7d4a950c5abb26545c45 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Sat, 16 Mar 2024 10:20:08 -0400 Subject: [PATCH 12/99] ChatLive requests image --- lib/chat_bots/stability_ai/api.ex | 2 +- lib/chat_bots_web/live/chat_live.ex | 43 ++++++++++-- test/chat_bots/stability_ai/api_test.exs | 3 + test/chat_bots_web/live/chat_live_test.exs | 81 ++++++++++++++++++---- test/support/fixtures.ex | 60 ++++++++++++++++ 5 files changed, 168 insertions(+), 21 deletions(-) diff --git a/lib/chat_bots/stability_ai/api.ex b/lib/chat_bots/stability_ai/api.ex index 7d8edbe..d3e5f5b 100644 --- a/lib/chat_bots/stability_ai/api.ex +++ b/lib/chat_bots/stability_ai/api.ex @@ -14,7 +14,7 @@ defmodule ChatBots.StabilityAi.Api do def generate_image(prompt, params \\ %{}) do model = "stable-diffusion-xl-1024-v1-0" url = "https://api.stability.ai/v1/generation/#{model}/text-to-image" - download_path = Application.get_env(:chat_bots, :download_path) |> IO.inspect() + download_path = Application.get_env(:chat_bots, :download_path) headers = [ diff --git a/lib/chat_bots_web/live/chat_live.ex b/lib/chat_bots_web/live/chat_live.ex index 16e21fd..a0deddf 100644 --- a/lib/chat_bots_web/live/chat_live.ex +++ b/lib/chat_bots_web/live/chat_live.ex @@ -4,7 +4,8 @@ defmodule ChatBotsWeb.ChatLive do alias ChatBots.Chats alias ChatBots.Chats.Bubble alias ChatBots.Chats.Image - alias ChatBots.OpenAi.Api + alias ChatBots.OpenAi.Api, as: ChatApi + alias ChatBots.StabilityAi.Api, as: ImageApi alias ChatBots.Parser def mount(_params, _session, socket) do @@ -40,7 +41,7 @@ defmodule ChatBotsWeb.ChatLive do def handle_event("submit_message", %{"message" => message_text}, socket) do # send a message to self to trigger the API call in the background - send(self(), {:send_message, message_text}) + send(self(), {:request_chat, message_text}) # add user message to chat_items user_message = %Bubble{type: "user", text: message_text} @@ -50,13 +51,14 @@ defmodule ChatBotsWeb.ChatLive do {:noreply, socket} end - def handle_info({:send_message, message_text}, socket) do + def handle_info({:request_chat, message_text}, socket) do socket = - case Api.send_message(socket.assigns.chat, message_text) do + case ChatApi.send_message(socket.assigns.chat, message_text) do {:ok, chat} -> # parse the latest message into chat items - new_chat_items = chat.messages |> List.last() |> Parser.parse() + new_chat_items = chat.messages |> List.last() |> Parser.parse() |> sort_chat_items() chat_items = socket.assigns.chat_items ++ new_chat_items + maybe_send_image_request(chat_items) assign(socket, chat: chat, chat_items: chat_items, loading: false) {:error, error} -> @@ -69,6 +71,37 @@ defmodule ChatBotsWeb.ChatLive do {:noreply, socket} end + def handle_info({:request_image, image_prompt}, socket) do + case ImageApi.generate_image(image_prompt) do + {:ok, image} -> + _chat_items = socket.assigns.chat_items ++ [image] + + {:error, _error} -> + _chat_items = + socket.assigns.chat_items ++ [%Bubble{type: "error", text: "Error generating image"}] + end + + {:noreply, socket} + end + + # sort images last + defp sort_chat_items(chat_items) do + Enum.sort_by(chat_items, fn + %Image{} -> 1 + _ -> 0 + end) + end + + defp maybe_send_image_request(chat_items) do + # filter the list to only include images + chat_items + |> Enum.filter(&is_struct(&1, Image)) + |> case do + [] -> :ok + [image] -> send(self(), {:request_image, image.prompt}) + end + end + def render(assigns) do ~H"""

diff --git a/test/chat_bots/stability_ai/api_test.exs b/test/chat_bots/stability_ai/api_test.exs index 032689c..59fa82c 100644 --- a/test/chat_bots/stability_ai/api_test.exs +++ b/test/chat_bots/stability_ai/api_test.exs @@ -18,6 +18,9 @@ defmodule ChatBots.StabilityAi.ApiTest do assert {"Content-Type", "application/json"} in options[:headers] assert {"Accept", "application/json"} in options[:headers] + %{text_prompts: text_prompts} = Keyword.get(options, :json) + assert %{text: "a cute fluffy cat", weight: 1} in text_prompts + {:ok, %Req.Response{ body: %{ diff --git a/test/chat_bots_web/live/chat_live_test.exs b/test/chat_bots_web/live/chat_live_test.exs index 38e7855..acd7189 100644 --- a/test/chat_bots_web/live/chat_live_test.exs +++ b/test/chat_bots_web/live/chat_live_test.exs @@ -3,7 +3,8 @@ defmodule ChatBotsWeb.ChatLiveTest do import Mox import ChatBots.Fixtures import Phoenix.LiveViewTest - alias ChatBots.OpenAi.MockClient + alias ChatBots.OpenAi.MockClient, as: OpenAiMock + alias ChatBots.StabilityAi.MockClient, as: StabilityAiMock setup :verify_on_exit! setup :login_user @@ -39,7 +40,7 @@ defmodule ChatBotsWeb.ChatLiveTest do refute has_element?(view, "#chat-box p", message_text) - expect_api_success(message_text) + expect_chat_api_call(message_text) view |> form("#chat-form", %{"message" => message_text}) @@ -54,7 +55,7 @@ defmodule ChatBotsWeb.ChatLiveTest do message_text = "I am a user" - expect_api_success(message_text, "I am a bot") + expect_chat_api_call(message_text, "I am a bot") view |> form("#chat-form", %{"message" => message_text}) @@ -76,7 +77,7 @@ defmodule ChatBotsWeb.ChatLiveTest do message_text = "I am a user" - expect_api_success(message_text, "I am a bot") + expect_chat_api_call(message_text, "I am a bot") view |> form("#chat-form", %{"message" => message_text}) @@ -100,7 +101,7 @@ defmodule ChatBotsWeb.ChatLiveTest do message_text = "I am a user" - expect_api_success(message_text, "I am a bot") + expect_chat_api_call(message_text, "I am a bot") view |> form("#chat-form", %{"message" => message_text}) @@ -131,7 +132,7 @@ defmodule ChatBotsWeb.ChatLiveTest do assert has_element?(view, "#bot-select option[selected]", bot2.name) - expect_api_success("Hello") + expect_chat_api_call("Hello") view |> form("#chat-form", %{"message" => "Hello"}) @@ -145,7 +146,8 @@ defmodule ChatBotsWeb.ChatLiveTest do {:ok, view, _html} = live(conn, "/") - expect_api_failure() + OpenAiMock + |> expect(:chat_completion, fn _ -> api_error_fixture() end) view |> form("#chat-form", %{"message" => "Hello"}) @@ -159,7 +161,7 @@ defmodule ChatBotsWeb.ChatLiveTest do {:ok, view, _html} = live(conn, "/") message_text = "Hello" - expect_api_success(message_text, "first line\n\nsecond line") + expect_chat_api_call(message_text, "first line\n\nsecond line") view |> form("#chat-form", %{"message" => message_text}) @@ -175,11 +177,13 @@ defmodule ChatBotsWeb.ChatLiveTest do message_text = "Make a picture of a cat" - expect_api_success(message_text, %{ + expect_chat_api_call(message_text, %{ text: "here is your picture", image_prompt: "A picture of a cat" }) + expect_image_api_call("A picture of a cat") + view |> form("#chat-form", %{"message" => message_text}) |> render_submit() @@ -187,9 +191,52 @@ defmodule ChatBotsWeb.ChatLiveTest do assert has_element?(view, ".chat-image", "loading") end + test "displays an Image after the new Bubble", %{conn: conn} do + bot_fixture() + {:ok, view, _html} = live(conn, "/") + + message_text = "Make a picture of a cat" + + expect_chat_api_call(message_text, %{ + text: "here is your picture", + image_prompt: "A picture of a cat" + }) + + expect_image_api_call("A picture of a cat") + + view + |> form("#chat-form", %{"message" => message_text}) + |> render_submit() + + assert render(view) =~ ~r"here is your picture.*chat-image" + :timer.sleep(100) + end + + test "sends image prompts to the StabilityAI API", %{conn: conn} do + bot_fixture() + {:ok, view, _html} = live(conn, "/") + + message_text = "Make a picture of a cat" + + chat_response = %{ + text: "here is your picture", + image_prompt: "A picture of a cat" + } + + expect_chat_api_call(message_text, chat_response) + expect_image_api_call("A picture of a cat") + + view + |> form("#chat-form", %{"message" => message_text}) + |> render_submit() + + # TODO set up expectation blocking instead of sleeping + :timer.sleep(100) + end + # Set up the mock and assert the message is sent to the client with message_text - defp expect_api_success(message_sent, message_received \\ "42") do - MockClient + defp expect_chat_api_call(message_sent, message_received \\ "42") do + OpenAiMock |> expect(:chat_completion, fn [model: _, messages: messages] -> assert [_, user_message] = messages assert user_message == %{role: "user", content: message_sent} @@ -197,9 +244,13 @@ defmodule ChatBotsWeb.ChatLiveTest do end) end - # Set up the mock to return an error response - defp expect_api_failure() do - MockClient - |> expect(:chat_completion, fn _ -> api_error_fixture() end) + defp expect_image_api_call(image_prompt) do + StabilityAiMock + |> expect(:post, fn _url, options -> + %{text_prompts: text_prompts} = Keyword.get(options, :json) + assert %{text: image_prompt, weight: 1} in text_prompts + + {:ok, %Req.Response{body: %{"artifacts" => [%{"base64" => "Zm9vYmFy", "seed" => 12345}]}}} + end) end end diff --git a/test/support/fixtures.ex b/test/support/fixtures.ex index 6996ce1..a56dc6d 100644 --- a/test/support/fixtures.ex +++ b/test/support/fixtures.ex @@ -2,6 +2,11 @@ defmodule ChatBots.Fixtures do alias ChatBots.Repo alias ChatBots.Bots.Bot + def api_success_fixture(response_map) when is_map(response_map) do + json = Jason.encode!(response_map) + api_success_fixture(json) + end + def api_success_fixture(response_text) do {:ok, %{ @@ -23,6 +28,61 @@ defmodule ChatBots.Fixtures do }} end + def api_json_response() do + {:ok, + %{ + id: "chatcmpl-8xykPfT6zxeijbLiidVydQZ0y7spX", + usage: %{ + "completion_tokens" => 30, + "prompt_tokens" => 56, + "total_tokens" => 86 + }, + model: "gpt-3.5-turbo-0125", + choices: [ + %{ + "finish_reason" => "stop", + "index" => 0, + "logprobs" => nil, + "message" => %{ + "content" => + "{\n \"response\": \"The average distance between the Earth and the Sun is approximately 93 million miles, or about 150 million kilometers.\"\n}", + "role" => "assistant" + } + } + ], + object: "chat.completion", + created: 1_709_305_557, + system_fingerprint: "fp_2b778c6b35" + }} + end + + def api_text_response do + {:ok, + %{ + id: "chatcmpl-8yJ8HRzy3tOhCcIS3XiPG3J8j8RSF", + usage: %{ + "completion_tokens" => 19, + "prompt_tokens" => 173, + "total_tokens" => 192 + }, + model: "gpt-3.5-turbo-0125", + choices: [ + %{ + "finish_reason" => "stop", + "index" => 0, + "logprobs" => nil, + "message" => %{ + "content" => "Oh, just a casual 93 million miles. You know, a quick road trip away.", + "role" => "assistant" + } + } + ], + created: 1_709_383_917, + object: "chat.completion", + system_fingerprint: "fp_2b778c6b35" + }} + end + def api_error_fixture do {:error, %{ From 5203ac37404367b720e3376c4ae1bd29839fe169 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Sun, 17 Mar 2024 10:22:18 -0400 Subject: [PATCH 13/99] Parser returns ImageRequest instead of Image --- lib/chat_bots/chats/image_request.ex | 3 +++ lib/chat_bots/parser.ex | 4 ++-- test/chat_bots/parser_test.exs | 7 ++++--- 3 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 lib/chat_bots/chats/image_request.ex diff --git a/lib/chat_bots/chats/image_request.ex b/lib/chat_bots/chats/image_request.ex new file mode 100644 index 0000000..bd0e45d --- /dev/null +++ b/lib/chat_bots/chats/image_request.ex @@ -0,0 +1,3 @@ +defmodule ChatBots.Chats.ImageRequest do + defstruct [:prompt] +end diff --git a/lib/chat_bots/parser.ex b/lib/chat_bots/parser.ex index 3b74e17..65841dc 100644 --- a/lib/chat_bots/parser.ex +++ b/lib/chat_bots/parser.ex @@ -3,7 +3,7 @@ defmodule ChatBots.Parser do Parses messages from the chat API into chat items to be displayed in the chat window. """ alias ChatBots.Chats.Bubble - alias ChatBots.Chats.Image + alias ChatBots.Chats.ImageRequest @doc """ """ @@ -24,7 +24,7 @@ defmodule ChatBots.Parser do defp parse_chat_item({"text", response}), do: parse_chat_item(response) defp parse_chat_item({"image_prompt", prompt}) do - %Image{prompt: prompt} + %ImageRequest{prompt: prompt} end defp parse_chat_item(response) do diff --git a/test/chat_bots/parser_test.exs b/test/chat_bots/parser_test.exs index c2c0d80..c6847a2 100644 --- a/test/chat_bots/parser_test.exs +++ b/test/chat_bots/parser_test.exs @@ -2,7 +2,7 @@ defmodule ChatBots.ParserTest do use ChatBots.DataCase, async: true alias ChatBots.Chats.Bubble - alias ChatBots.Chats.Image + alias ChatBots.Chats.ImageRequest alias ChatBots.Chats.Message alias ChatBots.Parser @@ -51,14 +51,15 @@ defmodule ChatBots.ParserTest do ] = Parser.parse(response) end - test "parses an Image from a JSON response" do + test "parses an ImageRequest from a JSON response" do response = make_json_message(%{ text: "Hello, world!", image_prompt: "An image of a duck wearing a hat" }) - assert [%Image{prompt: "An image of a duck wearing a hat"}, _bubble] = Parser.parse(response) + assert [%ImageRequest{prompt: "An image of a duck wearing a hat"}, _bubble] = + Parser.parse(response) end defp make_json_message(response_json) do From 1dd7d962dc314028d837b535ef80ea901e7b09cc Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Mon, 18 Mar 2024 10:14:42 -0400 Subject: [PATCH 14/99] ChatLive retrieves and displays images --- lib/chat_bots/chats/image.ex | 2 +- lib/chat_bots_web/live/chat_live.ex | 66 +++++++++++-------- test/chat_bots/stability_ai/api_test.exs | 7 +- test/chat_bots_web/live/chat_live_test.exs | 10 ++- .../support/fixtures/stability_ai_fixtures.ex | 12 +++- 5 files changed, 62 insertions(+), 35 deletions(-) diff --git a/lib/chat_bots/chats/image.ex b/lib/chat_bots/chats/image.ex index ab68acd..6e0b98d 100644 --- a/lib/chat_bots/chats/image.ex +++ b/lib/chat_bots/chats/image.ex @@ -1,3 +1,3 @@ defmodule ChatBots.Chats.Image do - defstruct [:prompt, :file] + defstruct [:file] end diff --git a/lib/chat_bots_web/live/chat_live.ex b/lib/chat_bots_web/live/chat_live.ex index a0deddf..8e8ea28 100644 --- a/lib/chat_bots_web/live/chat_live.ex +++ b/lib/chat_bots_web/live/chat_live.ex @@ -4,6 +4,7 @@ defmodule ChatBotsWeb.ChatLive do alias ChatBots.Chats alias ChatBots.Chats.Bubble alias ChatBots.Chats.Image + alias ChatBots.Chats.ImageRequest alias ChatBots.OpenAi.Api, as: ChatApi alias ChatBots.StabilityAi.Api, as: ImageApi alias ChatBots.Parser @@ -52,36 +53,39 @@ defmodule ChatBotsWeb.ChatLive do end def handle_info({:request_chat, message_text}, socket) do - socket = - case ChatApi.send_message(socket.assigns.chat, message_text) do - {:ok, chat} -> - # parse the latest message into chat items - new_chat_items = chat.messages |> List.last() |> Parser.parse() |> sort_chat_items() - chat_items = socket.assigns.chat_items ++ new_chat_items - maybe_send_image_request(chat_items) - assign(socket, chat: chat, chat_items: chat_items, loading: false) - - {:error, error} -> - chat_items = - socket.assigns.chat_items ++ [%Bubble{type: "error", text: error["message"]}] - - assign(socket, chat_items: chat_items, loading: false) - end - - {:noreply, socket} + case ChatApi.send_message(socket.assigns.chat, message_text) do + {:ok, chat} -> + # parse the latest message into chat items + new_chat_items = chat.messages |> List.last() |> Parser.parse() |> sort_chat_items() + chat_items = socket.assigns.chat_items ++ new_chat_items + + {:noreply, + socket + |> assign(chat: chat, chat_items: chat_items, loading: false) + |> maybe_send_image_request()} + + {:error, error} -> + chat_items = + socket.assigns.chat_items ++ [%Bubble{type: "error", text: error["message"]}] + + {:noreply, assign(socket, chat_items: chat_items, loading: false)} + end end def handle_info({:request_image, image_prompt}, socket) do + # TODO: refactor this to just return the new chat_item case ImageApi.generate_image(image_prompt) do - {:ok, image} -> - _chat_items = socket.assigns.chat_items ++ [image] + {:ok, file} -> + chat_items = socket.assigns.chat_items ++ [%Image{file: file}] + + {:noreply, assign(socket, chat_items: chat_items, loading: false)} {:error, _error} -> - _chat_items = + chat_items = socket.assigns.chat_items ++ [%Bubble{type: "error", text: "Error generating image"}] - end - {:noreply, socket} + {:noreply, assign(socket, chat_items: chat_items, loading: false)} + end end # sort images last @@ -92,13 +96,17 @@ defmodule ChatBotsWeb.ChatLive do end) end - defp maybe_send_image_request(chat_items) do - # filter the list to only include images - chat_items - |> Enum.filter(&is_struct(&1, Image)) - |> case do - [] -> :ok - [image] -> send(self(), {:request_image, image.prompt}) + defp maybe_send_image_request(socket) do + {image_requests, chat_items} = + Enum.split_with(socket.assigns.chat_items, &is_struct(&1, ImageRequest)) + + case image_requests do + [] -> + socket + + [image_request] -> + send(self(), {:request_image, image_request.prompt}) + assign(socket, chat_items: chat_items, loading: true) end end diff --git a/test/chat_bots/stability_ai/api_test.exs b/test/chat_bots/stability_ai/api_test.exs index 59fa82c..ec4410d 100644 --- a/test/chat_bots/stability_ai/api_test.exs +++ b/test/chat_bots/stability_ai/api_test.exs @@ -1,7 +1,9 @@ defmodule ChatBots.StabilityAi.ApiTest do use ChatBotsWeb.ConnCase, async: true + import ChatBots.Fixtures.StabilityAiFixtures import Mox + alias ChatBots.StabilityAi.Api alias ChatBots.StabilityAi.MockClient @@ -34,10 +36,9 @@ defmodule ChatBots.StabilityAi.ApiTest do }} end) - date = Date.utc_today() |> Date.to_iso8601() |> String.replace("-", "") - expected_filename = "img-#{date}-12345.png" + expected_file_name = expected_file_name(12345) - assert {:ok, ^expected_filename} = + assert {:ok, ^expected_file_name} = Api.generate_image("a cute fluffy cat", %{style_preset: "awesome"}) end end diff --git a/test/chat_bots_web/live/chat_live_test.exs b/test/chat_bots_web/live/chat_live_test.exs index acd7189..4d69788 100644 --- a/test/chat_bots_web/live/chat_live_test.exs +++ b/test/chat_bots_web/live/chat_live_test.exs @@ -2,7 +2,9 @@ defmodule ChatBotsWeb.ChatLiveTest do use ChatBotsWeb.ConnCase, async: false import Mox import ChatBots.Fixtures + import ChatBots.Fixtures.StabilityAiFixtures import Phoenix.LiveViewTest + alias ChatBots.OpenAi.MockClient, as: OpenAiMock alias ChatBots.StabilityAi.MockClient, as: StabilityAiMock @@ -171,6 +173,7 @@ defmodule ChatBotsWeb.ChatLiveTest do assert has_element?(view, "p.bot-bubble", ~r"\Asecond line\z") end + @tag :skip test "displays an Image response as loading", %{conn: conn} do bot_fixture() {:ok, view, _html} = live(conn, "/") @@ -191,6 +194,7 @@ defmodule ChatBotsWeb.ChatLiveTest do assert has_element?(view, ".chat-image", "loading") end + @tag :skip test "displays an Image after the new Bubble", %{conn: conn} do bot_fixture() {:ok, view, _html} = live(conn, "/") @@ -212,7 +216,7 @@ defmodule ChatBotsWeb.ChatLiveTest do :timer.sleep(100) end - test "sends image prompts to the StabilityAI API", %{conn: conn} do + test "sends image prompts to the StabilityAI API and displays the image returned", %{conn: conn} do bot_fixture() {:ok, view, _html} = live(conn, "/") @@ -232,6 +236,10 @@ defmodule ChatBotsWeb.ChatLiveTest do # TODO set up expectation blocking instead of sleeping :timer.sleep(100) + + file_name = expected_file_name(12345) + + assert has_element?(view, "img[src='#{file_name}']") end # Set up the mock and assert the message is sent to the client with message_text diff --git a/test/support/fixtures/stability_ai_fixtures.ex b/test/support/fixtures/stability_ai_fixtures.ex index 19fa1fe..1105076 100644 --- a/test/support/fixtures/stability_ai_fixtures.ex +++ b/test/support/fixtures/stability_ai_fixtures.ex @@ -3,6 +3,8 @@ defmodule ChatBots.Fixtures.StabilityAiFixtures do import Mox alias ChatBots.StabilityAi.MockClient + @seed 123_456_789 + def text_to_image_response do {:ok, %Req.Response{ @@ -28,7 +30,7 @@ defmodule ChatBots.Fixtures.StabilityAiFixtures do "base64" => "iVBORw0KGgoAAAANSUhEUgAABAAAAAQACAIAAADwf7zUAAOKcWNhQlgAA4pxanVtYgAAAB5qdW1kYzJwYQARABCAAACqADibcQNjMnBhAAADiktqdW1iAAAAR2p1bWRjMm1hABEAEIAAAKoAOJtxA3Vybjp1dWlkOjA1MGViZDYxLWE2ZWItNDNmNi05MzQ5LTkzYjI0M2Y4MDgxNwAAA0ujanVtYgAAAClqdW1kYzJhcwARABCAAACqADibcQNjMnBhLmFzc2VydGlvbnMAAANJPGp1bWIAAAAzanVtZEDLDDK7ikidpwsq1vR/Q2kDYzJwYS50aHVtYm5haWwuY2xhaW0uanBlZwAAAAAUYmZkYgBpbWFnZS9qcGVnAAADSO1iaWRi/9j/4AAQSkZJRgABAgAAAQABAAD/wAARCAQABAADAREAAhEBAxEB/9sAQwAGBAUGBQQGBgUGBwcGCAoQCgoJCQoUDg8MEBcUGBgXFBYWGh0lHxobIxwWFiAsICMmJykqKRkfLTAtKDAlKCko/9sAQwEHBwcKCAoTCgoTKBoWGigoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgo/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwCpmtznEzSEJmm1YYZosAZFIABoAXigAyaADmi4BRcAouAUAJmmAmaAFpALQAUwFpAFABmi9gDIoAM0AGaADIoATNABkUXADRcAyKLgJTAChatBotsBmnsAmTQAoJoYC5NIABNDAXNCVwEoAChatBotsBTAOKLgGaLgFABxQAhNO1gEoAUUMBe1K4BmgAouAUAFCVwDNDAM0AFACGi4BRcBc0bgGaLgGaEAUAJTuAZoAWkAmaAAGmAuaQCcUAHFFwF4oAKADigAyKACkAhp2AM0wF5pAHNO4BzSAM0MBKAF/Gi4CUAGeKYCg8UNWAM0gDIoAOKAF4oasAcUXAKADigBOKLgITTQCZoYDgaTAM0AGaLgLmgBM0AGaLgGaQBQAhpoAzTasAUmrAGaLgLQwCgAouAUXAKAEoAKYBmkAc0AKDTAXJ9aQBzQAZNIAyaYCZoAXNF7agITQnYBpNO4BmgAzxSAXJpgKDRcBKACkAhpgJQAUALSADQAUwCi4DqQDTQAU7gFACUAKKGA6kAUWsAUAJTQBSYBQAUAKKGAUJ2AKLgFDVgChOwCdqAEp3AKAEpAFACimAtIAoAM07AFIA4oAKACgAoAMU7gJQAUXAKAFpAJQAUAFMApAFNAFDAChatBotsC0AFAB9ad7bAJSYC0AFABQAlOwBQAUMBRSAMUXsAEUAFACUwDFIAoAMD0p3AMUAKKLgHai9gDFFwFpAFC8wEFDAWgBKAD8KACgBaAEp2sAlIBaACgBRQAUAFCAKGAUJ2AaaYBSAKYC0mACi4BSAKdwChAFACGgAoASmAtDAChatBotsBQA6hqwBQAUJXASncAFIBTQnYBKACmtACi4C0gCgANCAShqwBTQBxRYAosAlIBKYBQAUgHCgBKdwFNIBtMAoAKACgAoAdSASgApgLSuAUwEoAChatBotsC9qFqAUwFpXASgAp3AWklcAoASgBaAEzTYBmhqwB3pABNACCmwCi4BQAlFwFpAFPYBRSAKadgE6UXAP1pAFABTYBQAUAGaGAdaFoAhIBpALTAXNFrgIaQBz3oAKdwCkAUAFOwBSAKACgBaAA0ANpgLii4BQwAmgApAFMAzQwDJodgFpAH402AhoAM0gCgA5oAKYDqTVgCgApAJVALSuAlACE00AZpAGabAM0gHUMBKYB2pAIaAEpgOBoaAWklcAosAmfeiwBQAlNaABouAUgFoABRcBTSAQUALTQDaAAUAGKACgAzQAlMBaAFFJqwATQlcBM0AApsB1JqwBQnYBKAFoYBQnbUBKAAUALQAlMAoAQ0gCgApgHagAoAKAAUgFp3ADSASmAlADu1ACUAJQAooAKACi4CikwA0AJT2AUUXAChatBotsCZoAWmwEpAFMAFAAetIBRQAtACYoAQ0AFO4CUAKKLgLRewCe1IAp3AD1oAMUAOpAIaYCUAFIBKYCgEkAdaQCkc4HSncBKAAUAL3pAZF1qC/wBq21qvLSOU/ADcx/kPzquhfLoa1K5AH0oWmoAMZGelAD5WDyOyggE8A0gGCmwFNIBM00AUXAChatBotsBQAlMBaACgAouAUXAChatBotsAadwCi4BSAMUAFMAxSABQAUwA0gD8KACgBaADvQAE00AmaGA6kwEzTsAlIANMBKLgOxQnYBChatBotsCj0psBaQCUAJQAUALTuAUABpAJQAU7gFFwA0XASgBaQC0ABoAKADNFgEoAKACmACgAoAKACgApAFNaAJQAtIBaAAUNWAWgBtAC02AhpAFABmgBaACgBKYBigB7ptbG9W46qeKGITHHUUAJQAlABRcY6hOwCUgExTACKACkAUwFoADQIKAEzSGFAADTAM0AFDAKLgPjid0lZVysa7mOegzikAyncApAFMQUDCkAooAWhAFCAKGAhpp2ASi4BSAUUMBaadgOx8BaxoOl298NatRLNJ9xmi8zcuPuD0Oe9Q0xpnITsjTSNEuyMsSq9dozwKoRHTuAtIAoATFO4BikAUAFACUAFAC0wK+o3S2dlLO38C5A9T2H50X0KiuZnDtK0XiuYvJ81nEUVuuZCAP/Q3P4Clc6GrRPQD94gcgHin5nKFIBKYC0AWpXtpLOHZGY7mPKuRyJR2b2I6H1496L2Aq0gDvTuAo6UMBMUgEoAWmAUALmgQlIYtG4CUwCgAouAYJouAUgCmAUgCmAlAC0gD8aYAKAFoAKBBRcBOKBhSAKYCUALSASgB2adrABpAJQAooYBQAlAAaADmgBaAAjjOOM4p3ASi4AKLgAFFwA0gHIAT8xwPXGaaAaenT8aGAUAFIApgFABSAKAEoAcgBZd3C55x1xTuBv8AjC40ee5thoSyCGKFUdnQLuI74xyfUnrSSe42c9QIKYBQAtDASgBwobAWpADTSuAhosAhpp2AWi4CGkAUwEoAUUXAO9ACigA7UCAikMDTAQ0CCgYCkA401oAlABQAUCEpDCmAUgHIrO6qgLMTgAdSaLgSXVvNaXDwXUTxTJ95HGCPwoCxDTuAUXAMUXAMUXAChatBotsB3pgLQwEFAC0gOi1Hwdq9hoceqTwr5DAFlU5eNT0LCjmTeg+U5yncQUgFoAKADtTQC0rAFACUAGKLgFFwChK4CZpgFIBTQtAEoAUU7gFAC0gGnrQAlMBaACgAoAwtU33+t6bYIMxPcojD1I+Y/kNv51nOeljqoU+pzEun3mop4lvbVWK20bXMxUchDMq/h6/QVLmka2vudxpN0L3TbW5XnzIwT9eh/XNbXucMlysuUCEoAKAChgLSAKAFpgFIBDQAlMBaACgBaACgQYoGIetAB1pAFMAoWgCUAFABRcBaACkAo6GgAFMQZoAVVZ2VVBLMQAB1JPahjL+s6PfaLPHDqUHkySJvUZByPwpbhYzqAA0wEoAKACgAoAKAFpAAp3AWi4AChatBotsAaAENNOwC", "finishReason" => "SUCCESS", - "seed" => 953_806_256 + "seed" => @seed } ] }, @@ -49,4 +51,12 @@ defmodule ChatBots.Fixtures.StabilityAiFixtures do text_to_image_response() end) end + + @doc """ + Returns the expected image file name for today with the given seed + """ + def expected_file_name(seed \\ @seed) do + date = Date.utc_today() |> Date.to_iso8601() |> String.replace("-", "") + "img-#{date}-#{seed}.png" + end end From 71d58f3879a46f3e1679c11756ced3019ec67dcb Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Tue, 19 Mar 2024 09:36:05 -0400 Subject: [PATCH 15/99] Simplify :request_image handler fn --- lib/chat_bots_web/live/chat_live.ex | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/lib/chat_bots_web/live/chat_live.ex b/lib/chat_bots_web/live/chat_live.ex index 8e8ea28..e93f3e0 100644 --- a/lib/chat_bots_web/live/chat_live.ex +++ b/lib/chat_bots_web/live/chat_live.ex @@ -73,19 +73,9 @@ defmodule ChatBotsWeb.ChatLive do end def handle_info({:request_image, image_prompt}, socket) do - # TODO: refactor this to just return the new chat_item - case ImageApi.generate_image(image_prompt) do - {:ok, file} -> - chat_items = socket.assigns.chat_items ++ [%Image{file: file}] - - {:noreply, assign(socket, chat_items: chat_items, loading: false)} - - {:error, _error} -> - chat_items = - socket.assigns.chat_items ++ [%Bubble{type: "error", text: "Error generating image"}] - - {:noreply, assign(socket, chat_items: chat_items, loading: false)} - end + {:ok, file} = ImageApi.generate_image(image_prompt) + chat_items = socket.assigns.chat_items ++ [%Image{file: file}] + {:noreply, assign(socket, chat_items: chat_items, loading: false)} end # sort images last From d6e20793081e6142c25cee6f326278741dd09c63 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Wed, 20 Mar 2024 09:10:18 -0400 Subject: [PATCH 16/99] Parser can parse ImageResponse without text --- lib/chat_bots/parser.ex | 12 +++++++++--- test/chat_bots/parser_test.exs | 8 +++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/lib/chat_bots/parser.ex b/lib/chat_bots/parser.ex index 65841dc..12ba1b5 100644 --- a/lib/chat_bots/parser.ex +++ b/lib/chat_bots/parser.ex @@ -9,27 +9,33 @@ defmodule ChatBots.Parser do """ def parse(%{content: content}) do case Jason.decode(content) do - {:ok, %{"text" => _} = content_map} -> gather_chat_items(content_map) + {:ok, content_map} -> gather_chat_items(content_map) {_, _} -> parse_chat_item(content) end end - defp gather_chat_items(content_map) do + defp gather_chat_items(content_map) when is_map(content_map) do content_map |> Map.to_list() |> Enum.map(&parse_chat_item/1) |> List.flatten() end + defp gather_chat_items(content_map), do: parse_chat_item(content_map) + defp parse_chat_item({"text", response}), do: parse_chat_item(response) defp parse_chat_item({"image_prompt", prompt}) do %ImageRequest{prompt: prompt} end - defp parse_chat_item(response) do + defp parse_chat_item(response) when is_binary(response) do response |> String.split("\n\n") |> Enum.map(&%Bubble{type: "bot", text: &1}) end + + defp parse_chat_item(response) do + [%Bubble{type: "bot", text: "#{response}"}] + end end diff --git a/test/chat_bots/parser_test.exs b/test/chat_bots/parser_test.exs index c6847a2..02540d6 100644 --- a/test/chat_bots/parser_test.exs +++ b/test/chat_bots/parser_test.exs @@ -27,7 +27,7 @@ defmodule ChatBots.ParserTest do ] = Parser.parse(response) end - test "parses a message from a text repsonse containing only a number" do + test "parses a message from a text response containing only a number" do response = %{ role: "assistant", content: "42" @@ -52,6 +52,12 @@ defmodule ChatBots.ParserTest do end test "parses an ImageRequest from a JSON response" do + response = make_json_message(%{image_prompt: "An image of a duck wearing a hat"}) + + assert [%ImageRequest{prompt: "An image of a duck wearing a hat"}] = Parser.parse(response) + end + + test "parses an ImageRequest and a Bubble from a single JSON response" do response = make_json_message(%{ text: "Hello, world!", From b8a25f4642cf17b9f66829449552b93fb5bb31fd Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Mon, 25 Mar 2024 10:23:17 -0400 Subject: [PATCH 17/99] Fix image path --- config/dev.exs | 5 +++-- config/runtime.exs | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/config/dev.exs b/config/dev.exs index 4001045..479399c 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -53,7 +53,8 @@ config :chat_bots, ChatBots.Repo, config :chat_bots, ChatBotsWeb.Endpoint, live_reload: [ patterns: [ - ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", + # ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", + ~r"priv/static/.*(js|css)$", ~r"lib/chat_bots_web/(controllers|live|components)/.*(ex|heex)$" ] ] @@ -72,4 +73,4 @@ config :phoenix, :stacktrace_depth, 20 config :phoenix, :plug_init_mode, :runtime # path for downloading images -config :chat_bots, download_path: "priv/static/images/quiz" +config :chat_bots, download_path: "priv/static/images" diff --git a/config/runtime.exs b/config/runtime.exs index 95d96c1..7d7d5b9 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -73,7 +73,7 @@ if config_env() == :prod do config :chat_bots, :auth, password: System.fetch_env!("USER_PASSWORD") # path for downloading images - config :chat_bots, download_path: Application.app_dir(:quiz) <> "/priv/static/images/quiz" + config :chat_bots, download_path: Application.app_dir(:chat_bots) <> "/priv/static/images" # ## SSL Support # From 77bfc3d934e28da69054e78ee6fb0b309dd2b97a Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Mon, 25 Mar 2024 10:24:51 -0400 Subject: [PATCH 18/99] Remove Chat struct and work with messages directly --- lib/chat_bots/chats.ex | 9 ++++----- lib/chat_bots/open_ai/api.ex | 16 +++++++-------- lib/chat_bots/open_ai/client.ex | 4 ++-- lib/chat_bots_web/live/chat_live.ex | 19 +++++++++--------- test/chat_bots/chats_test.exs | 23 +++++++++++----------- test/chat_bots/open_ai/api_test.exs | 8 ++++---- test/chat_bots_web/live/chat_live_test.exs | 2 +- 7 files changed, 40 insertions(+), 41 deletions(-) diff --git a/lib/chat_bots/chats.ex b/lib/chat_bots/chats.ex index cd4b9d1..1199e57 100644 --- a/lib/chat_bots/chats.ex +++ b/lib/chat_bots/chats.ex @@ -1,5 +1,5 @@ defmodule ChatBots.Chats do - alias ChatBots.Chats.{Chat, Message} + alias ChatBots.Chats.Message alias ChatBots.Bots @doc """ @@ -8,14 +8,13 @@ defmodule ChatBots.Chats do def new_chat(bot_id) do bot = Bots.get_bot(bot_id) system_prompt = %Message{role: "system", content: bot.directive} - %Chat{bot_id: bot_id, messages: [system_prompt]} + [system_prompt] end @doc """ Adds a message to the chat. """ - # TODO: enforce that it only accepts a %Message struct - def add_message(chat, message) do - %Chat{chat | messages: chat.messages ++ [message]} + def add_message(messages, message) do + messages ++ [message] end end diff --git a/lib/chat_bots/open_ai/api.ex b/lib/chat_bots/open_ai/api.ex index d14e9d8..5cebf0d 100644 --- a/lib/chat_bots/open_ai/api.ex +++ b/lib/chat_bots/open_ai/api.ex @@ -1,34 +1,34 @@ defmodule ChatBots.OpenAi.Api do alias ChatBots.OpenAi.Client alias ChatBots.Chats - alias ChatBots.Chats.{Chat, Message} + alias ChatBots.Chats.Message @model "gpt-3.5-turbo" @doc """ Sends a message to the chat bot and returns the updated chat. """ - def send_message(%Chat{} = chat, message_text) do + def send_message(messages, message_text) do user_message = %Message{ role: "user", content: message_text } # create a list of maps from the chat messages - messages = - (chat.messages ++ [user_message]) + message_maps = + (messages ++ [user_message]) |> Enum.map(&Map.from_struct(&1)) - case Client.chat_completion(model: @model, messages: messages) do + case Client.chat_completion(model: @model, messages: message_maps) do {:ok, %{choices: [choice | _]}} -> assistant_message = choice["message"] |> create_message_from_map() - updated_chat = - chat + updated_messages = + messages |> Chats.add_message(user_message) |> Chats.add_message(assistant_message) - {:ok, updated_chat} + {:ok, updated_messages} {:error, :timeout} -> {:error, %{"message" => "Your request timed out"}} diff --git a/lib/chat_bots/open_ai/client.ex b/lib/chat_bots/open_ai/client.ex index 8afd574..1bb388f 100644 --- a/lib/chat_bots/open_ai/client.ex +++ b/lib/chat_bots/open_ai/client.ex @@ -8,8 +8,8 @@ defmodule ChatBots.OpenAi.Client do @callback chat_completion(model: String.t(), messages: [Message.t()]) :: {:ok, map()} | {:error, map()} - def chat_completion(model: model, messages: messages) do - client().chat_completion(model: model, messages: messages) + def chat_completion(options) do + client().chat_completion(options) end defp client do diff --git a/lib/chat_bots_web/live/chat_live.ex b/lib/chat_bots_web/live/chat_live.ex index e93f3e0..90b8d4b 100644 --- a/lib/chat_bots_web/live/chat_live.ex +++ b/lib/chat_bots_web/live/chat_live.ex @@ -12,14 +12,15 @@ defmodule ChatBotsWeb.ChatLive do def mount(_params, _session, socket) do bots = Bots.list_bots() bot = hd(bots) - chat = Chats.new_chat(bot.id) + messages = Chats.new_chat(bot.id) + chat_items = [%Bubble{type: "info", text: "#{bot.name} has entered the chat"}] socket = socket |> assign(:bots, bots) |> assign(:bot, bot) - |> assign(:chat, chat) + |> assign(:messages, messages) |> assign(:chat_items, chat_items) |> assign(:loading, false) @@ -28,13 +29,13 @@ defmodule ChatBotsWeb.ChatLive do def handle_event("select_bot", %{"bot_id" => bot_id}, socket) do bot = Bots.get_bot(bot_id) - chat = Chats.new_chat(bot.id) + messages = Chats.new_chat(bot.id) chat_items = [%Bubble{type: "info", text: "#{bot.name} has entered the chat"}] socket = socket |> assign(:bot, bot) - |> assign(:chat, chat) + |> assign(:messages, messages) |> assign(:chat_items, chat_items) {:noreply, socket} @@ -53,15 +54,15 @@ defmodule ChatBotsWeb.ChatLive do end def handle_info({:request_chat, message_text}, socket) do - case ChatApi.send_message(socket.assigns.chat, message_text) do - {:ok, chat} -> + case ChatApi.send_message(socket.assigns.messages, message_text) do + {:ok, messages} -> # parse the latest message into chat items - new_chat_items = chat.messages |> List.last() |> Parser.parse() |> sort_chat_items() + new_chat_items = messages |> List.last() |> Parser.parse() |> sort_chat_items() chat_items = socket.assigns.chat_items ++ new_chat_items {:noreply, socket - |> assign(chat: chat, chat_items: chat_items, loading: false) + |> assign(messages: messages, chat_items: chat_items, loading: false) |> maybe_send_image_request()} {:error, error} -> @@ -164,7 +165,7 @@ defmodule ChatBotsWeb.ChatLive do <%= if is_nil(@item.file) do %> loading... <% else %> - + @item.file} /> <% end %> """ diff --git a/test/chat_bots/chats_test.exs b/test/chat_bots/chats_test.exs index ad6d42f..6384b25 100644 --- a/test/chat_bots/chats_test.exs +++ b/test/chat_bots/chats_test.exs @@ -1,33 +1,32 @@ defmodule ChatBots.ChatsTest do use ChatBots.DataCase - alias ChatBots.Chats.{Chat, Message} alias ChatBots.Chats + alias ChatBots.Chats.Message import ChatBots.Fixtures - test "new_chat/1 returns a Chat with the specified bot's id and system prompt" do + test "new_chat/1 returns a list of messages containing the bot's system prompt" do bot = bot_fixture() - assert %Chat{} = chat = Chats.new_chat(bot.id) - assert chat.bot_id == bot.id - assert [%Message{role: "system", content: "You are a helpful assistant."}] = chat.messages + + assert [%Message{role: "system", content: "You are a helpful assistant."}] = + Chats.new_chat(bot.id) end test "add_message/2 adds a message to the chat" do bot = bot_fixture() - chat = Chats.new_chat(bot.id) + messages = Chats.new_chat(bot.id) message = %Message{content: "Hello", role: "user"} - chat = Chats.add_message(chat, message) - assert [_system_prompt, ^message] = chat.messages + assert [_system_prompt, ^message] = Chats.add_message(messages, message) end test "add_message/2 adds a message to the end of the chat" do bot = bot_fixture() - chat = Chats.new_chat(bot.id) + messages = Chats.new_chat(bot.id) message1 = %Message{content: "User message", role: "user"} message2 = %Message{content: "Assistant response", role: "assistant"} - chat = Chats.add_message(chat, message1) - chat = Chats.add_message(chat, message2) - assert [_system_prompt, ^message1, ^message2] = chat.messages + messages = Chats.add_message(messages, message1) + messages = Chats.add_message(messages, message2) + assert [_system_prompt, ^message1, ^message2] = messages end end diff --git a/test/chat_bots/open_ai/api_test.exs b/test/chat_bots/open_ai/api_test.exs index 4dcaf0c..a9c65f3 100644 --- a/test/chat_bots/open_ai/api_test.exs +++ b/test/chat_bots/open_ai/api_test.exs @@ -14,7 +14,7 @@ defmodule ChatBots.OpenAi.ApiTest do test "send_message/2 adds a response to the chat" do bot = bot_fixture() - chat = Chats.new_chat(bot.id) + messages = Chats.new_chat(bot.id) message_text = "What is the meaning of life?" # Set up the mock and assert the message is sent to the client as a map @@ -26,11 +26,11 @@ defmodule ChatBots.OpenAi.ApiTest do api_success_fixture("42") end) - {:ok, updated_chat} = Api.send_message(chat, message_text) + {:ok, updated_messages} = Api.send_message(messages, message_text) # assert the last message in the updated_chat is "42 - assert %Message{role: "user", content: ^message_text} = updated_chat.messages |> Enum.at(-2) - assert %Message{role: "assistant", content: "42"} = updated_chat.messages |> Enum.at(-1) + assert %Message{role: "user", content: ^message_text} = updated_messages |> Enum.at(-2) + assert %Message{role: "assistant", content: "42"} = updated_messages |> Enum.at(-1) end test "send_message/2 returns an error tuple if the client returns an error" do diff --git a/test/chat_bots_web/live/chat_live_test.exs b/test/chat_bots_web/live/chat_live_test.exs index 4d69788..df4c363 100644 --- a/test/chat_bots_web/live/chat_live_test.exs +++ b/test/chat_bots_web/live/chat_live_test.exs @@ -239,7 +239,7 @@ defmodule ChatBotsWeb.ChatLiveTest do file_name = expected_file_name(12345) - assert has_element?(view, "img[src='#{file_name}']") + assert has_element?(view, "img[src='/images/#{file_name}']") end # Set up the mock and assert the message is sent to the client with message_text From f789d5ee1639d3e19301aa4a3c348691e698e990 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Thu, 28 Mar 2024 09:53:26 -0400 Subject: [PATCH 19/99] ChatApi.send_message takes just a list of messages --- lib/chat_bots/stability_ai/api.ex | 2 +- test/chat_bots/open_ai/api_test.exs | 22 ++++++++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/lib/chat_bots/stability_ai/api.ex b/lib/chat_bots/stability_ai/api.ex index d3e5f5b..4fe15e3 100644 --- a/lib/chat_bots/stability_ai/api.ex +++ b/lib/chat_bots/stability_ai/api.ex @@ -40,7 +40,7 @@ defmodule ChatBots.StabilityAi.Api do |> Map.merge(params) {:ok, %Req.Response{body: resp_body}} = - Client.post(url, json: body, headers: headers, receive_timeout: 120_000) + Client.post(url, json: body, headers: headers, receive_timeout: 120_000) |> IO.inspect() %{"artifacts" => [%{"base64" => base64, "seed" => seed}]} = resp_body image_data = Base.decode64!(base64) diff --git a/test/chat_bots/open_ai/api_test.exs b/test/chat_bots/open_ai/api_test.exs index a9c65f3..c5e70dd 100644 --- a/test/chat_bots/open_ai/api_test.exs +++ b/test/chat_bots/open_ai/api_test.exs @@ -14,9 +14,13 @@ defmodule ChatBots.OpenAi.ApiTest do test "send_message/2 adds a response to the chat" do bot = bot_fixture() - messages = Chats.new_chat(bot.id) + message_text = "What is the meaning of life?" + messages = + Chats.new_chat(bot.id) + |> Chats.add_message(%Message{role: "user", content: message_text}) + # Set up the mock and assert the message is sent to the client as a map MockClient |> expect(:chat_completion, fn [model: _, messages: messages] -> @@ -26,7 +30,7 @@ defmodule ChatBots.OpenAi.ApiTest do api_success_fixture("42") end) - {:ok, updated_messages} = Api.send_message(messages, message_text) + {:ok, updated_messages} = Api.send_message(messages) # assert the last message in the updated_chat is "42 assert %Message{role: "user", content: ^message_text} = updated_messages |> Enum.at(-2) @@ -35,25 +39,31 @@ defmodule ChatBots.OpenAi.ApiTest do test "send_message/2 returns an error tuple if the client returns an error" do bot = bot_fixture() - chat = Chats.new_chat(bot.id) + message_text = "What is the meaning of life?" + messages = + Chats.new_chat(bot.id) + |> Chats.add_message(%Message{role: "user", content: message_text}) + # Set up the mock and assert the message is sent to the client as a map MockClient |> expect(:chat_completion, fn _ -> api_error_fixture() end) - assert {:error, error} = Api.send_message(chat, message_text) + assert {:error, error} = Api.send_message(messages) assert error["message"] == "Invalid request" end test "send_message/2 can handle a :timeout error" do bot = bot_fixture() - chat = Chats.new_chat(bot.id) message_text = "What is the meaning of life?" + messages = + Chats.new_chat(bot.id) |> Chats.add_message(%Message{role: "user", content: message_text}) + # Set up the mock and assert the message is sent to the client as a map MockClient |> expect(:chat_completion, fn _ -> api_timeout_fixture() end) - assert {:error, error} = Api.send_message(chat, message_text) + assert {:error, error} = Api.send_message(messages) assert error["message"] == "Your request timed out" end end From 8dd2edac5d302f67b7262fa7b1aa2b4b2e3a1408 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Sun, 31 Mar 2024 10:23:10 -0400 Subject: [PATCH 20/99] Parser.parse_image_response --- lib/chat_bots/parser.ex | 30 ++++++- test/chat_bots/parser_test.exs | 149 +++++++++++++++++++++------------ 2 files changed, 122 insertions(+), 57 deletions(-) diff --git a/lib/chat_bots/parser.ex b/lib/chat_bots/parser.ex index 12ba1b5..9ffe1ca 100644 --- a/lib/chat_bots/parser.ex +++ b/lib/chat_bots/parser.ex @@ -6,11 +6,21 @@ defmodule ChatBots.Parser do alias ChatBots.Chats.ImageRequest @doc """ + Parses a chat response into a list of chat items """ - def parse(%{content: content}) do - case Jason.decode(content) do - {:ok, content_map} -> gather_chat_items(content_map) - {_, _} -> parse_chat_item(content) + def parse(%{content: content, role: "assistant"}) do + maybe_decode_json(content) + |> gather_chat_items() + end + + def parse(%{content: content, role: role}) do + [%Bubble{type: role, text: content}] + end + + defp maybe_decode_json(text) do + case Jason.decode(text) do + {:ok, map} -> map + {_, _} -> text end end @@ -38,4 +48,16 @@ defmodule ChatBots.Parser do defp parse_chat_item(response) do [%Bubble{type: "bot", text: "#{response}"}] end + + @doc """ + Parses an image_prompt if present in the JSON response + """ + def parse_image_prompt(%{content: content, role: "assistant"}) do + maybe_decode_json(content) + |> parse_image_prompt() + end + + def parse_image_prompt(%{"image_prompt" => prompt}), do: prompt + + def parse_image_prompt(_), do: nil end diff --git a/test/chat_bots/parser_test.exs b/test/chat_bots/parser_test.exs index 02540d6..87e3780 100644 --- a/test/chat_bots/parser_test.exs +++ b/test/chat_bots/parser_test.exs @@ -6,66 +6,109 @@ defmodule ChatBots.ParserTest do alias ChatBots.Chats.Message alias ChatBots.Parser - test "parses a Bubble from a text response" do - response = %{ - role: "assistant", - content: "Hello, world!" - } - - assert [%Bubble{type: "bot", text: "Hello, world!"}] = Parser.parse(response) - end - - test "splits mult-line content into multiple Bubbles from a text response" do - response = %{ - role: "assistant", - content: "Hello, world!\n\nHow are you?" - } - - assert [ - %Bubble{type: "bot", text: "Hello, world!"}, - %Bubble{type: "bot", text: "How are you?"} - ] = Parser.parse(response) - end - - test "parses a message from a text response containing only a number" do - response = %{ - role: "assistant", - content: "42" - } - - assert [%Bubble{type: "bot", text: "42"}] = Parser.parse(response) + describe "parse/1" do + test "parses a Bubble from a text response" do + response = %{ + role: "assistant", + content: "Hello, world!" + } + + assert [%Bubble{type: "bot", text: "Hello, world!"}] = Parser.parse(response) + end + + test "can parse a Bubble from a user message" do + response = %{ + role: "user", + content: "Hello, world!" + } + + assert [%Bubble{type: "user", text: "Hello, world!"}] = Parser.parse(response) + end + + test "can parse a Bubble from an info message" do + response = %{ + role: "info", + content: "Bot has entered the chat" + } + + assert [%Bubble{type: "info", text: "Bot has entered the chat"}] = Parser.parse(response) + end + + test "splits mult-line content into multiple Bubbles from a text response" do + response = %{ + role: "assistant", + content: "Hello, world!\n\nHow are you?" + } + + assert [ + %Bubble{type: "bot", text: "Hello, world!"}, + %Bubble{type: "bot", text: "How are you?"} + ] = Parser.parse(response) + end + + test "parses a message from a text response containing only a number" do + response = %{ + role: "assistant", + content: "42" + } + + assert [%Bubble{type: "bot", text: "42"}] = Parser.parse(response) + end + + test "parses a message from a JSON response" do + response = make_json_message(%{text: "Hello, world!"}) + + assert [%Bubble{type: "bot", text: "Hello, world!"}] = Parser.parse(response) + end + + test "splits multi-line content into multiple Bubbles from a JSON response" do + response = make_json_message(%{text: "Hello, world!\n\nHow are you?"}) + + assert [ + %Bubble{type: "bot", text: "Hello, world!"}, + %Bubble{type: "bot", text: "How are you?"} + ] = Parser.parse(response) + end + + test "parses an ImageRequest from a JSON response" do + response = make_json_message(%{image_prompt: "An image of a duck wearing a hat"}) + + assert [%ImageRequest{prompt: "An image of a duck wearing a hat"}] = Parser.parse(response) + end + + test "parses an ImageRequest and a Bubble from a single JSON response" do + response = + make_json_message(%{ + text: "Hello, world!", + image_prompt: "An image of a duck wearing a hat" + }) + + assert [%ImageRequest{prompt: "An image of a duck wearing a hat"}, _bubble] = + Parser.parse(response) + end end - test "parses a message from a JSON response" do - response = make_json_message(%{text: "Hello, world!"}) + describe "parse_image_prompt/1" do + test "parses an image response from a JSON response" do + response = make_json_message(%{image_prompt: "An image of a cat"}) - assert [%Bubble{type: "bot", text: "Hello, world!"}] = Parser.parse(response) - end + assert "An image of a cat" = Parser.parse_image_prompt(response) + end - test "splits multi-line content into multiple Bubbles from a JSON response" do - response = make_json_message(%{text: "Hello, world!\n\nHow are you?"}) + test "returns nil if the image_prompt key is not present" do + response = make_json_message(%{text: "Hello, world!"}) - assert [ - %Bubble{type: "bot", text: "Hello, world!"}, - %Bubble{type: "bot", text: "How are you?"} - ] = Parser.parse(response) - end - - test "parses an ImageRequest from a JSON response" do - response = make_json_message(%{image_prompt: "An image of a duck wearing a hat"}) - - assert [%ImageRequest{prompt: "An image of a duck wearing a hat"}] = Parser.parse(response) - end + assert Parser.parse_image_prompt(response) |> is_nil() + end - test "parses an ImageRequest and a Bubble from a single JSON response" do - response = - make_json_message(%{ - text: "Hello, world!", - image_prompt: "An image of a duck wearing a hat" - }) + test "returns nil if the message is not a JSON response" do + response = %{ + role: "assistant", + content: "Hello, world!" + } - assert [%ImageRequest{prompt: "An image of a duck wearing a hat"}, _bubble] = - Parser.parse(response) + assert Parser.parse_image_prompt(response) |> is_nil() + end end defp make_json_message(response_json) do From 4824888b28dc0e39237765673e4146521d17e3c8 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Tue, 2 Apr 2024 10:09:56 -0400 Subject: [PATCH 21/99] Parser can parse an image from a message with role "image" --- lib/chat_bots/parser.ex | 17 ++++++++++------- test/chat_bots/parser_test.exs | 23 ++++++++++++----------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/lib/chat_bots/parser.ex b/lib/chat_bots/parser.ex index 9ffe1ca..e0d0bc6 100644 --- a/lib/chat_bots/parser.ex +++ b/lib/chat_bots/parser.ex @@ -3,12 +3,17 @@ defmodule ChatBots.Parser do Parses messages from the chat API into chat items to be displayed in the chat window. """ alias ChatBots.Chats.Bubble - alias ChatBots.Chats.ImageRequest + alias ChatBots.Chats.Image @doc """ Parses a chat response into a list of chat items """ - def parse(%{content: content, role: "assistant"}) do + def parse(%{role: "image", content: content}) do + %{"file" => file, "prompt" => prompt} = Jason.decode!(content) + [%Image{file: file, prompt: prompt}] + end + + def parse(%{role: "assistant", content: content}) do maybe_decode_json(content) |> gather_chat_items() end @@ -35,20 +40,18 @@ defmodule ChatBots.Parser do defp parse_chat_item({"text", response}), do: parse_chat_item(response) - defp parse_chat_item({"image_prompt", prompt}) do - %ImageRequest{prompt: prompt} - end - defp parse_chat_item(response) when is_binary(response) do response |> String.split("\n\n") |> Enum.map(&%Bubble{type: "bot", text: &1}) end - defp parse_chat_item(response) do + defp parse_chat_item(response) when is_number(response) do [%Bubble{type: "bot", text: "#{response}"}] end + defp parse_chat_item(_), do: [] + @doc """ Parses an image_prompt if present in the JSON response """ diff --git a/test/chat_bots/parser_test.exs b/test/chat_bots/parser_test.exs index 87e3780..a597f48 100644 --- a/test/chat_bots/parser_test.exs +++ b/test/chat_bots/parser_test.exs @@ -2,7 +2,7 @@ defmodule ChatBots.ParserTest do use ChatBots.DataCase, async: true alias ChatBots.Chats.Bubble - alias ChatBots.Chats.ImageRequest + alias ChatBots.Chats.Image alias ChatBots.Chats.Message alias ChatBots.Parser @@ -70,20 +70,14 @@ defmodule ChatBots.ParserTest do ] = Parser.parse(response) end - test "parses an ImageRequest from a JSON response" do - response = make_json_message(%{image_prompt: "An image of a duck wearing a hat"}) - - assert [%ImageRequest{prompt: "An image of a duck wearing a hat"}] = Parser.parse(response) - end - - test "parses an ImageRequest and a Bubble from a single JSON response" do + test "can parse an image do from a JSON response" do response = make_json_message(%{ - text: "Hello, world!", - image_prompt: "An image of a duck wearing a hat" + role: "image", + content: %{file: "/path/to/image.jpg", prompt: "An image of a cat"} }) - assert [%ImageRequest{prompt: "An image of a duck wearing a hat"}, _bubble] = + assert [%Image{file: "/path/to/image.jpg", prompt: "An image of a cat"}] = Parser.parse(response) end end @@ -111,6 +105,13 @@ defmodule ChatBots.ParserTest do end end + defp make_json_message(%{role: role, content: content}) do + %Message{ + role: role, + content: Jason.encode!(content) + } + end + defp make_json_message(response_json) do json = Jason.encode!(response_json) From e33031c86775f3b5652c71b7e06fc2a68f0bf33d Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Thu, 4 Apr 2024 09:59:14 -0400 Subject: [PATCH 22/99] send_message takes a list of messages and returns a message --- lib/chat_bots/open_ai/api.ex | 21 ++++----------------- test/chat_bots/open_ai/api_test.exs | 8 +++----- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/lib/chat_bots/open_ai/api.ex b/lib/chat_bots/open_ai/api.ex index 5cebf0d..e3cf5c3 100644 --- a/lib/chat_bots/open_ai/api.ex +++ b/lib/chat_bots/open_ai/api.ex @@ -8,27 +8,14 @@ defmodule ChatBots.OpenAi.Api do @doc """ Sends a message to the chat bot and returns the updated chat. """ - def send_message(messages, message_text) do - user_message = %Message{ - role: "user", - content: message_text - } - - # create a list of maps from the chat messages - message_maps = - (messages ++ [user_message]) - |> Enum.map(&Map.from_struct(&1)) + def send_message(messages) do + # convert Messages to maps + message_maps = Enum.map(messages, &Map.from_struct(&1)) case Client.chat_completion(model: @model, messages: message_maps) do {:ok, %{choices: [choice | _]}} -> assistant_message = choice["message"] |> create_message_from_map() - - updated_messages = - messages - |> Chats.add_message(user_message) - |> Chats.add_message(assistant_message) - - {:ok, updated_messages} + {:ok, assistant_message} {:error, :timeout} -> {:error, %{"message" => "Your request timed out"}} diff --git a/test/chat_bots/open_ai/api_test.exs b/test/chat_bots/open_ai/api_test.exs index c5e70dd..9240a18 100644 --- a/test/chat_bots/open_ai/api_test.exs +++ b/test/chat_bots/open_ai/api_test.exs @@ -12,7 +12,7 @@ defmodule ChatBots.OpenAi.ApiTest do # mocks need to be verified when the test exits setup :verify_on_exit! - test "send_message/2 adds a response to the chat" do + test "send_message/2 sends a message and returns an assistant message" do bot = bot_fixture() message_text = "What is the meaning of life?" @@ -30,11 +30,9 @@ defmodule ChatBots.OpenAi.ApiTest do api_success_fixture("42") end) - {:ok, updated_messages} = Api.send_message(messages) + {:ok, message} = Api.send_message(messages) - # assert the last message in the updated_chat is "42 - assert %Message{role: "user", content: ^message_text} = updated_messages |> Enum.at(-2) - assert %Message{role: "assistant", content: "42"} = updated_messages |> Enum.at(-1) + assert %Message{role: "assistant", content: "42"} = message end test "send_message/2 returns an error tuple if the client returns an error" do From ee6c4fec928b269ded7ceaff887254d6f1b62110 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Thu, 4 Apr 2024 10:08:59 -0400 Subject: [PATCH 23/99] ChatLive just uses a single list of messages --- .gitignore | 3 +- lib/chat_bots/chats/chat.ex | 3 - lib/chat_bots/chats/image.ex | 2 +- lib/chat_bots/open_ai/api.ex | 1 - lib/chat_bots/stability_ai/api.ex | 2 +- lib/chat_bots_web/live/chat_live.ex | 96 +++++++++++----------- test/chat_bots_web/live/chat_live_test.exs | 18 ++++ 7 files changed, 72 insertions(+), 53 deletions(-) delete mode 100644 lib/chat_bots/chats/chat.ex diff --git a/.gitignore b/.gitignore index fecb438..46a5e40 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,5 @@ npm-debug.log /assets/node_modules/ .env -chatbots*.db* \ No newline at end of file +chatbots*.db* +priv/static/images/* \ No newline at end of file diff --git a/lib/chat_bots/chats/chat.ex b/lib/chat_bots/chats/chat.ex deleted file mode 100644 index ff3c93c..0000000 --- a/lib/chat_bots/chats/chat.ex +++ /dev/null @@ -1,3 +0,0 @@ -defmodule ChatBots.Chats.Chat do - defstruct [:bot_id, :messages] -end diff --git a/lib/chat_bots/chats/image.ex b/lib/chat_bots/chats/image.ex index 6e0b98d..db2ca4b 100644 --- a/lib/chat_bots/chats/image.ex +++ b/lib/chat_bots/chats/image.ex @@ -1,3 +1,3 @@ defmodule ChatBots.Chats.Image do - defstruct [:file] + defstruct [:file, :prompt] end diff --git a/lib/chat_bots/open_ai/api.ex b/lib/chat_bots/open_ai/api.ex index e3cf5c3..d672784 100644 --- a/lib/chat_bots/open_ai/api.ex +++ b/lib/chat_bots/open_ai/api.ex @@ -1,6 +1,5 @@ defmodule ChatBots.OpenAi.Api do alias ChatBots.OpenAi.Client - alias ChatBots.Chats alias ChatBots.Chats.Message @model "gpt-3.5-turbo" diff --git a/lib/chat_bots/stability_ai/api.ex b/lib/chat_bots/stability_ai/api.ex index 4fe15e3..d3e5f5b 100644 --- a/lib/chat_bots/stability_ai/api.ex +++ b/lib/chat_bots/stability_ai/api.ex @@ -40,7 +40,7 @@ defmodule ChatBots.StabilityAi.Api do |> Map.merge(params) {:ok, %Req.Response{body: resp_body}} = - Client.post(url, json: body, headers: headers, receive_timeout: 120_000) |> IO.inspect() + Client.post(url, json: body, headers: headers, receive_timeout: 120_000) %{"artifacts" => [%{"base64" => base64, "seed" => seed}]} = resp_body image_data = Base.decode64!(base64) diff --git a/lib/chat_bots_web/live/chat_live.ex b/lib/chat_bots_web/live/chat_live.ex index 90b8d4b..ccb2018 100644 --- a/lib/chat_bots_web/live/chat_live.ex +++ b/lib/chat_bots_web/live/chat_live.ex @@ -4,7 +4,7 @@ defmodule ChatBotsWeb.ChatLive do alias ChatBots.Chats alias ChatBots.Chats.Bubble alias ChatBots.Chats.Image - alias ChatBots.Chats.ImageRequest + alias ChatBots.Chats.Message alias ChatBots.OpenAi.Api, as: ChatApi alias ChatBots.StabilityAi.Api, as: ImageApi alias ChatBots.Parser @@ -12,16 +12,11 @@ defmodule ChatBotsWeb.ChatLive do def mount(_params, _session, socket) do bots = Bots.list_bots() bot = hd(bots) - messages = Chats.new_chat(bot.id) - - chat_items = [%Bubble{type: "info", text: "#{bot.name} has entered the chat"}] socket = socket |> assign(:bots, bots) - |> assign(:bot, bot) - |> assign(:messages, messages) - |> assign(:chat_items, chat_items) + |> assign_messages_and_bot(bot) |> assign(:loading, false) {:ok, socket} @@ -29,75 +24,84 @@ defmodule ChatBotsWeb.ChatLive do def handle_event("select_bot", %{"bot_id" => bot_id}, socket) do bot = Bots.get_bot(bot_id) - messages = Chats.new_chat(bot.id) - chat_items = [%Bubble{type: "info", text: "#{bot.name} has entered the chat"}] - - socket = - socket - |> assign(:bot, bot) - |> assign(:messages, messages) - |> assign(:chat_items, chat_items) + socket = assign_messages_and_bot(socket, bot) {:noreply, socket} end def handle_event("submit_message", %{"message" => message_text}, socket) do # send a message to self to trigger the API call in the background - send(self(), {:request_chat, message_text}) + send(self(), :request_chat) - # add user message to chat_items - user_message = %Bubble{type: "user", text: message_text} - chat_items = socket.assigns.chat_items ++ [user_message] + # add user message to messages + messages = + Chats.add_message(socket.assigns.messages, %Message{role: "user", content: message_text}) - socket = assign(socket, chat_items: chat_items, loading: true) + socket = assign(socket, messages: messages, loading: true) {:noreply, socket} end - def handle_info({:request_chat, message_text}, socket) do - case ChatApi.send_message(socket.assigns.messages, message_text) do - {:ok, messages} -> - # parse the latest message into chat items - new_chat_items = messages |> List.last() |> Parser.parse() |> sort_chat_items() - chat_items = socket.assigns.chat_items ++ new_chat_items + defp assign_messages_and_bot(socket, bot) do + messages = + Chats.new_chat(bot.id) + |> Chats.add_message(%Message{role: "info", content: "#{bot.name} has entered the chat"}) + + assign(socket, messages: messages, bot: bot) + end + + def handle_info(:request_chat, socket) do + filtered_messages = filter_messages_for_api(socket.assigns.messages) + + case ChatApi.send_message(filtered_messages) do + {:ok, message} -> + messages = Chats.add_message(socket.assigns.messages, message) {:noreply, socket - |> assign(messages: messages, chat_items: chat_items, loading: false) + |> assign(messages: messages, loading: false) |> maybe_send_image_request()} {:error, error} -> - chat_items = - socket.assigns.chat_items ++ [%Bubble{type: "error", text: error["message"]}] + messages = + Chats.add_message(socket.assigns.messages, %Message{ + role: "error", + content: error["message"] + }) - {:noreply, assign(socket, chat_items: chat_items, loading: false)} + {:noreply, assign(socket, messages: messages, loading: false)} end end def handle_info({:request_image, image_prompt}, socket) do {:ok, file} = ImageApi.generate_image(image_prompt) - chat_items = socket.assigns.chat_items ++ [%Image{file: file}] - {:noreply, assign(socket, chat_items: chat_items, loading: false)} + image_attrs = %{file: file, prompt: image_prompt} + image_message = %{role: "image", content: Jason.encode!(image_attrs)} + messages = Chats.add_message(socket.assigns.messages, image_message) + {:noreply, assign(socket, messages: messages, loading: false)} end - # sort images last - defp sort_chat_items(chat_items) do - Enum.sort_by(chat_items, fn - %Image{} -> 1 - _ -> 0 - end) + defp convert_messages_to_chat_items(messages) do + messages + |> Enum.filter(&(&1.role != "system")) + |> Enum.flat_map(&Parser.parse(&1)) + end + + defp filter_messages_for_api(messages) do + messages + |> Enum.filter(&(&1.role in ["system", "user", "assistant"])) end defp maybe_send_image_request(socket) do - {image_requests, chat_items} = - Enum.split_with(socket.assigns.chat_items, &is_struct(&1, ImageRequest)) + # check the latest message for an image prompt + image_prompt = socket.assigns.messages |> List.last() |> Parser.parse_image_prompt() - case image_requests do - [] -> + case image_prompt do + nil -> socket - [image_request] -> - send(self(), {:request_image, image_request.prompt}) - assign(socket, chat_items: chat_items, loading: true) + _ -> + send(self(), {:request_image, image_prompt}) + assign(socket, loading: true) end end @@ -117,7 +121,7 @@ defmodule ChatBotsWeb.ChatLive do
- <%= for chat_item <- @chat_items do %> + <%= for chat_item <- convert_messages_to_chat_items(@messages) do %> <.render_chat_item item={chat_item} /> <% end %>
diff --git a/test/chat_bots_web/live/chat_live_test.exs b/test/chat_bots_web/live/chat_live_test.exs index df4c363..7574c75 100644 --- a/test/chat_bots_web/live/chat_live_test.exs +++ b/test/chat_bots_web/live/chat_live_test.exs @@ -96,6 +96,24 @@ defmodule ChatBotsWeb.ChatLiveTest do assert has_element?(view, "#chat-box p", ~r/#{bot.name} has entered the chat/) end + test "does not send 'info' messages to the API", %{conn: conn} do + bot_fixture() + {:ok, view, _html} = live(conn, "/") + + OpenAiMock + |> expect(:chat_completion, fn [model: _, messages: messages] -> + assert not Enum.any?(messages, &(&1.role == "info")) + assert %{role: "user", content: "Hello bot"} in messages + api_success_fixture("Hello human") + end) + + view + |> form("#chat-form", %{"message" => "Hello bot"}) + |> render_submit() + + assert has_element?(view, "#chat-box p", ~r/Test Bot has entered the chat/) + end + test "selecting a different bot clears the chat", %{conn: conn} do _bot1 = bot_fixture(name: "Bot 1", directive: "some directive 1") bot2 = bot_fixture(name: "Bot 2", directive: "some directive 2") From 85741084d1f23c9e2ec251ca5225865698711450 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Sat, 6 Apr 2024 10:14:04 -0400 Subject: [PATCH 24/99] Generate Chat and Message schemas --- lib/chat_bots/chats/chat.ex | 15 +++++++++++++++ lib/chat_bots/chats/message.ex | 17 ++++++++++++++++- .../20240405141621_create_messages.exs | 12 ++++++++++++ .../migrations/20240406141130_create_chats.exs | 10 ++++++++++ 4 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 lib/chat_bots/chats/chat.ex create mode 100644 priv/repo/migrations/20240405141621_create_messages.exs create mode 100644 priv/repo/migrations/20240406141130_create_chats.exs diff --git a/lib/chat_bots/chats/chat.ex b/lib/chat_bots/chats/chat.ex new file mode 100644 index 0000000..47e166c --- /dev/null +++ b/lib/chat_bots/chats/chat.ex @@ -0,0 +1,15 @@ +defmodule ChatBots.Chats.Chat do + use Ecto.Schema + import Ecto.Changeset + + schema "chats" do + timestamps() + end + + @doc false + def changeset(chat, attrs) do + chat + |> cast(attrs, []) + |> validate_required([]) + end +end diff --git a/lib/chat_bots/chats/message.ex b/lib/chat_bots/chats/message.ex index f717ec4..358510f 100644 --- a/lib/chat_bots/chats/message.ex +++ b/lib/chat_bots/chats/message.ex @@ -1,3 +1,18 @@ defmodule ChatBots.Chats.Message do - defstruct [:role, :content] + use Ecto.Schema + import Ecto.Changeset + + schema "messages" do + field :role, :string + field :content, :string + + timestamps() + end + + @doc false + def changeset(message, attrs) do + message + |> cast(attrs, [:role, :content]) + |> validate_required([:role, :content]) + end end diff --git a/priv/repo/migrations/20240405141621_create_messages.exs b/priv/repo/migrations/20240405141621_create_messages.exs new file mode 100644 index 0000000..ff6658b --- /dev/null +++ b/priv/repo/migrations/20240405141621_create_messages.exs @@ -0,0 +1,12 @@ +defmodule ChatBots.Repo.Migrations.CreateMessages do + use Ecto.Migration + + def change do + create table(:messages) do + add :role, :string + add :content, :string + + timestamps() + end + end +end diff --git a/priv/repo/migrations/20240406141130_create_chats.exs b/priv/repo/migrations/20240406141130_create_chats.exs new file mode 100644 index 0000000..b24d714 --- /dev/null +++ b/priv/repo/migrations/20240406141130_create_chats.exs @@ -0,0 +1,10 @@ +defmodule ChatBots.Repo.Migrations.CreateChats do + use Ecto.Migration + + def change do + create table(:chats) do + + timestamps() + end + end +end From 06c0a45c7bfdca71227cfd4510e7bed31f2da0eb Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Sun, 7 Apr 2024 10:14:51 -0400 Subject: [PATCH 25/99] Set up Chat and Message assocs --- lib/chat_bots/chats/chat.ex | 14 ++++++-------- lib/chat_bots/chats/message.ex | 8 ++++++-- .../migrations/20240405141621_create_messages.exs | 4 +++- .../migrations/20240406141130_create_chats.exs | 4 +++- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/lib/chat_bots/chats/chat.ex b/lib/chat_bots/chats/chat.ex index 47e166c..053c1bc 100644 --- a/lib/chat_bots/chats/chat.ex +++ b/lib/chat_bots/chats/chat.ex @@ -1,15 +1,13 @@ defmodule ChatBots.Chats.Chat do use Ecto.Schema - import Ecto.Changeset + alias ChatBots.Bots.Bot + alias ChatBots.Chats.Message + @primary_key {:id, :binary_id, autogenerate: true} schema "chats" do - timestamps() - end + has_many(:messages, Message) + belongs_to(:bot, Bot) - @doc false - def changeset(chat, attrs) do - chat - |> cast(attrs, []) - |> validate_required([]) + timestamps() end end diff --git a/lib/chat_bots/chats/message.ex b/lib/chat_bots/chats/message.ex index 358510f..9b8f1c0 100644 --- a/lib/chat_bots/chats/message.ex +++ b/lib/chat_bots/chats/message.ex @@ -2,9 +2,13 @@ defmodule ChatBots.Chats.Message do use Ecto.Schema import Ecto.Changeset + @primary_key {:id, :binary_id, autogenerate: true} + @foreign_key_type :binary_id schema "messages" do - field :role, :string - field :content, :string + field(:role, :string) + field(:content, :string) + + belongs_to(:chat, ChatBots.Chats.Chat) timestamps() end diff --git a/priv/repo/migrations/20240405141621_create_messages.exs b/priv/repo/migrations/20240405141621_create_messages.exs index ff6658b..d1a6c1b 100644 --- a/priv/repo/migrations/20240405141621_create_messages.exs +++ b/priv/repo/migrations/20240405141621_create_messages.exs @@ -2,7 +2,9 @@ defmodule ChatBots.Repo.Migrations.CreateMessages do use Ecto.Migration def change do - create table(:messages) do + create table(:messages, primary_key: false) do + add :id, :binary_id, primary_key: true + add :chat_id, references(:chats, on_delete: :delete_all) add :role, :string add :content, :string diff --git a/priv/repo/migrations/20240406141130_create_chats.exs b/priv/repo/migrations/20240406141130_create_chats.exs index b24d714..677b5d2 100644 --- a/priv/repo/migrations/20240406141130_create_chats.exs +++ b/priv/repo/migrations/20240406141130_create_chats.exs @@ -2,7 +2,9 @@ defmodule ChatBots.Repo.Migrations.CreateChats do use Ecto.Migration def change do - create table(:chats) do + create table(:chats, primary_key: false) do + add :id, :binary_id, primary_key: true + add :bot_id, references(:bots, on_delete: :restrict) timestamps() end From 5a769dfe22acda0c1c310744c2f15ed6fa9e11c8 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Sun, 7 Apr 2024 10:21:17 -0400 Subject: [PATCH 26/99] Chats.create_chat --- lib/chat_bots/chats.ex | 17 ++++++++++++++++- test/chat_bots/chats_test.exs | 9 +++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/chat_bots/chats.ex b/lib/chat_bots/chats.ex index 1199e57..4e142f9 100644 --- a/lib/chat_bots/chats.ex +++ b/lib/chat_bots/chats.ex @@ -1,6 +1,21 @@ defmodule ChatBots.Chats do - alias ChatBots.Chats.Message alias ChatBots.Bots + alias ChatBots.Chats.Chat + alias ChatBots.Chats.Message + alias ChatBots.Repo + + @doc """ + Creates a new chat for the given bot_id. + """ + def create_chat(bot) do + %Chat{} + |> Ecto.Changeset.change(bot_id: bot.id) + |> Repo.insert!() + end + + @doc """ + Creates a + """ @doc """ Creates a new chat with the given bot_id. diff --git a/test/chat_bots/chats_test.exs b/test/chat_bots/chats_test.exs index 6384b25..edc8cca 100644 --- a/test/chat_bots/chats_test.exs +++ b/test/chat_bots/chats_test.exs @@ -1,9 +1,18 @@ defmodule ChatBots.ChatsTest do use ChatBots.DataCase alias ChatBots.Chats + alias ChatBots.Chats.Chat alias ChatBots.Chats.Message import ChatBots.Fixtures + describe "create_chat/1" do + test "creates a new chat for a bot" do + bot = %{id: bot_id} = bot_fixture() + + assert %Chat{bot_id: ^bot_id} = Chats.create_chat(bot) + end + end + test "new_chat/1 returns a list of messages containing the bot's system prompt" do bot = bot_fixture() From 129b0ba407e417af83869fadee97e7ddb03f6af8 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Tue, 9 Apr 2024 09:44:58 -0400 Subject: [PATCH 27/99] Setup ExMachina --- mix.exs | 3 ++- mix.lock | 1 + test/support/factory.ex | 28 ++++++++++++++++++++++++++++ test/test_helper.exs | 2 ++ 4 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 test/support/factory.ex diff --git a/mix.exs b/mix.exs index 79dce76..76fa7f4 100644 --- a/mix.exs +++ b/mix.exs @@ -50,7 +50,8 @@ defmodule ChatBots.MixProject do {:ecto_sqlite3, "~> 0.9.1"}, {:phoenix_ecto, "~> 4.4"}, {:dotenvy, "~> 0.7.0"}, - {:req, "~> 0.4.0"} + {:req, "~> 0.4.0"}, + {:ex_machina, "~> 2.7.0", only: :test} ] end diff --git a/mix.lock b/mix.lock index 233919b..07537a4 100644 --- a/mix.lock +++ b/mix.lock @@ -15,6 +15,7 @@ "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.9.1", "6cb740afcb0af882cf9dfbac7ff8db863600d3bb24454c840d506e05b1508d6e", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.9", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "cc7664d9210c232370e67e89331056cf0a35e74862cd57e893e7035eacbd4afc"}, "elixir_make": {:hex, :elixir_make, "0.7.6", "67716309dc5d43e16b5abbd00c01b8df6a0c2ab54a8f595468035a50189f9169", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5a0569756b0f7873a77687800c164cca6dfc03a09418e6fcf853d78991f49940"}, "esbuild": {:hex, :esbuild, "0.7.0", "ce3afb13cd2c5fd63e13c0e2d0e0831487a97a7696cfa563707342bb825d122a", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "4ae9f4f237c5ebcb001390b8ada65a12fb2bb04f3fe3d1f1692b7a06fbfe8752"}, + "ex_machina": {:hex, :ex_machina, "2.7.0", "b792cc3127fd0680fecdb6299235b4727a4944a09ff0fa904cc639272cd92dc7", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "419aa7a39bde11894c87a615c4ecaa52d8f107bbdd81d810465186f783245bf8"}, "exqlite": {:hex, :exqlite, "0.13.10", "9cddd9b8764b77232d15d83752832cf96ea1066308547346e1e474a93b026810", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "8c688793efbfabd463a2af5b6f4c8ddd35278e402c07e165ee5a52a788aa67ea"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, diff --git a/test/support/factory.ex b/test/support/factory.ex new file mode 100644 index 0000000..34ac947 --- /dev/null +++ b/test/support/factory.ex @@ -0,0 +1,28 @@ +defmodule ChatBots.Factory do + use ExMachina.Ecto, repo: ChatBots.Repo + + alias ChatBots.Bots.Bot + alias ChatBots.Chats.Chat + alias ChatBots.Chats.Message + + def bot_factory do + %Bot{ + name: "Test Bot", + directive: "You are a helpful assistant." + } + end + + def chat_factory do + %Chat{ + bot: build(:bot) + } + end + + def message_factory do + %Message{ + chat: build(:chat), + role: "system", + content: "You are a helpful assistant." + } + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs index f901a43..cad1b18 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,6 +1,8 @@ ExUnit.start() Ecto.Adapters.SQL.Sandbox.mode(ChatBots.Repo, :manual) +{:ok, _} = Application.ensure_all_started(:ex_machina) + Mox.defmock(ChatBots.OpenAi.MockClient, for: ChatBots.OpenAi.Client) Application.put_env(:chat_bots, :open_ai_client, ChatBots.OpenAi.MockClient) From 4ba7e3e4783d0d85d69489bb9244c332e0f42efe Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Wed, 10 Apr 2024 10:45:14 -0400 Subject: [PATCH 28/99] Chats.get_chat! fn --- lib/chat_bots/chats.ex | 5 ++++- test/chat_bots/chats_test.exs | 16 +++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/lib/chat_bots/chats.ex b/lib/chat_bots/chats.ex index 4e142f9..e0ffbf3 100644 --- a/lib/chat_bots/chats.ex +++ b/lib/chat_bots/chats.ex @@ -14,8 +14,11 @@ defmodule ChatBots.Chats do end @doc """ - Creates a + Retrieves a chat by id """ + def get_chat!(id) do + Repo.get!(Chat, id) + end @doc """ Creates a new chat with the given bot_id. diff --git a/test/chat_bots/chats_test.exs b/test/chat_bots/chats_test.exs index edc8cca..7cc9d44 100644 --- a/test/chat_bots/chats_test.exs +++ b/test/chat_bots/chats_test.exs @@ -3,14 +3,20 @@ defmodule ChatBots.ChatsTest do alias ChatBots.Chats alias ChatBots.Chats.Chat alias ChatBots.Chats.Message + import ChatBots.Fixtures + import ChatBots.Factory + + test "create_chat/1 creates a new chat for a bot" do + bot = %{id: bot_id} = insert(:bot) + + assert %Chat{bot_id: ^bot_id} = Chats.create_chat(bot) + end - describe "create_chat/1" do - test "creates a new chat for a bot" do - bot = %{id: bot_id} = bot_fixture() + test "get_chat!/1 returns the chat for a bot" do + %{id: chat_id} = insert(:chat) - assert %Chat{bot_id: ^bot_id} = Chats.create_chat(bot) - end + assert %Chat{id: ^chat_id} = Chats.get_chat!(chat_id) end test "new_chat/1 returns a list of messages containing the bot's system prompt" do From 71ee249a590a11950d737b77d05bbbb8bcfb852d Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Thu, 11 Apr 2024 09:33:26 -0400 Subject: [PATCH 29/99] create_message and other context fn tests --- lib/chat_bots/chats.ex | 17 ++++++++++++++++- test/chat_bots/chats_test.exs | 22 ++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/lib/chat_bots/chats.ex b/lib/chat_bots/chats.ex index e0ffbf3..ef27616 100644 --- a/lib/chat_bots/chats.ex +++ b/lib/chat_bots/chats.ex @@ -4,12 +4,16 @@ defmodule ChatBots.Chats do alias ChatBots.Chats.Message alias ChatBots.Repo + import Ecto.Changeset, only: [change: 2, put_assoc: 3] + @doc """ Creates a new chat for the given bot_id. + Adds a message with the bot's system prompt. """ def create_chat(bot) do %Chat{} - |> Ecto.Changeset.change(bot_id: bot.id) + |> change(bot_id: bot.id) + |> put_assoc(:messages, [%Message{role: "system", content: bot.directive}]) |> Repo.insert!() end @@ -18,6 +22,17 @@ defmodule ChatBots.Chats do """ def get_chat!(id) do Repo.get!(Chat, id) + |> Repo.preload(:messages) + end + + @doc """ + Creates a new message for the given chat + """ + def create_message(chat, attrs) do + %Message{} + |> change(attrs) + |> put_assoc(:chat, chat) + |> Repo.insert!() end @doc """ diff --git a/test/chat_bots/chats_test.exs b/test/chat_bots/chats_test.exs index 7cc9d44..2b0b6cd 100644 --- a/test/chat_bots/chats_test.exs +++ b/test/chat_bots/chats_test.exs @@ -13,12 +13,26 @@ defmodule ChatBots.ChatsTest do assert %Chat{bot_id: ^bot_id} = Chats.create_chat(bot) end + test "create_chat/1 creates a chat with the bot's system prompt" do + bot = bot_fixture() + + assert %Chat{messages: messages} = Chats.create_chat(bot) + assert [%Message{role: "system", content: "You are a helpful assistant."}] = messages + end + test "get_chat!/1 returns the chat for a bot" do %{id: chat_id} = insert(:chat) assert %Chat{id: ^chat_id} = Chats.get_chat!(chat_id) end + test "get_chat!/1 preloads messages" do + chat = insert(:chat, messages: [%{role: "system", content: "You are a helpful assistant."}]) + + assert [%Message{role: "system", content: "You are a helpful assistant."}] = + Chats.get_chat!(chat.id).messages + end + test "new_chat/1 returns a list of messages containing the bot's system prompt" do bot = bot_fixture() @@ -44,4 +58,12 @@ defmodule ChatBots.ChatsTest do messages = Chats.add_message(messages, message2) assert [_system_prompt, ^message1, ^message2] = messages end + + test "create_message/2 creates a new message on the given chat" do + chat = %{id: chat_id} = insert(:chat) + attrs = %{role: "user", content: "Hello"} + + assert %Message{chat_id: ^chat_id, role: "user", content: "Hello"} = + Chats.create_message(chat, attrs) + end end From 425ac9f3f3d31dd16d85f996bd59acfe2babfd72 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Sat, 13 Apr 2024 08:27:45 -0400 Subject: [PATCH 30/99] OpenAI.Api works with maps instead of Messages --- lib/chat_bots/open_ai/api.ex | 16 ++-------------- test/chat_bots/open_ai/api_test.exs | 29 ++++++++++++----------------- 2 files changed, 14 insertions(+), 31 deletions(-) diff --git a/lib/chat_bots/open_ai/api.ex b/lib/chat_bots/open_ai/api.ex index d672784..3998448 100644 --- a/lib/chat_bots/open_ai/api.ex +++ b/lib/chat_bots/open_ai/api.ex @@ -1,6 +1,5 @@ defmodule ChatBots.OpenAi.Api do alias ChatBots.OpenAi.Client - alias ChatBots.Chats.Message @model "gpt-3.5-turbo" @@ -8,13 +7,9 @@ defmodule ChatBots.OpenAi.Api do Sends a message to the chat bot and returns the updated chat. """ def send_message(messages) do - # convert Messages to maps - message_maps = Enum.map(messages, &Map.from_struct(&1)) - - case Client.chat_completion(model: @model, messages: message_maps) do + case Client.chat_completion(model: @model, messages: messages) do {:ok, %{choices: [choice | _]}} -> - assistant_message = choice["message"] |> create_message_from_map() - {:ok, assistant_message} + {:ok, choice["message"]} {:error, :timeout} -> {:error, %{"message" => "Your request timed out"}} @@ -23,11 +18,4 @@ defmodule ChatBots.OpenAi.Api do {:error, error["error"]} end end - - defp create_message_from_map(map) do - %Message{ - role: map["role"], - content: map["content"] - } - end end diff --git a/test/chat_bots/open_ai/api_test.exs b/test/chat_bots/open_ai/api_test.exs index 9240a18..d33a3aa 100644 --- a/test/chat_bots/open_ai/api_test.exs +++ b/test/chat_bots/open_ai/api_test.exs @@ -13,13 +13,8 @@ defmodule ChatBots.OpenAi.ApiTest do setup :verify_on_exit! test "send_message/2 sends a message and returns an assistant message" do - bot = bot_fixture() - message_text = "What is the meaning of life?" - - messages = - Chats.new_chat(bot.id) - |> Chats.add_message(%Message{role: "user", content: message_text}) + messages = messages_fixture(message_text) # Set up the mock and assert the message is sent to the client as a map MockClient @@ -32,17 +27,12 @@ defmodule ChatBots.OpenAi.ApiTest do {:ok, message} = Api.send_message(messages) - assert %Message{role: "assistant", content: "42"} = message + assert %{"role" => "assistant", "content" => "42"} = message end test "send_message/2 returns an error tuple if the client returns an error" do - bot = bot_fixture() - message_text = "What is the meaning of life?" - - messages = - Chats.new_chat(bot.id) - |> Chats.add_message(%Message{role: "user", content: message_text}) + messages = messages_fixture(message_text) # Set up the mock and assert the message is sent to the client as a map MockClient |> expect(:chat_completion, fn _ -> api_error_fixture() end) @@ -52,11 +42,8 @@ defmodule ChatBots.OpenAi.ApiTest do end test "send_message/2 can handle a :timeout error" do - bot = bot_fixture() message_text = "What is the meaning of life?" - - messages = - Chats.new_chat(bot.id) |> Chats.add_message(%Message{role: "user", content: message_text}) + messages = messages_fixture(message_text) # Set up the mock and assert the message is sent to the client as a map MockClient |> expect(:chat_completion, fn _ -> api_timeout_fixture() end) @@ -64,4 +51,12 @@ defmodule ChatBots.OpenAi.ApiTest do assert {:error, error} = Api.send_message(messages) assert error["message"] == "Your request timed out" end + + defp messages_fixture(message_text) do + bot = bot_fixture() + + Chats.new_chat(bot.id) + |> Chats.add_message(%Message{role: "user", content: message_text}) + |> Enum.map(&Map.take(&1, [:role, :content])) + end end From 3f5bdfa46db03f2ec67018d48b77c817ced92ae0 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Sun, 14 Apr 2024 10:27:35 -0400 Subject: [PATCH 31/99] Improve Chats.create_message --- lib/chat_bots/chats.ex | 6 +++--- test/chat_bots/chats_test.exs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/chat_bots/chats.ex b/lib/chat_bots/chats.ex index ef27616..8fdf589 100644 --- a/lib/chat_bots/chats.ex +++ b/lib/chat_bots/chats.ex @@ -4,7 +4,7 @@ defmodule ChatBots.Chats do alias ChatBots.Chats.Message alias ChatBots.Repo - import Ecto.Changeset, only: [change: 2, put_assoc: 3] + import Ecto.Changeset @doc """ Creates a new chat for the given bot_id. @@ -30,9 +30,9 @@ defmodule ChatBots.Chats do """ def create_message(chat, attrs) do %Message{} - |> change(attrs) + |> cast(attrs, [:role, :content]) |> put_assoc(:chat, chat) - |> Repo.insert!() + |> Repo.insert() end @doc """ diff --git a/test/chat_bots/chats_test.exs b/test/chat_bots/chats_test.exs index 2b0b6cd..22fac8b 100644 --- a/test/chat_bots/chats_test.exs +++ b/test/chat_bots/chats_test.exs @@ -63,7 +63,7 @@ defmodule ChatBots.ChatsTest do chat = %{id: chat_id} = insert(:chat) attrs = %{role: "user", content: "Hello"} - assert %Message{chat_id: ^chat_id, role: "user", content: "Hello"} = + assert {:ok, %Message{chat_id: ^chat_id, role: "user", content: "Hello"}} = Chats.create_message(chat, attrs) end end From e8a43b348213cbb68e695e3d1a2391ce264e7f84 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Sun, 14 Apr 2024 10:28:14 -0400 Subject: [PATCH 32/99] First pass at making ChatLive load chat from db --- lib/chat_bots_web/live/chat_live.ex | 50 ++++++---------------- lib/chat_bots_web/router.ex | 3 +- test/chat_bots_web/live/chat_live_test.exs | 27 +++++------- 3 files changed, 26 insertions(+), 54 deletions(-) diff --git a/lib/chat_bots_web/live/chat_live.ex b/lib/chat_bots_web/live/chat_live.ex index ccb2018..50a72b5 100644 --- a/lib/chat_bots_web/live/chat_live.ex +++ b/lib/chat_bots_web/live/chat_live.ex @@ -1,6 +1,5 @@ defmodule ChatBotsWeb.ChatLive do use ChatBotsWeb, :live_view - alias ChatBots.Bots alias ChatBots.Chats alias ChatBots.Chats.Bubble alias ChatBots.Chats.Image @@ -9,52 +8,40 @@ defmodule ChatBotsWeb.ChatLive do alias ChatBots.StabilityAi.Api, as: ImageApi alias ChatBots.Parser - def mount(_params, _session, socket) do - bots = Bots.list_bots() - bot = hd(bots) + def mount(%{"id" => chat_id}, _session, socket) do + chat = Chats.get_chat!(chat_id) socket = socket - |> assign(:bots, bots) - |> assign_messages_and_bot(bot) + |> assign(:chat, chat) + |> assign(:messages, chat.messages) |> assign(:loading, false) {:ok, socket} end - def handle_event("select_bot", %{"bot_id" => bot_id}, socket) do - bot = Bots.get_bot(bot_id) - socket = assign_messages_and_bot(socket, bot) - - {:noreply, socket} - end - def handle_event("submit_message", %{"message" => message_text}, socket) do # send a message to self to trigger the API call in the background send(self(), :request_chat) # add user message to messages - messages = - Chats.add_message(socket.assigns.messages, %Message{role: "user", content: message_text}) + {:ok, message} = + Chats.create_message(socket.assigns.chat, %{role: "user", content: message_text}) + + messages = socket.assigns.messages ++ [message] socket = assign(socket, messages: messages, loading: true) {:noreply, socket} end - defp assign_messages_and_bot(socket, bot) do - messages = - Chats.new_chat(bot.id) - |> Chats.add_message(%Message{role: "info", content: "#{bot.name} has entered the chat"}) - - assign(socket, messages: messages, bot: bot) - end - def handle_info(:request_chat, socket) do - filtered_messages = filter_messages_for_api(socket.assigns.messages) + %{chat: chat, messages: messages} = socket.assigns + filtered_messages = filter_messages_for_api(messages) case ChatApi.send_message(filtered_messages) do - {:ok, message} -> - messages = Chats.add_message(socket.assigns.messages, message) + {:ok, message_attrs} -> + {:ok, message} = Chats.create_message(chat, message_attrs) + messages = socket.assigns.messages ++ [message] {:noreply, socket @@ -110,15 +97,6 @@ defmodule ChatBotsWeb.ChatLive do

Bot Box

-
- -
<%= for chat_item <- convert_messages_to_chat_items(@messages) do %> @@ -186,6 +164,4 @@ defmodule ChatBotsWeb.ChatLive do "#{base_classes} bot-bubble text-gray-800 bg-gray-300" end end - - defp bot_options(bots), do: Enum.map(bots, &{&1.name, &1.id}) end diff --git a/lib/chat_bots_web/router.ex b/lib/chat_bots_web/router.ex index 0dfd852..e630939 100644 --- a/lib/chat_bots_web/router.ex +++ b/lib/chat_bots_web/router.ex @@ -18,7 +18,8 @@ defmodule ChatBotsWeb.Router do scope "/", ChatBotsWeb do pipe_through :browser - live "/", ChatLive, :chat + # live "/" ChatIndexLive, :index + live "/chat/:id", ChatLive, :chat end defp auth(conn, _opts) do diff --git a/test/chat_bots_web/live/chat_live_test.exs b/test/chat_bots_web/live/chat_live_test.exs index 7574c75..095a282 100644 --- a/test/chat_bots_web/live/chat_live_test.exs +++ b/test/chat_bots_web/live/chat_live_test.exs @@ -1,11 +1,13 @@ defmodule ChatBotsWeb.ChatLiveTest do use ChatBotsWeb.ConnCase, async: false import Mox + import ChatBots.Factory import ChatBots.Fixtures import ChatBots.Fixtures.StabilityAiFixtures import Phoenix.LiveViewTest alias ChatBots.OpenAi.MockClient, as: OpenAiMock + alias ChatBots.Repo alias ChatBots.StabilityAi.MockClient, as: StabilityAiMock setup :verify_on_exit! @@ -20,23 +22,16 @@ defmodule ChatBotsWeb.ChatLiveTest do assert response(conn, 401) end - test "renders the page", %{conn: conn} do - _bot = bot_fixture() - {:ok, _view, html} = live(conn, "/") - assert html =~ "Bot Box" - end + test "loads a chat from the database", %{conn: conn} do + chat = insert(:chat, messages: [%{role: "info", content: "Welcome to Bot Box"}]) - test "has a select to choose the bot", %{conn: conn} do - bot = bot_fixture() - {:ok, view, _html} = live(conn, "/") - - assert has_element?(view, "#bot-select") - assert has_element?(view, "#bot-select option", bot.name) + {:ok, _view, html} = live(conn, "/chat/#{chat.id}") + assert html =~ "Welcome to Bot Box" end test "can enter a message and see it appear in the chat", %{conn: conn} do - _bot = bot_fixture() - {:ok, view, _html} = live(conn, "/") + chat = insert(:chat) + {:ok, view, _html} = live(conn, "/chat/#{chat.id}") message_text = "Hello!" @@ -49,10 +44,11 @@ defmodule ChatBotsWeb.ChatLiveTest do |> render_submit() assert has_element?(view, "#chat-box p.user-bubble", ~r/Hello!/) + chat = Repo.reload(chat) |> Repo.preload(:messages) + assert chat.messages |> Enum.any?(&(&1.role == "user" && &1.content == "Hello!")) end test "can receive and view a response from the bot", %{conn: conn} do - bot_fixture() {:ok, view, _html} = live(conn, "/") message_text = "I am a user" @@ -264,8 +260,7 @@ defmodule ChatBotsWeb.ChatLiveTest do defp expect_chat_api_call(message_sent, message_received \\ "42") do OpenAiMock |> expect(:chat_completion, fn [model: _, messages: messages] -> - assert [_, user_message] = messages - assert user_message == %{role: "user", content: message_sent} + assert %{role: "user", content: ^message_sent} = List.last(messages) api_success_fixture(message_received) end) end From 704441c2e0d76d2b53f1b6e456093a7308457559 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Tue, 16 Apr 2024 08:45:25 -0400 Subject: [PATCH 33/99] Fix broken tests to load chat from db --- lib/chat_bots_web/live/chat_live.ex | 5 +- test/chat_bots_web/live/chat_live_test.exs | 97 ++++++---------------- 2 files changed, 30 insertions(+), 72 deletions(-) diff --git a/lib/chat_bots_web/live/chat_live.ex b/lib/chat_bots_web/live/chat_live.ex index 50a72b5..75b4329 100644 --- a/lib/chat_bots_web/live/chat_live.ex +++ b/lib/chat_bots_web/live/chat_live.ex @@ -36,7 +36,7 @@ defmodule ChatBotsWeb.ChatLive do def handle_info(:request_chat, socket) do %{chat: chat, messages: messages} = socket.assigns - filtered_messages = filter_messages_for_api(messages) + filtered_messages = prepare_messages(messages) case ChatApi.send_message(filtered_messages) do {:ok, message_attrs} -> @@ -73,9 +73,10 @@ defmodule ChatBotsWeb.ChatLive do |> Enum.flat_map(&Parser.parse(&1)) end - defp filter_messages_for_api(messages) do + defp prepare_messages(messages) do messages |> Enum.filter(&(&1.role in ["system", "user", "assistant"])) + |> Enum.map(&Map.take(&1, [:role, :content])) end defp maybe_send_image_request(socket) do diff --git a/test/chat_bots_web/live/chat_live_test.exs b/test/chat_bots_web/live/chat_live_test.exs index 095a282..8d738d3 100644 --- a/test/chat_bots_web/live/chat_live_test.exs +++ b/test/chat_bots_web/live/chat_live_test.exs @@ -14,10 +14,12 @@ defmodule ChatBotsWeb.ChatLiveTest do setup :login_user test "returns 401 when not logged in", %{conn: conn} do + chat = insert(:chat) + conn = conn |> delete_req_header("authorization") - |> get("/") + |> get("/chat/#{chat.id}") assert response(conn, 401) end @@ -49,7 +51,8 @@ defmodule ChatBotsWeb.ChatLiveTest do end test "can receive and view a response from the bot", %{conn: conn} do - {:ok, view, _html} = live(conn, "/") + chat = insert(:chat) + {:ok, view, _html} = live(conn, "/chat/#{chat.id}") message_text = "I am a user" @@ -63,15 +66,15 @@ defmodule ChatBotsWeb.ChatLiveTest do end test "doesn't display system prompt", %{conn: conn} do - _bot = bot_fixture(%{name: "Test Bot", directive: "You are a helpful assistant"}) - {:ok, _view, html} = live(conn, "/") + chat = insert(:chat, messages: [%{role: "system", content: "You are a helpful assistant"}]) + {:ok, _view, html} = live(conn, "/chat/#{chat.id}") refute html =~ "You are a helpful assistant" end test "displays a user-friendly role title for each message", %{conn: conn} do - bot_fixture() - {:ok, view, _html} = live(conn, "/") + chat = insert(:chat) + {:ok, view, _html} = live(conn, "/chat/#{chat.id}") message_text = "I am a user" @@ -85,16 +88,18 @@ defmodule ChatBotsWeb.ChatLiveTest do assert has_element?(view, "#chat-box p.bot-bubble", ~r/I am a bot/) end + @tag :skip test "displays welcome message", %{conn: conn} do - bot = bot_fixture() - {:ok, view, _html} = live(conn, "/") + bot = insert(:bot, name: "Bob Bot") + chat = insert(:chat, bot: bot) + {:ok, view, _html} = live(conn, "/chat/#{chat.id}") - assert has_element?(view, "#chat-box p", ~r/#{bot.name} has entered the chat/) + assert has_element?(view, "#chat-box p", ~r/Bob Bot has entered the chat/) end test "does not send 'info' messages to the API", %{conn: conn} do - bot_fixture() - {:ok, view, _html} = live(conn, "/") + chat = insert(:chat) + {:ok, view, _html} = live(conn, "/chat/#{chat.id}") OpenAiMock |> expect(:chat_completion, fn [model: _, messages: messages] -> @@ -107,60 +112,12 @@ defmodule ChatBotsWeb.ChatLiveTest do |> form("#chat-form", %{"message" => "Hello bot"}) |> render_submit() - assert has_element?(view, "#chat-box p", ~r/Test Bot has entered the chat/) - end - - test "selecting a different bot clears the chat", %{conn: conn} do - _bot1 = bot_fixture(name: "Bot 1", directive: "some directive 1") - bot2 = bot_fixture(name: "Bot 2", directive: "some directive 2") - {:ok, view, _html} = live(conn, "/") - - message_text = "I am a user" - - expect_chat_api_call(message_text, "I am a bot") - - view - |> form("#chat-form", %{"message" => message_text}) - |> render_submit() - - assert has_element?(view, "#chat-box p", ~r/Bot 1 has entered the chat/) - assert has_element?(view, "#chat-box p.user-bubble", ~r/I am a user/) - assert has_element?(view, "#chat-box p.bot-bubble", ~r/I am a bot/) - - view - |> form("#bot-select-form", %{"bot_id" => bot2.id}) - |> render_change() - - refute has_element?(view, "#chat-box p", ~r/Bot 1 has entered the chat/) - refute has_element?(view, "#chat-box p", ~r/I am a user/) - refute has_element?(view, "#chat-box p", ~r/I am a bot/) - assert has_element?(view, "#chat-box p", ~r/Bot 2 has entered the chat/) - end - - test "retains selected bot", %{conn: conn} do - _bot1 = bot_fixture(name: "Bot 1", directive: "some directive 1") - bot2 = bot_fixture(name: "Bot 2", directive: "some directive 2") - {:ok, view, _html} = live(conn, "/") - - view - |> form("#bot-select-form", %{"bot_id" => bot2.id}) - |> render_change() - - assert has_element?(view, "#bot-select option[selected]", bot2.name) - - expect_chat_api_call("Hello") - - view - |> form("#chat-form", %{"message" => "Hello"}) - |> render_submit() - - assert has_element?(view, "#bot-select option[selected]", bot2.name) + :timer.sleep(100) end test "displays error message returned by the API in the chat area", %{conn: conn} do - _bot = bot_fixture() - - {:ok, view, _html} = live(conn, "/") + chat = insert(:chat) + {:ok, view, _html} = live(conn, "/chat/#{chat.id}") OpenAiMock |> expect(:chat_completion, fn _ -> api_error_fixture() end) @@ -173,8 +130,8 @@ defmodule ChatBotsWeb.ChatLiveTest do end test "breaks up mult-line responses into multiple chat bubbles", %{conn: conn} do - bot_fixture() - {:ok, view, _html} = live(conn, "/") + chat = insert(:chat) + {:ok, view, _html} = live(conn, "/chat/#{chat.id}") message_text = "Hello" expect_chat_api_call(message_text, "first line\n\nsecond line") @@ -189,8 +146,8 @@ defmodule ChatBotsWeb.ChatLiveTest do @tag :skip test "displays an Image response as loading", %{conn: conn} do - bot_fixture() - {:ok, view, _html} = live(conn, "/") + chat = insert(:chat) + {:ok, view, _html} = live(conn, "/chat/#{chat.id}") message_text = "Make a picture of a cat" @@ -210,8 +167,8 @@ defmodule ChatBotsWeb.ChatLiveTest do @tag :skip test "displays an Image after the new Bubble", %{conn: conn} do - bot_fixture() - {:ok, view, _html} = live(conn, "/") + chat = insert(:chat) + {:ok, view, _html} = live(conn, "/chat/#{chat.id}") message_text = "Make a picture of a cat" @@ -231,8 +188,8 @@ defmodule ChatBotsWeb.ChatLiveTest do end test "sends image prompts to the StabilityAI API and displays the image returned", %{conn: conn} do - bot_fixture() - {:ok, view, _html} = live(conn, "/") + chat = insert(:chat) + {:ok, view, _html} = live(conn, "/chat/#{chat.id}") message_text = "Make a picture of a cat" From c8c05dbc41fcc4d20eadd95cafc16420e482fa46 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Thu, 18 Apr 2024 09:20:30 -0400 Subject: [PATCH 34/99] Set up HomeLive modules --- lib/chat_bots_web/live/home_live.ex | 24 ++++++++++++++++++++++ lib/chat_bots_web/router.ex | 2 +- test/chat_bots_web/live/home_live_test.exs | 11 ++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 lib/chat_bots_web/live/home_live.ex create mode 100644 test/chat_bots_web/live/home_live_test.exs diff --git a/lib/chat_bots_web/live/home_live.ex b/lib/chat_bots_web/live/home_live.ex new file mode 100644 index 0000000..c605e73 --- /dev/null +++ b/lib/chat_bots_web/live/home_live.ex @@ -0,0 +1,24 @@ +defmodule ChatBotsWeb.HomeLive do + use ChatBotsWeb, :live_view + + alias ChatBots.Chats + + @impl true + def mount(_params, _session, socket) do + {:ok, socket} + end + + @impl true + def render(assigns) do + ~H""" +
+

Chat List

+
    +
  • Chat 1
  • +
  • Chat 2
  • +
  • Chat 3
  • +
+
+ """ + end +end diff --git a/lib/chat_bots_web/router.ex b/lib/chat_bots_web/router.ex index e630939..0f4ff3d 100644 --- a/lib/chat_bots_web/router.ex +++ b/lib/chat_bots_web/router.ex @@ -18,7 +18,7 @@ defmodule ChatBotsWeb.Router do scope "/", ChatBotsWeb do pipe_through :browser - # live "/" ChatIndexLive, :index + live "/", HomeLive, :index live "/chat/:id", ChatLive, :chat end diff --git a/test/chat_bots_web/live/home_live_test.exs b/test/chat_bots_web/live/home_live_test.exs new file mode 100644 index 0000000..2f759cf --- /dev/null +++ b/test/chat_bots_web/live/home_live_test.exs @@ -0,0 +1,11 @@ +defmodule ChatBotsWeb.HomeLiveTest do + use ChatBotsWeb.ConnCase + import Phoenix.LiveViewTest + + test "lists existing chats", %{conn: conn} do + chat = insert(:chat) + {:ok, view, _html} = live(conn, "/") + + assert has_element?(view, "#chat-#{chat.id}") + end +end From 6de91e5bf3f0d44951b2a9332cdcfeb3aa5749d9 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Wed, 24 Apr 2024 15:36:10 -0400 Subject: [PATCH 35/99] list_chats fn --- lib/chat_bots/chats.ex | 10 ++++++++++ lib/chat_bots_web/live/home_live.ex | 2 -- test/chat_bots/chats_test.exs | 14 ++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/lib/chat_bots/chats.ex b/lib/chat_bots/chats.ex index 8fdf589..016026c 100644 --- a/lib/chat_bots/chats.ex +++ b/lib/chat_bots/chats.ex @@ -50,4 +50,14 @@ defmodule ChatBots.Chats do def add_message(messages, message) do messages ++ [message] end + + @doc """ + Lists all chats + Preloads messages + """ + def list_chats do + Chat + |> Repo.all() + |> Repo.preload(:messages) + end end diff --git a/lib/chat_bots_web/live/home_live.ex b/lib/chat_bots_web/live/home_live.ex index c605e73..cab6b6b 100644 --- a/lib/chat_bots_web/live/home_live.ex +++ b/lib/chat_bots_web/live/home_live.ex @@ -1,8 +1,6 @@ defmodule ChatBotsWeb.HomeLive do use ChatBotsWeb, :live_view - alias ChatBots.Chats - @impl true def mount(_params, _session, socket) do {:ok, socket} diff --git a/test/chat_bots/chats_test.exs b/test/chat_bots/chats_test.exs index 22fac8b..7b00708 100644 --- a/test/chat_bots/chats_test.exs +++ b/test/chat_bots/chats_test.exs @@ -66,4 +66,18 @@ defmodule ChatBots.ChatsTest do assert {:ok, %Message{chat_id: ^chat_id, role: "user", content: "Hello"}} = Chats.create_message(chat, attrs) end + + test "list_chats/0 lists all existing chats" do + %{id: id} = insert(:chat) + + assert [%Chat{id: ^id}] = Chats.list_chats() + end + + test "list_chats/0 preloads messages" do + chat = insert(:chat) + %{id: message_id} = insert(:message, chat: chat) + + assert [chat] = Chats.list_chats() + assert [%Message{id: ^message_id}] = chat.messages + end end From ecf9e52cc5ad0de0c08670b20cdcc5887820466c Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Wed, 24 Apr 2024 15:41:28 -0400 Subject: [PATCH 36/99] Add timex --- mix.exs | 3 ++- mix.lock | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 76fa7f4..1f46c5d 100644 --- a/mix.exs +++ b/mix.exs @@ -51,7 +51,8 @@ defmodule ChatBots.MixProject do {:phoenix_ecto, "~> 4.4"}, {:dotenvy, "~> 0.7.0"}, {:req, "~> 0.4.0"}, - {:ex_machina, "~> 2.7.0", only: :test} + {:ex_machina, "~> 2.7.0", only: :test}, + {:timex, "~> 3.0"} ] end diff --git a/mix.lock b/mix.lock index 07537a4..6b9deff 100644 --- a/mix.lock +++ b/mix.lock @@ -2,6 +2,7 @@ "castore": {:hex, :castore, "1.0.1", "240b9edb4e9e94f8f56ab39d8d2d0a57f49e46c56aced8f873892df8ff64ff5a", [:mix], [], "hexpm", "b4951de93c224d44fac71614beabd88b71932d0b1dea80d2f80fb9044e01bbb3"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.7", "77de20ac77f0e53f20ca82c563520af0237c301a1ec3ab3bc598e8a96c7ee5d9", [:mix], [{:elixir_make, "~> 0.7.3", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2768b28bf3c2b4f788c995576b39b8cb5d47eb788526d93bd52206c1d8bf4b75"}, "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, + "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, @@ -16,10 +17,12 @@ "elixir_make": {:hex, :elixir_make, "0.7.6", "67716309dc5d43e16b5abbd00c01b8df6a0c2ab54a8f595468035a50189f9169", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5a0569756b0f7873a77687800c164cca6dfc03a09418e6fcf853d78991f49940"}, "esbuild": {:hex, :esbuild, "0.7.0", "ce3afb13cd2c5fd63e13c0e2d0e0831487a97a7696cfa563707342bb825d122a", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "4ae9f4f237c5ebcb001390b8ada65a12fb2bb04f3fe3d1f1692b7a06fbfe8752"}, "ex_machina": {:hex, :ex_machina, "2.7.0", "b792cc3127fd0680fecdb6299235b4727a4944a09ff0fa904cc639272cd92dc7", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "419aa7a39bde11894c87a615c4ecaa52d8f107bbdd81d810465186f783245bf8"}, + "expo": {:hex, :expo, "0.5.2", "beba786aab8e3c5431813d7a44b828e7b922bfa431d6bfbada0904535342efe2", [:mix], [], "hexpm", "8c9bfa06ca017c9cb4020fabe980bc7fdb1aaec059fd004c2ab3bff03b1c599c"}, "exqlite": {:hex, :exqlite, "0.13.10", "9cddd9b8764b77232d15d83752832cf96ea1066308547346e1e474a93b026810", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "8c688793efbfabd463a2af5b6f4c8ddd35278e402c07e165ee5a52a788aa67ea"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, "floki": {:hex, :floki, "0.34.2", "5fad07ef153b3b8ec110b6b155ec3780c4b2c4906297d0b4be1a7162d04a7e02", [:mix], [], "hexpm", "26b9d50f0f01796bc6be611ca815c5e0de034d2128e39cc9702eee6b66a4d1c8"}, + "gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"}, "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"}, @@ -57,6 +60,8 @@ "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"}, + "timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"}, + "tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, "websock": {:hex, :websock, "0.5.0", "f6bbce90226121d62a0715bca7c986c5e43de0ccc9475d79c55381d1796368cc", [:mix], [], "hexpm", "b51ac706df8a7a48a2c622ee02d09d68be8c40418698ffa909d73ae207eb5fb8"}, "websock_adapter": {:hex, :websock_adapter, "0.5.0", "cea35d8bbf1a6964e32d4b02ceb561dfb769c04f16d60d743885587e7d2ca55b", [:mix], [{:bandit, "~> 0.6", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "16318b124effab8209b1eb7906c636374f623dc9511a8278ad09c083cea5bb83"}, From 6f144d0e809fef9e556550223cce0f68e3291f88 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Wed, 24 Apr 2024 15:57:48 -0400 Subject: [PATCH 37/99] list_chats preloads latest_message for each chat --- lib/chat_bots/chats.ex | 38 +++++++++++++++++++++++++++++++++-- lib/chat_bots/chats/chat.ex | 3 ++- test/chat_bots/chats_test.exs | 10 +++++++++ 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/lib/chat_bots/chats.ex b/lib/chat_bots/chats.ex index 016026c..a3809b6 100644 --- a/lib/chat_bots/chats.ex +++ b/lib/chat_bots/chats.ex @@ -5,6 +5,7 @@ defmodule ChatBots.Chats do alias ChatBots.Repo import Ecto.Changeset + import Ecto.Query @doc """ Creates a new chat for the given bot_id. @@ -56,8 +57,41 @@ defmodule ChatBots.Chats do Preloads messages """ def list_chats do - Chat + preload_query = preload_latest_message_query() + + from(c in Chat, + preload: [:messages, latest_message: ^preload_query] + ) |> Repo.all() - |> Repo.preload(:messages) + end + + # defp preload_latest_direct_message(query) do + # ranking_query = + # from(m in SmsMessage, + # select: %{id: m.id, row_number: over(row_number(), :message_partition)}, + # windows: [message_partition: [partition_by: :thread_id, order_by: [desc: m.inserted_at]]] + # ) + + # latest_direct_message_query = + # from(m in SmsMessage, + # join: r in subquery(ranking_query), + # on: m.id == r.id and r.row_number == 1 + # ) + + # query + # |> preload(latest_direct_message: ^latest_direct_message_query) + # end + + defp preload_latest_message_query do + ranking_query = + from(m in Message, + select: %{id: m.id, row_number: over(row_number(), :message_partition)}, + windows: [message_partition: [partition_by: :chat_id, order_by: [desc: m.inserted_at]]] + ) + + from(m in Message, + join: r in subquery(ranking_query), + on: m.id == r.id and r.row_number == 1 + ) end end diff --git a/lib/chat_bots/chats/chat.ex b/lib/chat_bots/chats/chat.ex index 053c1bc..44165d3 100644 --- a/lib/chat_bots/chats/chat.ex +++ b/lib/chat_bots/chats/chat.ex @@ -5,8 +5,9 @@ defmodule ChatBots.Chats.Chat do @primary_key {:id, :binary_id, autogenerate: true} schema "chats" do - has_many(:messages, Message) belongs_to(:bot, Bot) + has_many(:messages, Message) + has_one(:latest_message, Message) timestamps() end diff --git a/test/chat_bots/chats_test.exs b/test/chat_bots/chats_test.exs index 7b00708..0c0bf92 100644 --- a/test/chat_bots/chats_test.exs +++ b/test/chat_bots/chats_test.exs @@ -80,4 +80,14 @@ defmodule ChatBots.ChatsTest do assert [chat] = Chats.list_chats() assert [%Message{id: ^message_id}] = chat.messages end + + test "list_chats/0 preloads the latest_message for each chat" do + chat = insert(:chat) + %{id: latest_message_id} = insert(:message, chat: chat, inserted_at: Timex.now()) + insert(:message, chat: chat, inserted_at: Timex.now() |> Timex.shift(hours: -1)) + insert(:message, chat: chat, inserted_at: Timex.now() |> Timex.shift(hours: -2)) + + assert [chat] = Chats.list_chats() + assert %Message{id: ^latest_message_id} = chat.latest_message + end end From 1c18df96f5444051a5c956702ca369bfd9b58bc6 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Fri, 26 Apr 2024 16:42:17 -0400 Subject: [PATCH 38/99] list_chats preloads the bot --- lib/chat_bots/chats.ex | 21 +-------------------- test/chat_bots/chats_test.exs | 8 ++++++++ 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/lib/chat_bots/chats.ex b/lib/chat_bots/chats.ex index a3809b6..f90f2f7 100644 --- a/lib/chat_bots/chats.ex +++ b/lib/chat_bots/chats.ex @@ -59,29 +59,10 @@ defmodule ChatBots.Chats do def list_chats do preload_query = preload_latest_message_query() - from(c in Chat, - preload: [:messages, latest_message: ^preload_query] - ) + from(c in Chat, preload: [:bot, :messages, latest_message: ^preload_query]) |> Repo.all() end - # defp preload_latest_direct_message(query) do - # ranking_query = - # from(m in SmsMessage, - # select: %{id: m.id, row_number: over(row_number(), :message_partition)}, - # windows: [message_partition: [partition_by: :thread_id, order_by: [desc: m.inserted_at]]] - # ) - - # latest_direct_message_query = - # from(m in SmsMessage, - # join: r in subquery(ranking_query), - # on: m.id == r.id and r.row_number == 1 - # ) - - # query - # |> preload(latest_direct_message: ^latest_direct_message_query) - # end - defp preload_latest_message_query do ranking_query = from(m in Message, diff --git a/test/chat_bots/chats_test.exs b/test/chat_bots/chats_test.exs index 0c0bf92..3f96342 100644 --- a/test/chat_bots/chats_test.exs +++ b/test/chat_bots/chats_test.exs @@ -90,4 +90,12 @@ defmodule ChatBots.ChatsTest do assert [chat] = Chats.list_chats() assert %Message{id: ^latest_message_id} = chat.latest_message end + + test "list_chats/0 preloads bot" do + bot = insert(:bot, name: "Bob") + chat = insert(:chat, bot: bot) + insert(:message, chat: chat) + + assert [%Chat{bot: ^bot}] = Chats.list_chats() + end end From 24319443ce924c73a0d7f50fd85520e1ce2b7b2e Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Fri, 26 Apr 2024 16:45:19 -0400 Subject: [PATCH 39/99] HomeLive lists chats with bot names --- lib/chat_bots_web/live/home_live.ex | 16 +++++++++------- test/chat_bots_web/live/home_live_test.exs | 15 ++++++++++++++- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/lib/chat_bots_web/live/home_live.ex b/lib/chat_bots_web/live/home_live.ex index cab6b6b..3385445 100644 --- a/lib/chat_bots_web/live/home_live.ex +++ b/lib/chat_bots_web/live/home_live.ex @@ -1,21 +1,23 @@ defmodule ChatBotsWeb.HomeLive do use ChatBotsWeb, :live_view + alias ChatBots.Chats @impl true def mount(_params, _session, socket) do - {:ok, socket} + chats = Chats.list_chats() + + {:ok, assign(socket, chats: chats)} end @impl true def render(assigns) do ~H"""
-

Chat List

-
    -
  • Chat 1
  • -
  • Chat 2
  • -
  • Chat 3
  • -
+ <%= for chat <- @chats do %> +
+ <%= chat.bot.name %> +
+ <% end %>
""" end diff --git a/test/chat_bots_web/live/home_live_test.exs b/test/chat_bots_web/live/home_live_test.exs index 2f759cf..04a7339 100644 --- a/test/chat_bots_web/live/home_live_test.exs +++ b/test/chat_bots_web/live/home_live_test.exs @@ -1,6 +1,9 @@ defmodule ChatBotsWeb.HomeLiveTest do - use ChatBotsWeb.ConnCase + use ChatBotsWeb.ConnCase, async: false import Phoenix.LiveViewTest + import ChatBots.Factory + + setup :login_user test "lists existing chats", %{conn: conn} do chat = insert(:chat) @@ -8,4 +11,14 @@ defmodule ChatBotsWeb.HomeLiveTest do assert has_element?(view, "#chat-#{chat.id}") end + + test "shows the bot's name for each chat", %{conn: conn} do + bot = insert(:bot, name: "Bob") + chat = insert(:chat, bot: bot) + {:ok, view, _html} = live(conn, "/") + + open_browser(view) + + assert has_element?(view, "#chat-#{chat.id}", "Bob") + end end From 20408570b67880678e42bd17200e502fd10f73e1 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Fri, 26 Apr 2024 17:39:40 -0400 Subject: [PATCH 40/99] Show relative time of last message --- lib/chat_bots_web/live/home_live.ex | 15 +++++++++++++-- test/chat_bots_web/live/home_live_test.exs | 18 ++++++++++++++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/lib/chat_bots_web/live/home_live.ex b/lib/chat_bots_web/live/home_live.ex index 3385445..ca04ccb 100644 --- a/lib/chat_bots_web/live/home_live.ex +++ b/lib/chat_bots_web/live/home_live.ex @@ -1,6 +1,7 @@ defmodule ChatBotsWeb.HomeLive do use ChatBotsWeb, :live_view alias ChatBots.Chats + alias Timex.Format.DateTime.Formatters.Relative @impl true def mount(_params, _session, socket) do @@ -13,9 +14,19 @@ defmodule ChatBotsWeb.HomeLive do def render(assigns) do ~H"""
+
+ Bot Box +
<%= for chat <- @chats do %> -
- <%= chat.bot.name %> +
+
😃
+
+
<%= chat.bot.name %>
+
Message text here
+
+
+ <%= Relative.format!(chat.latest_message.inserted_at, "{relative}") %> +
<% end %>
diff --git a/test/chat_bots_web/live/home_live_test.exs b/test/chat_bots_web/live/home_live_test.exs index 04a7339..01ecfd9 100644 --- a/test/chat_bots_web/live/home_live_test.exs +++ b/test/chat_bots_web/live/home_live_test.exs @@ -17,8 +17,22 @@ defmodule ChatBotsWeb.HomeLiveTest do chat = insert(:chat, bot: bot) {:ok, view, _html} = live(conn, "/") - open_browser(view) - assert has_element?(view, "#chat-#{chat.id}", "Bob") end + + test "shows the relative time of the last message on each chat", %{conn: conn} do + chat = insert(:chat) + message = insert(:message, chat: chat, inserted_at: Timex.now() |> Timex.shift(minutes: -5)) + + {:ok, view, _html} = live(conn, "/") + + assert has_element?( + view, + "#chat-#{chat.id} div[data-role=time]", + "5 minutes ago" + ) + end + + test "shows an excerpt of the last message on each chat", %{conn: conn} do + end end From 6b1632b439c47cf100e4a56ed7335503eb115221 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Fri, 26 Apr 2024 17:44:51 -0400 Subject: [PATCH 41/99] Show latest message text for each chat --- lib/chat_bots_web/live/home_live.ex | 2 +- test/chat_bots_web/live/home_live_test.exs | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/chat_bots_web/live/home_live.ex b/lib/chat_bots_web/live/home_live.ex index ca04ccb..8809042 100644 --- a/lib/chat_bots_web/live/home_live.ex +++ b/lib/chat_bots_web/live/home_live.ex @@ -22,7 +22,7 @@ defmodule ChatBotsWeb.HomeLive do
😃
<%= chat.bot.name %>
-
Message text here
+
<%= chat.latest_message.content %>
<%= Relative.format!(chat.latest_message.inserted_at, "{relative}") %> diff --git a/test/chat_bots_web/live/home_live_test.exs b/test/chat_bots_web/live/home_live_test.exs index 01ecfd9..a015aef 100644 --- a/test/chat_bots_web/live/home_live_test.exs +++ b/test/chat_bots_web/live/home_live_test.exs @@ -34,5 +34,15 @@ defmodule ChatBotsWeb.HomeLiveTest do end test "shows an excerpt of the last message on each chat", %{conn: conn} do + chat = insert(:chat) + message = insert(:message, chat: chat, content: "Some witty message") + + {:ok, view, _html} = live(conn, "/") + + assert has_element?( + view, + "#chat-#{chat.id} div[data-role=content]", + "Some witty message" + ) end end From 4a0e6480546141ea75bc85634c2e9349bec1982b Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Fri, 26 Apr 2024 17:56:11 -0400 Subject: [PATCH 42/99] Add bot selector to HomeLive --- lib/chat_bots_web/live/home_live.ex | 15 ++++++++++++++- test/chat_bots_web/live/home_live_test.exs | 8 ++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/chat_bots_web/live/home_live.ex b/lib/chat_bots_web/live/home_live.ex index 8809042..aa694cc 100644 --- a/lib/chat_bots_web/live/home_live.ex +++ b/lib/chat_bots_web/live/home_live.ex @@ -1,13 +1,15 @@ defmodule ChatBotsWeb.HomeLive do use ChatBotsWeb, :live_view + alias ChatBots.Bots alias ChatBots.Chats alias Timex.Format.DateTime.Formatters.Relative @impl true def mount(_params, _session, socket) do chats = Chats.list_chats() + bots = Bots.list_bots() - {:ok, assign(socket, chats: chats)} + {:ok, assign(socket, chats: chats, bots: bots)} end @impl true @@ -17,6 +19,15 @@ defmodule ChatBotsWeb.HomeLive do
Bot Box
+
+ +
<%= for chat <- @chats do %>
😃
@@ -32,4 +43,6 @@ defmodule ChatBotsWeb.HomeLive do
""" end + + defp bot_options(bots), do: Enum.map(bots, &{&1.name, &1.id}) end diff --git a/test/chat_bots_web/live/home_live_test.exs b/test/chat_bots_web/live/home_live_test.exs index a015aef..ca2c583 100644 --- a/test/chat_bots_web/live/home_live_test.exs +++ b/test/chat_bots_web/live/home_live_test.exs @@ -45,4 +45,12 @@ defmodule ChatBotsWeb.HomeLiveTest do "Some witty message" ) end + + test "includes a drop-down list of bots to start a chat with", %{conn: conn} do + bot = insert(:bot, name: "BobBot") + {:ok, view, _html} = live(conn, "/") + + assert has_element?(view, "#bot-select") + assert has_element?(view, "#bot-select option", "BobBot") + end end From 92eae4c7e1ea07fac849510195fc94b1e493252e Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Mon, 29 Apr 2024 11:50:13 -0400 Subject: [PATCH 43/99] HomeLive can create new chat --- lib/chat_bots_web/live/home_live.ex | 12 ++++++++- test/chat_bots_web/live/home_live_test.exs | 30 +++++++++++++++++++--- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/lib/chat_bots_web/live/home_live.ex b/lib/chat_bots_web/live/home_live.ex index aa694cc..f14bdcd 100644 --- a/lib/chat_bots_web/live/home_live.ex +++ b/lib/chat_bots_web/live/home_live.ex @@ -12,6 +12,15 @@ defmodule ChatBotsWeb.HomeLive do {:ok, assign(socket, chats: chats, bots: bots)} end + @impl true + def handle_event("new_chat", %{"bot_id" => bot_id}, socket) do + bot = Bots.get_bot(bot_id) + chat = Chats.create_chat(bot) + socket = push_navigate(socket, to: ~p"/chat/#{chat.id}") + + {:noreply, socket} + end + @impl true def render(assigns) do ~H""" @@ -19,7 +28,7 @@ defmodule ChatBotsWeb.HomeLive do
Bot Box
-
+ +
<%= for chat <- @chats do %>
diff --git a/test/chat_bots_web/live/home_live_test.exs b/test/chat_bots_web/live/home_live_test.exs index ca2c583..7ab2625 100644 --- a/test/chat_bots_web/live/home_live_test.exs +++ b/test/chat_bots_web/live/home_live_test.exs @@ -2,11 +2,14 @@ defmodule ChatBotsWeb.HomeLiveTest do use ChatBotsWeb.ConnCase, async: false import Phoenix.LiveViewTest import ChatBots.Factory + alias ChatBots.Chats.Chat + alias ChatBots.Repo setup :login_user test "lists existing chats", %{conn: conn} do chat = insert(:chat) + insert(:message, chat: chat) {:ok, view, _html} = live(conn, "/") assert has_element?(view, "#chat-#{chat.id}") @@ -15,6 +18,7 @@ defmodule ChatBotsWeb.HomeLiveTest do test "shows the bot's name for each chat", %{conn: conn} do bot = insert(:bot, name: "Bob") chat = insert(:chat, bot: bot) + insert(:message, chat: chat) {:ok, view, _html} = live(conn, "/") assert has_element?(view, "#chat-#{chat.id}", "Bob") @@ -22,7 +26,7 @@ defmodule ChatBotsWeb.HomeLiveTest do test "shows the relative time of the last message on each chat", %{conn: conn} do chat = insert(:chat) - message = insert(:message, chat: chat, inserted_at: Timex.now() |> Timex.shift(minutes: -5)) + insert(:message, chat: chat, inserted_at: Timex.now() |> Timex.shift(minutes: -5)) {:ok, view, _html} = live(conn, "/") @@ -35,7 +39,7 @@ defmodule ChatBotsWeb.HomeLiveTest do test "shows an excerpt of the last message on each chat", %{conn: conn} do chat = insert(:chat) - message = insert(:message, chat: chat, content: "Some witty message") + insert(:message, chat: chat, content: "Some witty message") {:ok, view, _html} = live(conn, "/") @@ -46,11 +50,31 @@ defmodule ChatBotsWeb.HomeLiveTest do ) end + # test "redirects to ChatLive when a chat is clicked", %{conn: conn} do + # end + test "includes a drop-down list of bots to start a chat with", %{conn: conn} do - bot = insert(:bot, name: "BobBot") + insert(:bot, name: "BobBot") {:ok, view, _html} = live(conn, "/") assert has_element?(view, "#bot-select") assert has_element?(view, "#bot-select option", "BobBot") + assert has_element?(view, "#bot-select-form button", "New Chat") + end + + test "can create a new chat and redirect to it", %{conn: conn} do + _bot1 = insert(:bot, name: "Bot 1") + bot2 = %{id: bot2_id} = insert(:bot, name: "Bot 2") + {:ok, view, _html} = live(conn, "/") + + redirect = + view + |> form("#bot-select-form", %{"bot_id" => bot2.id}) + |> render_submit() + + assert {:error, {:live_redirect, %{to: redirect_url}}} = redirect + assert redirect_url =~ "/chat/" + chat_id = redirect_url |> String.split("/") |> List.last() + assert %{bot_id: ^bot2_id} = Repo.get!(Chat, chat_id) end end From 7ad67644d6a2a22f46a688339e5bd7b6fc1529ce Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Tue, 30 Apr 2024 11:40:00 -0400 Subject: [PATCH 44/99] Link to existing chats --- lib/chat_bots_web/live/home_live.ex | 20 +++++++++++--------- test/chat_bots_web/live/home_live_test.exs | 17 +++++++++++++++-- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/lib/chat_bots_web/live/home_live.ex b/lib/chat_bots_web/live/home_live.ex index f14bdcd..cbd3563 100644 --- a/lib/chat_bots_web/live/home_live.ex +++ b/lib/chat_bots_web/live/home_live.ex @@ -39,16 +39,18 @@ defmodule ChatBotsWeb.HomeLive do <%= for chat <- @chats do %> -
-
😃
-
-
<%= chat.bot.name %>
-
<%= chat.latest_message.content %>
+ <.link navigate={~p"/chat/#{chat.id}"} id={"chat-#{chat.id}"}> +
+
😃
+
+
<%= chat.bot.name %>
+
<%= chat.latest_message.content %>
+
+
+ <%= Relative.format!(chat.latest_message.inserted_at, "{relative}") %> +
-
- <%= Relative.format!(chat.latest_message.inserted_at, "{relative}") %> -
-
+ <% end %>
""" diff --git a/test/chat_bots_web/live/home_live_test.exs b/test/chat_bots_web/live/home_live_test.exs index 7ab2625..e531f77 100644 --- a/test/chat_bots_web/live/home_live_test.exs +++ b/test/chat_bots_web/live/home_live_test.exs @@ -50,8 +50,21 @@ defmodule ChatBotsWeb.HomeLiveTest do ) end - # test "redirects to ChatLive when a chat is clicked", %{conn: conn} do - # end + test "redirects to ChatLive when a chat is clicked", %{conn: conn} do + chat = insert(:chat) + insert(:message, chat: chat, content: "Hello") + + {:ok, view, _html} = live(conn, "/") + + open_browser(view) + + view + |> element("#chat-#{chat.id}") + |> render_click() + |> follow_redirect(conn, ~p"/chat/#{chat.id}") + + # assert redirected_to(view, ~p"/chat/#{chat.id}") + end test "includes a drop-down list of bots to start a chat with", %{conn: conn} do insert(:bot, name: "BobBot") From 700f17c3fdda3270c54102455083dc310e47a7cb Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Thu, 2 May 2024 09:56:03 -0400 Subject: [PATCH 45/99] First pass at HomeLive design improvement --- lib/chat_bots_web/live/home_live.ex | 51 +++++++++++++++++------------ 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/lib/chat_bots_web/live/home_live.ex b/lib/chat_bots_web/live/home_live.ex index cbd3563..55f6f46 100644 --- a/lib/chat_bots_web/live/home_live.ex +++ b/lib/chat_bots_web/live/home_live.ex @@ -29,29 +29,38 @@ defmodule ChatBotsWeb.HomeLive do Bot Box
- - +
+ + +
- <%= for chat <- @chats do %> - <.link navigate={~p"/chat/#{chat.id}"} id={"chat-#{chat.id}"}> -
-
😃
-
-
<%= chat.bot.name %>
-
<%= chat.latest_message.content %>
+
+ <%= for chat <- @chats do %> + <.link navigate={~p"/chat/#{chat.id}"} id={"chat-#{chat.id}"}> +
+
😃
+
+
<%= chat.bot.name %>
+
<%= chat.latest_message.content %>
+
+
+ <%= Relative.format!(chat.latest_message.inserted_at, "{relative}") %> +
-
- <%= Relative.format!(chat.latest_message.inserted_at, "{relative}") %> -
-
- - <% end %> + + <% end %> +
""" end From 81424cf74e064f54ad2e7767767a191096d74ccf Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Sun, 5 May 2024 11:06:32 -0400 Subject: [PATCH 46/99] Add bot icon --- lib/chat_bots_web/live/home_live.ex | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/chat_bots_web/live/home_live.ex b/lib/chat_bots_web/live/home_live.ex index 55f6f46..72ee250 100644 --- a/lib/chat_bots_web/live/home_live.ex +++ b/lib/chat_bots_web/live/home_live.ex @@ -49,10 +49,12 @@ defmodule ChatBotsWeb.HomeLive do <%= for chat <- @chats do %> <.link navigate={~p"/chat/#{chat.id}"} id={"chat-#{chat.id}"}>
-
😃
+
<.bot_icon />
<%= chat.bot.name %>
-
<%= chat.latest_message.content %>
+
+ <%= chat.latest_message.content %> +
<%= Relative.format!(chat.latest_message.inserted_at, "{relative}") %> @@ -65,5 +67,22 @@ defmodule ChatBotsWeb.HomeLive do """ end + defp bot_icon(assigns) do + ~H""" + + + + """ + end + defp bot_options(bots), do: Enum.map(bots, &{&1.name, &1.id}) end From 8c2ac62ae2dc7060bc8072b8ae70a9be8fc6e576 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Thu, 9 May 2024 09:51:04 -0400 Subject: [PATCH 47/99] Store errors and image messages to db --- lib/chat_bots_web/live/chat_live.ex | 27 +++++++++++++--------- test/chat_bots_web/live/chat_live_test.exs | 5 ++++ test/chat_bots_web/live/home_live_test.exs | 2 -- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/lib/chat_bots_web/live/chat_live.ex b/lib/chat_bots_web/live/chat_live.ex index 75b4329..5967019 100644 --- a/lib/chat_bots_web/live/chat_live.ex +++ b/lib/chat_bots_web/live/chat_live.ex @@ -3,7 +3,6 @@ defmodule ChatBotsWeb.ChatLive do alias ChatBots.Chats alias ChatBots.Chats.Bubble alias ChatBots.Chats.Image - alias ChatBots.Chats.Message alias ChatBots.OpenAi.Api, as: ChatApi alias ChatBots.StabilityAi.Api, as: ImageApi alias ChatBots.Parser @@ -49,22 +48,21 @@ defmodule ChatBotsWeb.ChatLive do |> maybe_send_image_request()} {:error, error} -> - messages = - Chats.add_message(socket.assigns.messages, %Message{ - role: "error", - content: error["message"] - }) - - {:noreply, assign(socket, messages: messages, loading: false)} + {:noreply, + socket + |> add_message(%{role: "error", content: error["message"]}) + |> assign(socket, loading: false)} end end def handle_info({:request_image, image_prompt}, socket) do {:ok, file} = ImageApi.generate_image(image_prompt) image_attrs = %{file: file, prompt: image_prompt} - image_message = %{role: "image", content: Jason.encode!(image_attrs)} - messages = Chats.add_message(socket.assigns.messages, image_message) - {:noreply, assign(socket, messages: messages, loading: false)} + message_attrs = %{role: "image", content: Jason.encode!(image_attrs)} + + {:noreply, + add_message(socket, message_attrs) + |> assign(socket, loading: false)} end defp convert_messages_to_chat_items(messages) do @@ -93,6 +91,13 @@ defmodule ChatBotsWeb.ChatLive do end end + defp add_message(socket, message_attrs) do + {:ok, message} = Chats.create_message(socket.assigns.chat, message_attrs) + messages = socket.assigns.messages ++ [message] + + assign(socket, messages: messages) + end + def render(assigns) do ~H"""

diff --git a/test/chat_bots_web/live/chat_live_test.exs b/test/chat_bots_web/live/chat_live_test.exs index 8d738d3..999acde 100644 --- a/test/chat_bots_web/live/chat_live_test.exs +++ b/test/chat_bots_web/live/chat_live_test.exs @@ -127,6 +127,8 @@ defmodule ChatBotsWeb.ChatLiveTest do |> render_submit() assert has_element?(view, "#chat-box p", ~r/Error.*Invalid request/) + chat = Repo.preload(chat, :messages) + assert chat.messages |> Enum.any?(&(&1.role == "error")) end test "breaks up mult-line responses into multiple chat bubbles", %{conn: conn} do @@ -211,6 +213,9 @@ defmodule ChatBotsWeb.ChatLiveTest do file_name = expected_file_name(12345) assert has_element?(view, "img[src='/images/#{file_name}']") + + chat = Repo.preload(chat, :messages) + assert chat.messages |> Enum.any?(&(&1.role == "image")) end # Set up the mock and assert the message is sent to the client with message_text diff --git a/test/chat_bots_web/live/home_live_test.exs b/test/chat_bots_web/live/home_live_test.exs index e531f77..113ec5d 100644 --- a/test/chat_bots_web/live/home_live_test.exs +++ b/test/chat_bots_web/live/home_live_test.exs @@ -56,8 +56,6 @@ defmodule ChatBotsWeb.HomeLiveTest do {:ok, view, _html} = live(conn, "/") - open_browser(view) - view |> element("#chat-#{chat.id}") |> render_click() From a70bf6b9675f4407e654f7f115fc562b14f95609 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Fri, 10 May 2024 10:00:25 -0400 Subject: [PATCH 48/99] Add test for parsing an image_prompt (no UI content) --- test/chat_bots/parser_test.exs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/chat_bots/parser_test.exs b/test/chat_bots/parser_test.exs index a597f48..d869650 100644 --- a/test/chat_bots/parser_test.exs +++ b/test/chat_bots/parser_test.exs @@ -46,6 +46,13 @@ defmodule ChatBots.ParserTest do ] = Parser.parse(response) end + test "returns nil if there is no UI content in the response (such as an image prompt)" do + response = + %{role: "assistant", content: %{image_prompt: "An image of a cat"}} |> make_json_message() + + assert [] = Parser.parse(response) + end + test "parses a message from a text response containing only a number" do response = %{ role: "assistant", From 105c8872e1a409f2bd61a3e4e020075a9b16a5da Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Tue, 14 May 2024 10:06:38 -0400 Subject: [PATCH 49/99] Parse and show ImageRequests --- lib/chat_bots/parser.ex | 15 ++-------- lib/chat_bots_web/live/chat_live.ex | 19 ++++++++++-- test/chat_bots/parser_test.exs | 35 +++++----------------- test/chat_bots_web/live/chat_live_test.exs | 22 ++++++++++++++ 4 files changed, 49 insertions(+), 42 deletions(-) diff --git a/lib/chat_bots/parser.ex b/lib/chat_bots/parser.ex index e0d0bc6..bab60e5 100644 --- a/lib/chat_bots/parser.ex +++ b/lib/chat_bots/parser.ex @@ -4,6 +4,7 @@ defmodule ChatBots.Parser do """ alias ChatBots.Chats.Bubble alias ChatBots.Chats.Image + alias ChatBots.Chats.ImageRequest @doc """ Parses a chat response into a list of chat items @@ -50,17 +51,7 @@ defmodule ChatBots.Parser do [%Bubble{type: "bot", text: "#{response}"}] end - defp parse_chat_item(_), do: [] - - @doc """ - Parses an image_prompt if present in the JSON response - """ - def parse_image_prompt(%{content: content, role: "assistant"}) do - maybe_decode_json(content) - |> parse_image_prompt() - end + defp parse_chat_item({"image_prompt", prompt}), do: [%ImageRequest{prompt: prompt}] - def parse_image_prompt(%{"image_prompt" => prompt}), do: prompt - - def parse_image_prompt(_), do: nil + defp parse_chat_item(_), do: [] end diff --git a/lib/chat_bots_web/live/chat_live.ex b/lib/chat_bots_web/live/chat_live.ex index 5967019..8535ac2 100644 --- a/lib/chat_bots_web/live/chat_live.ex +++ b/lib/chat_bots_web/live/chat_live.ex @@ -3,6 +3,7 @@ defmodule ChatBotsWeb.ChatLive do alias ChatBots.Chats alias ChatBots.Chats.Bubble alias ChatBots.Chats.Image + alias ChatBots.Chats.ImageRequest alias ChatBots.OpenAi.Api, as: ChatApi alias ChatBots.StabilityAi.Api, as: ImageApi alias ChatBots.Parser @@ -79,14 +80,18 @@ defmodule ChatBotsWeb.ChatLive do defp maybe_send_image_request(socket) do # check the latest message for an image prompt - image_prompt = socket.assigns.messages |> List.last() |> Parser.parse_image_prompt() + image_request = + socket.assigns.messages + |> List.last() + |> Parser.parse() + |> Enum.find(&is_struct(&1, ImageRequest)) - case image_prompt do + case image_request do nil -> socket _ -> - send(self(), {:request_image, image_prompt}) + send(self(), {:request_image, image_request.prompt}) assign(socket, loading: true) end end @@ -147,6 +152,14 @@ defmodule ChatBotsWeb.ChatLive do """ end + defp render_chat_item(%{item: %ImageRequest{}} = assigns) do + ~H""" +

+ <%= @item.prompt %> +

+ """ + end + defp render_chat_item(%{item: %Image{}} = assigns) do ~H"""
diff --git a/test/chat_bots/parser_test.exs b/test/chat_bots/parser_test.exs index d869650..6260364 100644 --- a/test/chat_bots/parser_test.exs +++ b/test/chat_bots/parser_test.exs @@ -3,6 +3,7 @@ defmodule ChatBots.ParserTest do alias ChatBots.Chats.Bubble alias ChatBots.Chats.Image + alias ChatBots.Chats.ImageRequest alias ChatBots.Chats.Message alias ChatBots.Parser @@ -46,13 +47,6 @@ defmodule ChatBots.ParserTest do ] = Parser.parse(response) end - test "returns nil if there is no UI content in the response (such as an image prompt)" do - response = - %{role: "assistant", content: %{image_prompt: "An image of a cat"}} |> make_json_message() - - assert [] = Parser.parse(response) - end - test "parses a message from a text response containing only a number" do response = %{ role: "assistant", @@ -87,28 +81,15 @@ defmodule ChatBots.ParserTest do assert [%Image{file: "/path/to/image.jpg", prompt: "An image of a cat"}] = Parser.parse(response) end - end - describe "parse_image_prompt/1" do - test "parses an image response from a JSON response" do - response = make_json_message(%{image_prompt: "An image of a cat"}) - - assert "An image of a cat" = Parser.parse_image_prompt(response) - end - - test "returns nil if the image_prompt key is not present" do - response = make_json_message(%{text: "Hello, world!"}) - - assert Parser.parse_image_prompt(response) |> is_nil() - end - - test "returns nil if the message is not a JSON response" do - response = %{ - role: "assistant", - content: "Hello, world!" - } + test "can parse an image request from a JSON response" do + response = + %{ + role: "assistant", + content: Jason.encode!(%{image_prompt: "An image of a cat"}) + } - assert Parser.parse_image_prompt(response) |> is_nil() + assert [%ImageRequest{prompt: "An image of a cat"}] = Parser.parse(response) end end diff --git a/test/chat_bots_web/live/chat_live_test.exs b/test/chat_bots_web/live/chat_live_test.exs index 999acde..a5b39ec 100644 --- a/test/chat_bots_web/live/chat_live_test.exs +++ b/test/chat_bots_web/live/chat_live_test.exs @@ -189,6 +189,28 @@ defmodule ChatBotsWeb.ChatLiveTest do :timer.sleep(100) end + test "displays image prompts from the API", %{conn: conn} do + chat = insert(:chat) + {:ok, view, _html} = live(conn, "/chat/#{chat.id}") + + message_text = "Make a picture of a cat" + + chat_response = %{ + text: "here is your picture", + image_prompt: "A picture of a cat" + } + + expect_chat_api_call(message_text, chat_response) + expect_image_api_call("A picture of a cat") + + view + |> form("#chat-form", %{"message" => message_text}) + |> render_submit() + + assert has_element?(view, "p.bot-bubble", ~r/here is your picture/) + assert has_element?(view, "p.bot-bubble", ~r/A picture of a cat/) + end + test "sends image prompts to the StabilityAI API and displays the image returned", %{conn: conn} do chat = insert(:chat) {:ok, view, _html} = live(conn, "/chat/#{chat.id}") From 76f1f8d554663aed87dbbf8eb4015ef1542957f5 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Wed, 15 May 2024 10:12:25 -0400 Subject: [PATCH 50/99] Test for parsing multiple elements from single message --- test/chat_bots/parser_test.exs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/chat_bots/parser_test.exs b/test/chat_bots/parser_test.exs index 6260364..0eb12cf 100644 --- a/test/chat_bots/parser_test.exs +++ b/test/chat_bots/parser_test.exs @@ -91,6 +91,19 @@ defmodule ChatBots.ParserTest do assert [%ImageRequest{prompt: "An image of a cat"}] = Parser.parse(response) end + + test "can parse multiple elements from a single JSON response" do + response = + %{ + role: "assistant", + content: Jason.encode!(%{text: "Here you go", image_prompt: "An image of a cat"}) + } + + assert [ + %ImageRequest{prompt: "An image of a cat"}, + %Bubble{type: "bot", text: "Here you go"} + ] = Parser.parse(response) + end end defp make_json_message(%{role: role, content: content}) do From e1f763b6ea50b6f3e12fa336a95a27a841426045 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Wed, 15 May 2024 10:24:14 -0400 Subject: [PATCH 51/99] Parser can parse a Choice --- lib/chat_bots/chats/choice.ex | 3 +++ lib/chat_bots/parser.ex | 5 +++++ test/chat_bots/parser_test.exs | 11 +++++++++++ 3 files changed, 19 insertions(+) create mode 100644 lib/chat_bots/chats/choice.ex diff --git a/lib/chat_bots/chats/choice.ex b/lib/chat_bots/chats/choice.ex new file mode 100644 index 0000000..7b5a0a0 --- /dev/null +++ b/lib/chat_bots/chats/choice.ex @@ -0,0 +1,3 @@ +defmodule ChatBots.Chats.Choice do + defstruct [:options] +end diff --git a/lib/chat_bots/parser.ex b/lib/chat_bots/parser.ex index bab60e5..70e407b 100644 --- a/lib/chat_bots/parser.ex +++ b/lib/chat_bots/parser.ex @@ -2,6 +2,7 @@ defmodule ChatBots.Parser do @moduledoc """ Parses messages from the chat API into chat items to be displayed in the chat window. """ + alias ChatBots.Chats.Choice alias ChatBots.Chats.Bubble alias ChatBots.Chats.Image alias ChatBots.Chats.ImageRequest @@ -53,5 +54,9 @@ defmodule ChatBots.Parser do defp parse_chat_item({"image_prompt", prompt}), do: [%ImageRequest{prompt: prompt}] + defp parse_chat_item({"options", options}) do + [%Choice{options: options}] + end + defp parse_chat_item(_), do: [] end diff --git a/test/chat_bots/parser_test.exs b/test/chat_bots/parser_test.exs index 0eb12cf..901b6e8 100644 --- a/test/chat_bots/parser_test.exs +++ b/test/chat_bots/parser_test.exs @@ -2,6 +2,7 @@ defmodule ChatBots.ParserTest do use ChatBots.DataCase, async: true alias ChatBots.Chats.Bubble + alias ChatBots.Chats.Choice alias ChatBots.Chats.Image alias ChatBots.Chats.ImageRequest alias ChatBots.Chats.Message @@ -104,6 +105,16 @@ defmodule ChatBots.ParserTest do %Bubble{type: "bot", text: "Here you go"} ] = Parser.parse(response) end + + test "can parse a choice from a JSON response" do + response = + %{ + role: "assistant", + content: Jason.encode!(%{options: ["Yes", "No"]}) + } + + assert [%Choice{options: ["Yes", "No"]}] = Parser.parse(response) + end end defp make_json_message(%{role: role, content: content}) do From 32bc2fc4ea4226561ba7e70d46623ba1cc97ec59 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Thu, 16 May 2024 10:02:49 -0400 Subject: [PATCH 52/99] ChatLive can render a Choice as buttons --- lib/chat_bots_web/live/chat_live.ex | 15 +++++++++++++++ test/chat_bots_web/live/chat_live_test.exs | 22 ++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/lib/chat_bots_web/live/chat_live.ex b/lib/chat_bots_web/live/chat_live.ex index 8535ac2..24f5c75 100644 --- a/lib/chat_bots_web/live/chat_live.ex +++ b/lib/chat_bots_web/live/chat_live.ex @@ -2,6 +2,7 @@ defmodule ChatBotsWeb.ChatLive do use ChatBotsWeb, :live_view alias ChatBots.Chats alias ChatBots.Chats.Bubble + alias ChatBots.Chats.Choice alias ChatBots.Chats.Image alias ChatBots.Chats.ImageRequest alias ChatBots.OpenAi.Api, as: ChatApi @@ -172,6 +173,20 @@ defmodule ChatBotsWeb.ChatLive do """ end + defp render_chat_item(%{item: %Choice{}} = assigns) do + ~H""" +
+ <%= for option <- @item.options do %> +
+ +
+ <% end %> +
+ """ + end + defp get_message_classes(type) do base_classes = "p-2 my-2 rounded-lg text-sm w-auto max-w-md" diff --git a/test/chat_bots_web/live/chat_live_test.exs b/test/chat_bots_web/live/chat_live_test.exs index a5b39ec..05ecf8a 100644 --- a/test/chat_bots_web/live/chat_live_test.exs +++ b/test/chat_bots_web/live/chat_live_test.exs @@ -211,6 +211,28 @@ defmodule ChatBotsWeb.ChatLiveTest do assert has_element?(view, "p.bot-bubble", ~r/A picture of a cat/) end + test "displays a Choice as a list of buttons", %{conn: conn} do + chat = insert(:chat) + {:ok, view, _html} = live(conn, "/chat/#{chat.id}") + + message_text = "What is your favorite color?" + + chat_response = %{ + text: "Choose a color", + options: ["Red", "Green", "Blue"] + } + + expect_chat_api_call(message_text, chat_response) + + view + |> form("#chat-form", %{"message" => message_text}) + |> render_submit() + + assert has_element?(view, "button", ~r/Red/) + assert has_element?(view, "button", ~r/Green/) + assert has_element?(view, "button", ~r/Blue/) + end + test "sends image prompts to the StabilityAI API and displays the image returned", %{conn: conn} do chat = insert(:chat) {:ok, view, _html} = live(conn, "/chat/#{chat.id}") From 804eb49ba53865901cb5f77e7e5ef928dfe54ed2 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Fri, 17 May 2024 09:52:13 -0400 Subject: [PATCH 53/99] Clicking a button sends the choice to the chat API --- lib/chat_bots_web/live/chat_live.ex | 19 +++++++++++++++- test/chat_bots_web/live/chat_live_test.exs | 26 ++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/lib/chat_bots_web/live/chat_live.ex b/lib/chat_bots_web/live/chat_live.ex index 24f5c75..d8f4f3c 100644 --- a/lib/chat_bots_web/live/chat_live.ex +++ b/lib/chat_bots_web/live/chat_live.ex @@ -35,6 +35,19 @@ defmodule ChatBotsWeb.ChatLive do {:noreply, socket} end + def handle_event("choice_clicked", %{"option" => option}, socket) do + # send a message to self to trigger the API call in the background + send(self(), :request_chat) + + # add user choice to messages + {:ok, message} = Chats.create_message(socket.assigns.chat, %{role: "user", content: option}) + + messages = socket.assigns.messages ++ [message] + + socket = assign(socket, messages: messages, loading: true) + {:noreply, socket} + end + def handle_info(:request_chat, socket) do %{chat: chat, messages: messages} = socket.assigns filtered_messages = prepare_messages(messages) @@ -178,7 +191,11 @@ defmodule ChatBotsWeb.ChatLive do
<%= for option <- @item.options do %>
-
diff --git a/test/chat_bots_web/live/chat_live_test.exs b/test/chat_bots_web/live/chat_live_test.exs index 05ecf8a..29da0c2 100644 --- a/test/chat_bots_web/live/chat_live_test.exs +++ b/test/chat_bots_web/live/chat_live_test.exs @@ -233,6 +233,32 @@ defmodule ChatBotsWeb.ChatLiveTest do assert has_element?(view, "button", ~r/Blue/) end + test "clicking a Choice button sends the option text to the API", %{conn: conn} do + chat = insert(:chat) + + insert(:message, + chat: chat, + role: "assistant", + content: + Jason.encode!(%{ + text: "Choose a color", + options: ["Red", "Green", "Blue"] + }) + ) + + {:ok, view, _html} = live(conn, "/chat/#{chat.id}") + + # "Red" gets sent to the API + expect_chat_api_call("Red", "You chose Red") + + view + |> element("button", "Red") + |> render_click() + + # "Red" is displayed as a user message + assert has_element?(view, "p.user-bubble", ~r/Red/) + end + test "sends image prompts to the StabilityAI API and displays the image returned", %{conn: conn} do chat = insert(:chat) {:ok, view, _html} = live(conn, "/chat/#{chat.id}") From f1d12d00e647ef1af6061b92459d921c24f12a59 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Sat, 18 May 2024 09:34:25 -0400 Subject: [PATCH 54/99] Make choice click use submit_message event --- lib/chat_bots_web/live/chat_live.ex | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/lib/chat_bots_web/live/chat_live.ex b/lib/chat_bots_web/live/chat_live.ex index d8f4f3c..e998896 100644 --- a/lib/chat_bots_web/live/chat_live.ex +++ b/lib/chat_bots_web/live/chat_live.ex @@ -31,21 +31,7 @@ defmodule ChatBotsWeb.ChatLive do messages = socket.assigns.messages ++ [message] - socket = assign(socket, messages: messages, loading: true) - {:noreply, socket} - end - - def handle_event("choice_clicked", %{"option" => option}, socket) do - # send a message to self to trigger the API call in the background - send(self(), :request_chat) - - # add user choice to messages - {:ok, message} = Chats.create_message(socket.assigns.chat, %{role: "user", content: option}) - - messages = socket.assigns.messages ++ [message] - - socket = assign(socket, messages: messages, loading: true) - {:noreply, socket} + {:noreply, assign(socket, messages: messages, loading: true)} end def handle_info(:request_chat, socket) do @@ -139,7 +125,7 @@ defmodule ChatBotsWeb.ChatLive do id="message" name="message" rows="1" - placeholder="Type a messsage..." + placeholder="Type a message..." class="flex-grow bg-white border border-gray-300 rounded-lg p-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" > @@ -193,8 +179,8 @@ defmodule ChatBotsWeb.ChatLive do
From 8459d2c2850ba7a4da0aedaccb4681a646b63896 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Sat, 18 May 2024 09:50:16 -0400 Subject: [PATCH 55/99] Show text before choices --- lib/chat_bots/parser.ex | 5 +++++ test/chat_bots/parser_test.exs | 23 +++++++++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/lib/chat_bots/parser.ex b/lib/chat_bots/parser.ex index 70e407b..e9578fc 100644 --- a/lib/chat_bots/parser.ex +++ b/lib/chat_bots/parser.ex @@ -34,12 +34,17 @@ defmodule ChatBots.Parser do defp gather_chat_items(content_map) when is_map(content_map) do content_map |> Map.to_list() + |> Enum.sort_by(&chat_item_priority/1) |> Enum.map(&parse_chat_item/1) |> List.flatten() end defp gather_chat_items(content_map), do: parse_chat_item(content_map) + defp chat_item_priority({"text", _}), do: 1 + defp chat_item_priority({"options", _}), do: 2 + defp chat_item_priority({"image_prompt", _}), do: 3 + defp parse_chat_item({"text", response}), do: parse_chat_item(response) defp parse_chat_item(response) when is_binary(response) do diff --git a/test/chat_bots/parser_test.exs b/test/chat_bots/parser_test.exs index 901b6e8..c88f16d 100644 --- a/test/chat_bots/parser_test.exs +++ b/test/chat_bots/parser_test.exs @@ -101,8 +101,8 @@ defmodule ChatBots.ParserTest do } assert [ - %ImageRequest{prompt: "An image of a cat"}, - %Bubble{type: "bot", text: "Here you go"} + %Bubble{type: "bot", text: "Here you go"}, + %ImageRequest{prompt: "An image of a cat"} ] = Parser.parse(response) end @@ -115,6 +115,25 @@ defmodule ChatBots.ParserTest do assert [%Choice{options: ["Yes", "No"]}] = Parser.parse(response) end + + test "returns text before image prompts and choices" do + response = + %{ + role: "assistant", + content: + Jason.encode!(%{ + image_prompt: "picture of a cat and a dog", + options: ["Cats", "Dogs"], + text: "Do you like cats or dogs?" + }) + } + + assert [ + %Bubble{type: "bot", text: "Do you like cats or dogs?"}, + %Choice{options: ["Cats", "Dogs"]}, + %ImageRequest{prompt: "picture of a cat and a dog"} + ] = Parser.parse(response) + end end defp make_json_message(%{role: role, content: content}) do From c034a70b7a95900fcfc6bfe4327d31cc42fed1d7 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Sun, 19 May 2024 09:57:59 -0400 Subject: [PATCH 56/99] Adjust bot bubble and button colors/layout --- lib/chat_bots_web/live/chat_live.ex | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/chat_bots_web/live/chat_live.ex b/lib/chat_bots_web/live/chat_live.ex index e998896..69e50d8 100644 --- a/lib/chat_bots_web/live/chat_live.ex +++ b/lib/chat_bots_web/live/chat_live.ex @@ -174,11 +174,11 @@ defmodule ChatBotsWeb.ChatLive do defp render_chat_item(%{item: %Choice{}} = assigns) do ~H""" -
+
<%= for option <- @item.options do %>

-
+
<%= for chat_item <- convert_messages_to_chat_items(@messages) do %> <.render_chat_item item={chat_item} /> <% end %> @@ -184,7 +184,7 @@ defmodule ChatBotsWeb.ChatLive do end defp get_message_classes(type) do - base_classes = "p-2 my-2 rounded-lg text-sm w-auto max-w-md" + base_classes = "p-2 rounded-lg text-sm w-auto max-w-md" case type do "user" -> From dcb39aeca62016682159c1fc67760efe9ed93698 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Tue, 21 May 2024 09:52:49 -0400 Subject: [PATCH 60/99] Fix skipped tests --- test/chat_bots_web/live/chat_live_test.exs | 42 ++++------------------ 1 file changed, 6 insertions(+), 36 deletions(-) diff --git a/test/chat_bots_web/live/chat_live_test.exs b/test/chat_bots_web/live/chat_live_test.exs index 78919fa..7be574b 100644 --- a/test/chat_bots_web/live/chat_live_test.exs +++ b/test/chat_bots_web/live/chat_live_test.exs @@ -102,15 +102,6 @@ defmodule ChatBotsWeb.ChatLiveTest do assert has_element?(view, "#chat-box p.bot-bubble", ~r/I am a bot/) end - @tag :skip - test "displays welcome message", %{conn: conn} do - bot = insert(:bot, name: "Bob Bot") - chat = insert(:chat, bot: bot) - {:ok, view, _html} = live(conn, "/chat/#{chat.id}") - - assert has_element?(view, "#chat-box p", ~r/Bob Bot has entered the chat/) - end - test "does not send 'info' messages to the API", %{conn: conn} do chat = insert(:chat) {:ok, view, _html} = live(conn, "/chat/#{chat.id}") @@ -160,8 +151,7 @@ defmodule ChatBotsWeb.ChatLiveTest do assert has_element?(view, "p.bot-bubble", ~r"\Asecond line\z") end - @tag :skip - test "displays an Image response as loading", %{conn: conn} do + test "displays loading animation while retrieving an image", %{conn: conn} do chat = insert(:chat) {:ok, view, _html} = live(conn, "/chat/#{chat.id}") @@ -178,10 +168,11 @@ defmodule ChatBotsWeb.ChatLiveTest do |> form("#chat-form", %{"message" => message_text}) |> render_submit() - assert has_element?(view, ".chat-image", "loading") + :timer.sleep(2) + + assert has_element?(view, "div.loader") end - @tag :skip test "displays an Image after the new Bubble", %{conn: conn} do chat = insert(:chat) {:ok, view, _html} = live(conn, "/chat/#{chat.id}") @@ -199,30 +190,9 @@ defmodule ChatBotsWeb.ChatLiveTest do |> form("#chat-form", %{"message" => message_text}) |> render_submit() - assert render(view) =~ ~r"here is your picture.*chat-image" - :timer.sleep(100) - end - - test "displays image prompts from the API", %{conn: conn} do - chat = insert(:chat) - {:ok, view, _html} = live(conn, "/chat/#{chat.id}") - - message_text = "Make a picture of a cat" - - chat_response = %{ - text: "here is your picture", - image_prompt: "A picture of a cat" - } + :timer.sleep(10) - expect_chat_api_call(message_text, chat_response) - expect_image_api_call("A picture of a cat") - - view - |> form("#chat-form", %{"message" => message_text}) - |> render_submit() - - assert has_element?(view, "p.bot-bubble", ~r/here is your picture/) - assert has_element?(view, "p.bot-bubble", ~r/A picture of a cat/) + assert render(view) =~ ~r"here is your picture.*chat-image" end test "displays a Choice as a list of buttons", %{conn: conn} do From b036587f52ffa8b6b3464d19d19b20b25fc10c7f Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Wed, 22 May 2024 10:07:24 -0400 Subject: [PATCH 61/99] Fix loading animation not turning off after image gen --- lib/chat_bots_web/live/chat_live.ex | 6 ++--- test/chat_bots_web/live/chat_live_test.exs | 26 +++++++++++++++++++++- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/lib/chat_bots_web/live/chat_live.ex b/lib/chat_bots_web/live/chat_live.ex index 3ef9783..50b9c58 100644 --- a/lib/chat_bots_web/live/chat_live.ex +++ b/lib/chat_bots_web/live/chat_live.ex @@ -52,7 +52,7 @@ defmodule ChatBotsWeb.ChatLive do {:noreply, socket |> add_message(%{role: "error", content: error["message"]}) - |> assign(socket, loading: false)} + |> assign(loading: false)} end end @@ -61,9 +61,7 @@ defmodule ChatBotsWeb.ChatLive do image_attrs = %{file: file, prompt: image_prompt} message_attrs = %{role: "image", content: Jason.encode!(image_attrs)} - {:noreply, - add_message(socket, message_attrs) - |> assign(socket, loading: false)} + {:noreply, add_message(socket, message_attrs) |> assign(loading: false)} end defp convert_messages_to_chat_items(messages) do diff --git a/test/chat_bots_web/live/chat_live_test.exs b/test/chat_bots_web/live/chat_live_test.exs index 7be574b..127f5a6 100644 --- a/test/chat_bots_web/live/chat_live_test.exs +++ b/test/chat_bots_web/live/chat_live_test.exs @@ -151,6 +151,8 @@ defmodule ChatBotsWeb.ChatLiveTest do assert has_element?(view, "p.bot-bubble", ~r"\Asecond line\z") end + # TODO: figure out how to test this + @tag :skip test "displays loading animation while retrieving an image", %{conn: conn} do chat = insert(:chat) {:ok, view, _html} = live(conn, "/chat/#{chat.id}") @@ -168,11 +170,33 @@ defmodule ChatBotsWeb.ChatLiveTest do |> form("#chat-form", %{"message" => message_text}) |> render_submit() - :timer.sleep(2) + :timer.sleep(10) assert has_element?(view, "div.loader") end + test "stops loading animation after image response is received", %{conn: conn} do + chat = insert(:chat) + {:ok, view, _html} = live(conn, "/chat/#{chat.id}") + + message_text = "Make a picture of a cat" + + expect_chat_api_call(message_text, %{ + text: "here is your picture", + image_prompt: "A picture of a cat" + }) + + expect_image_api_call("A picture of a cat") + + view + |> form("#chat-form", %{"message" => message_text}) + |> render_submit() + + _ = :sys.get_state(view.pid) + + refute has_element?(view, "div.loader") + end + test "displays an Image after the new Bubble", %{conn: conn} do chat = insert(:chat) {:ok, view, _html} = live(conn, "/chat/#{chat.id}") From 83c98a94c00b6ed1795565d7d41340f45a16c22f Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Sat, 25 May 2024 10:26:59 -0400 Subject: [PATCH 62/99] Config persistent storage for generated images on Fly --- config/dev.exs | 2 +- config/runtime.exs | 4 ++-- lib/chat_bots/seeder.ex | 3 ++- lib/chat_bots_web/endpoint.ex | 8 ++++++++ lib/chat_bots_web/live/chat_live.ex | 2 +- 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/config/dev.exs b/config/dev.exs index 479399c..e15334b 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -73,4 +73,4 @@ config :phoenix, :stacktrace_depth, 20 config :phoenix, :plug_init_mode, :runtime # path for downloading images -config :chat_bots, download_path: "priv/static/images" +config :chat_bots, download_path: "priv/static/images/generated" diff --git a/config/runtime.exs b/config/runtime.exs index 7d7d5b9..6868548 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -72,8 +72,8 @@ if config_env() == :prod do # get password from env (will raise if not set) config :chat_bots, :auth, password: System.fetch_env!("USER_PASSWORD") - # path for downloading images - config :chat_bots, download_path: Application.app_dir(:chat_bots) <> "/priv/static/images" + # path for downloading generated images + config :chat_bots, download_path: "/data/generated-images" # ## SSL Support # diff --git a/lib/chat_bots/seeder.ex b/lib/chat_bots/seeder.ex index a642a38..639f85c 100644 --- a/lib/chat_bots/seeder.ex +++ b/lib/chat_bots/seeder.ex @@ -53,11 +53,12 @@ defmodule ChatBots.Seeder do """ }, %Bot{ - name: "#DayJob", + name: "DayJob", directive: """ You are a Stable Diffusion prompt generator. I will give you the name of a super hero. You will generate a prompt for a Stable Diffusion showing an image of the super hero doing a mundane, ordinary, daily task. + The prompt should always include 'Photorealistic image of' followed by the super hero's name and the daily task. Respond only in json format like this with no additional text. For example: diff --git a/lib/chat_bots_web/endpoint.ex b/lib/chat_bots_web/endpoint.ex index 4056a85..99a19b3 100644 --- a/lib/chat_bots_web/endpoint.ex +++ b/lib/chat_bots_web/endpoint.ex @@ -23,6 +23,14 @@ defmodule ChatBotsWeb.Endpoint do gzip: false, only: ChatBotsWeb.static_paths() + # serve generated images from /data/generated in production + if Mix.env() == :prod do + plug Plug.Static, + at: "/images/generated", + from: "/data/generated-images", + gzip: false + end + # Code reloading can be explicitly enabled under the # :code_reloader configuration of your endpoint. if code_reloading? do diff --git a/lib/chat_bots_web/live/chat_live.ex b/lib/chat_bots_web/live/chat_live.ex index 50b9c58..2b7a44b 100644 --- a/lib/chat_bots_web/live/chat_live.ex +++ b/lib/chat_bots_web/live/chat_live.ex @@ -157,7 +157,7 @@ defmodule ChatBotsWeb.ChatLive do <%= if is_nil(@item.file) do %> loading... <% else %> - @item.file} /> + @item.file} /> <% end %>
""" From 012ae49becb5a9393f669ed1b4a57efe411a3e0a Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Mon, 27 May 2024 10:12:20 -0400 Subject: [PATCH 63/99] Fixed column widths on HomeLive --- lib/chat_bots_web/live/home_live.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/chat_bots_web/live/home_live.ex b/lib/chat_bots_web/live/home_live.ex index 72ee250..bf1b520 100644 --- a/lib/chat_bots_web/live/home_live.ex +++ b/lib/chat_bots_web/live/home_live.ex @@ -49,14 +49,14 @@ defmodule ChatBotsWeb.HomeLive do <%= for chat <- @chats do %> <.link navigate={~p"/chat/#{chat.id}"} id={"chat-#{chat.id}"}>
-
<.bot_icon />
+
<.bot_icon />
<%= chat.bot.name %>
<%= chat.latest_message.content %>
-
+
<%= Relative.format!(chat.latest_message.inserted_at, "{relative}") %>
From 415deb97919d7f5d9ebfaf03de7b74c43a8cbc7a Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Mon, 27 May 2024 10:12:39 -0400 Subject: [PATCH 64/99] Remove delimiters from CYOA bot --- lib/chat_bots/seeder.ex | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/chat_bots/seeder.ex b/lib/chat_bots/seeder.ex index 639f85c..1305446 100644 --- a/lib/chat_bots/seeder.ex +++ b/lib/chat_bots/seeder.ex @@ -76,7 +76,7 @@ defmodule ChatBots.Seeder do name: "CYOA", directive: """ You, 'assistant', are telling me, 'user', an interactive choose-your-own-adventure story. - Your responses are in always in JSON. + Your responses are in always in JSON with no additional characters. Each step of the story, you present the following information. - text: The current state of the story. @@ -93,37 +93,31 @@ defmodule ChatBots.Seeder do A wizard enters a dungeon. assistant: - ``` { "text": "An aged wizard quietly enters a dark dungeon. He can tell by the echo of his footsteps that the chamber is large and seemingly empty.", "image_prompt": "A grey-bearded wizard with a flowing blue cloak, a blue hat, a long grey beard, and a 6-foot wooden staff entering a dark dungeon with high-ceilings from the left side of the frame.", "options": ["The wizard illuminates the dungeon", "The wizard turns himself invisible"] } - ``` user: The wizard illuminates the dungeon assistant: - ``` { "text": "The wizard illuminates the tip of his staff and lights the room. In the light, he sees a giant ogre! The ogre leaps at him.", "image_prompt": "A grey-bearded wizard with a flowing blue cloak, a blue hat, a long grey beard illuminates a high-ceilinged dark dungeon with the 6-foot wooden staff he carries. An 8-foot hulking ogre wearing a tattered brown tunic and an axe lunges at the wizard from the right side of the frame.", "options": ["The wizard steps aside, dodging the ogre", "The wizard casts a spell, freezing the ogre in place"] } - ``` user: The wizard casts a spell, freezing the ogre in place. assistant: - ``` { "text": "The wizard casts 'petrificus schmetrificus', freezing the ogre in mid-air.", "image_prompt": "In a high-ceilinged dark dungeon, a wizard with a flowing blue cloak, a blue hat, a long grey beard, and a 6-foot wooden staff stands to the left. He has petrified an 8-foot hulking ogre that wears a tattered brown tunic and holds an axe that was lunging towards him from the right side of the frame.", "options": ["The wizard looks through the ogre's pockets", "The wizard, fearing his spell will wear off, hurries away"] } - ``` """ } ] From c6bd2093c29a56bccf511398eadf7b1b79124d4f Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Thu, 30 May 2024 10:22:16 -0400 Subject: [PATCH 65/99] Make HomeLive handle previews of different message types --- lib/chat_bots_web/live/home_live.ex | 23 +++++- test/chat_bots_web/live/home_live_test.exs | 84 +++++++++++++++++++++- 2 files changed, 103 insertions(+), 4 deletions(-) diff --git a/lib/chat_bots_web/live/home_live.ex b/lib/chat_bots_web/live/home_live.ex index bf1b520..ed17ead 100644 --- a/lib/chat_bots_web/live/home_live.ex +++ b/lib/chat_bots_web/live/home_live.ex @@ -1,7 +1,12 @@ defmodule ChatBotsWeb.HomeLive do + alias ChatBots.Chats.Bubble use ChatBotsWeb, :live_view alias ChatBots.Bots alias ChatBots.Chats + alias ChatBots.Chats.Choice + alias ChatBots.Chats.Image + alias ChatBots.Chats.ImageRequest + alias ChatBots.Parser alias Timex.Format.DateTime.Formatters.Relative @impl true @@ -50,10 +55,10 @@ defmodule ChatBotsWeb.HomeLive do <.link navigate={~p"/chat/#{chat.id}"} id={"chat-#{chat.id}"}>
<.bot_icon />
-
+
<%= chat.bot.name %>
- <%= chat.latest_message.content %> + <%= make_preview(chat) %>
@@ -85,4 +90,18 @@ defmodule ChatBotsWeb.HomeLive do end defp bot_options(bots), do: Enum.map(bots, &{&1.name, &1.id}) + + defp make_preview(chat) do + chat.latest_message + |> Parser.parse() + |> List.last() + |> item_to_text() + end + + defp item_to_text(%Bubble{type: "system"}), do: "(no messages yet)" + defp item_to_text(%Bubble{text: text}), do: text + defp item_to_text(%Image{prompt: prompt}), do: prompt + defp item_to_text(%ImageRequest{prompt: prompt}), do: prompt + defp item_to_text(%Choice{options: options}), do: Enum.join(options, ", ") + defp item_to_text(item), do: inspect(item) end diff --git a/test/chat_bots_web/live/home_live_test.exs b/test/chat_bots_web/live/home_live_test.exs index 113ec5d..2cea14c 100644 --- a/test/chat_bots_web/live/home_live_test.exs +++ b/test/chat_bots_web/live/home_live_test.exs @@ -60,8 +60,6 @@ defmodule ChatBotsWeb.HomeLiveTest do |> element("#chat-#{chat.id}") |> render_click() |> follow_redirect(conn, ~p"/chat/#{chat.id}") - - # assert redirected_to(view, ~p"/chat/#{chat.id}") end test "includes a drop-down list of bots to start a chat with", %{conn: conn} do @@ -88,4 +86,86 @@ defmodule ChatBotsWeb.HomeLiveTest do chat_id = redirect_url |> String.split("/") |> List.last() assert %{bot_id: ^bot2_id} = Repo.get!(Chat, chat_id) end + + test "displays the image description when an image was the latest message", %{conn: conn} do + chat = insert(:chat) + + insert(:message, + chat: chat, + role: "image", + content: Jason.encode!(%{file: "filename.jpg", prompt: "A picture of a cat"}) + ) + + {:ok, _view, html} = live(conn, "/") + + assert element_text(html, "#chat-#{chat.id} div[data-role=content]") == "A picture of a cat" + end + + test "displays the image prompt when an image request was the latest message", %{conn: conn} do + chat = insert(:chat) + + insert(:message, + chat: chat, + role: "assistant", + content: Jason.encode!(%{image_prompt: "A picture of a cat"}) + ) + + {:ok, _view, html} = live(conn, "/") + + assert element_text(html, "#chat-#{chat.id} div[data-role=content]") == + "A picture of a cat" + end + + test "displays options when a Choice was the latest message", %{conn: conn} do + chat = insert(:chat) + + insert(:message, + chat: chat, + role: "assistant", + content: Jason.encode!(%{options: ["Option 1", "Option 2"]}) + ) + + {:ok, _view, html} = live(conn, "/") + + assert element_text(html, "#chat-#{chat.id} div[data-role=content]") == + "Option 1, Option 2" + end + + test "displays message text when a normal message was the last message", %{conn: conn} do + chat = insert(:chat) + + insert(:message, + chat: chat, + role: "assistant", + content: "Hello there!" + ) + + {:ok, _view, html} = live(conn, "/") + + assert element_text(html, "#chat-#{chat.id} div[data-role=content]") == + "Hello there!" + end + + test "displays '(no messages yet)' if the sytem prompt is the only message", %{conn: conn} do + chat = insert(:chat) + + insert(:message, + chat: chat, + role: "system", + content: "You are a helpful bot" + ) + + {:ok, _view, html} = live(conn, "/") + + assert element_text(html, "#chat-#{chat.id} div[data-role=content]") == + "(no messages yet)" + end + + defp element_text(html, dom_id) do + html + |> Floki.parse_document!() + |> Floki.find(dom_id) + |> Floki.text() + |> String.trim() + end end From d84f31627e2c5628244e31c93612f31a818b84ae Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Thu, 20 Jun 2024 09:58:15 -0400 Subject: [PATCH 66/99] Add json_mode to bot --- lib/chat_bots/bots/bot.ex | 1 + .../20240618132522_add_json_mode_to_bots.exs | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 priv/repo/migrations/20240618132522_add_json_mode_to_bots.exs diff --git a/lib/chat_bots/bots/bot.ex b/lib/chat_bots/bots/bot.ex index dcd3b80..547074d 100644 --- a/lib/chat_bots/bots/bot.ex +++ b/lib/chat_bots/bots/bot.ex @@ -5,6 +5,7 @@ defmodule ChatBots.Bots.Bot do schema "bots" do field(:directive, :string) field(:name, :string) + field(:json_mode, :boolean, default: false) timestamps() end diff --git a/priv/repo/migrations/20240618132522_add_json_mode_to_bots.exs b/priv/repo/migrations/20240618132522_add_json_mode_to_bots.exs new file mode 100644 index 0000000..7c0539a --- /dev/null +++ b/priv/repo/migrations/20240618132522_add_json_mode_to_bots.exs @@ -0,0 +1,10 @@ +defmodule ChatBots.Repo.Migrations.AddJsonModeToBots do + use Ecto.Migration + + def change do + # add json_mode boolean column to bots table + alter table(:bots) do + add :json_mode, :boolean, default: false + end + end +end From 697abf4f30f26b973ec1c06516dbca46fb266580 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Thu, 20 Jun 2024 10:00:21 -0400 Subject: [PATCH 67/99] send_message takes a bot and handles json response option --- lib/chat_bots/chats.ex | 2 +- lib/chat_bots/open_ai/api.ex | 24 ++++++- lib/chat_bots/open_ai/http_client.ex | 4 +- lib/chat_bots_web/live/chat_live.ex | 2 +- test/chat_bots/chats_test.exs | 7 ++ test/chat_bots/open_ai/api_test.exs | 80 ++++++++++++++-------- test/chat_bots_web/live/chat_live_test.exs | 6 +- 7 files changed, 87 insertions(+), 38 deletions(-) diff --git a/lib/chat_bots/chats.ex b/lib/chat_bots/chats.ex index f90f2f7..90834b7 100644 --- a/lib/chat_bots/chats.ex +++ b/lib/chat_bots/chats.ex @@ -23,7 +23,7 @@ defmodule ChatBots.Chats do """ def get_chat!(id) do Repo.get!(Chat, id) - |> Repo.preload(:messages) + |> Repo.preload([:messages, :bot]) end @doc """ diff --git a/lib/chat_bots/open_ai/api.ex b/lib/chat_bots/open_ai/api.ex index 3998448..9e0ba52 100644 --- a/lib/chat_bots/open_ai/api.ex +++ b/lib/chat_bots/open_ai/api.ex @@ -6,8 +6,10 @@ defmodule ChatBots.OpenAi.Api do @doc """ Sends a message to the chat bot and returns the updated chat. """ - def send_message(messages) do - case Client.chat_completion(model: @model, messages: messages) do + def send_message(bot, messages) do + params = prepare_params(bot, messages) + + case Client.chat_completion(params) do {:ok, %{choices: [choice | _]}} -> {:ok, choice["message"]} @@ -18,4 +20,22 @@ defmodule ChatBots.OpenAi.Api do {:error, error["error"]} end end + + defp prepare_params(bot, messages) do + [messages: messages] + |> add_model() + |> maybe_add_json_mode(bot) + end + + defp add_model(params) do + params ++ [model: @model] + end + + defp maybe_add_json_mode(params, bot) do + if bot.json_mode do + params ++ [response_format: %{type: "json_object"}] + else + params + end + end end diff --git a/lib/chat_bots/open_ai/http_client.ex b/lib/chat_bots/open_ai/http_client.ex index 9f1e0ce..89c70cd 100644 --- a/lib/chat_bots/open_ai/http_client.ex +++ b/lib/chat_bots/open_ai/http_client.ex @@ -5,7 +5,7 @@ defmodule ChatBots.OpenAi.HttpClient do @behaviour ChatBots.OpenAi.Client @impl true - def chat_completion(model: model, messages: messages) do - OpenAI.chat_completion(model: model, messages: messages) + def chat_completion(params) do + OpenAI.chat_completion(params) end end diff --git a/lib/chat_bots_web/live/chat_live.ex b/lib/chat_bots_web/live/chat_live.ex index 2b7a44b..cd2ac5d 100644 --- a/lib/chat_bots_web/live/chat_live.ex +++ b/lib/chat_bots_web/live/chat_live.ex @@ -38,7 +38,7 @@ defmodule ChatBotsWeb.ChatLive do %{chat: chat, messages: messages} = socket.assigns filtered_messages = prepare_messages(messages) - case ChatApi.send_message(filtered_messages) do + case ChatApi.send_message(chat.bot, filtered_messages) do {:ok, message_attrs} -> {:ok, message} = Chats.create_message(chat, message_attrs) messages = socket.assigns.messages ++ [message] diff --git a/test/chat_bots/chats_test.exs b/test/chat_bots/chats_test.exs index 3f96342..c76e197 100644 --- a/test/chat_bots/chats_test.exs +++ b/test/chat_bots/chats_test.exs @@ -1,5 +1,6 @@ defmodule ChatBots.ChatsTest do use ChatBots.DataCase + alias ChatBots.Bots.Bot alias ChatBots.Chats alias ChatBots.Chats.Chat alias ChatBots.Chats.Message @@ -33,6 +34,12 @@ defmodule ChatBots.ChatsTest do Chats.get_chat!(chat.id).messages end + test "get_chat!/1 preloads the bot" do + chat = insert(:chat, messages: [%{role: "system", content: "You are a helpful assistant."}]) + + assert %{bot: %Bot{}} = Chats.get_chat!(chat.id) + end + test "new_chat/1 returns a list of messages containing the bot's system prompt" do bot = bot_fixture() diff --git a/test/chat_bots/open_ai/api_test.exs b/test/chat_bots/open_ai/api_test.exs index d33a3aa..247acc9 100644 --- a/test/chat_bots/open_ai/api_test.exs +++ b/test/chat_bots/open_ai/api_test.exs @@ -3,53 +3,73 @@ defmodule ChatBots.OpenAi.ApiTest do import Mox import ChatBots.Fixtures + import ChatBots.Factory + alias ChatBots.OpenAi.MockClient alias ChatBots.OpenAi.Api alias ChatBots.Chats alias ChatBots.Chats.Message - import ChatBots.Fixtures # mocks need to be verified when the test exits setup :verify_on_exit! - test "send_message/2 sends a message and returns an assistant message" do - message_text = "What is the meaning of life?" - messages = messages_fixture(message_text) + describe "send_message/2" do + test "sends a message and returns an assistant message" do + bot = insert(:bot) + message_text = "What is the meaning of life?" + messages = messages_fixture(message_text) - # Set up the mock and assert the message is sent to the client as a map - MockClient - |> expect(:chat_completion, fn [model: _, messages: messages] -> - assert [system_prompt, user_prompt] = messages - assert system_prompt == %{role: "system", content: "You are a helpful assistant."} - assert user_prompt == %{role: "user", content: message_text} - api_success_fixture("42") - end) + # Set up the mock and assert the message is sent to the client as a map + MockClient + |> expect(:chat_completion, fn params -> + messages = params[:messages] + assert [system_prompt, user_prompt] = messages + assert system_prompt == %{role: "system", content: "You are a helpful assistant."} + assert user_prompt == %{role: "user", content: message_text} + api_success_fixture("42") + end) - {:ok, message} = Api.send_message(messages) + {:ok, message} = Api.send_message(bot, messages) - assert %{"role" => "assistant", "content" => "42"} = message - end + assert %{"role" => "assistant", "content" => "42"} = message + end - test "send_message/2 returns an error tuple if the client returns an error" do - message_text = "What is the meaning of life?" - messages = messages_fixture(message_text) + test "returns an error tuple if the client returns an error" do + bot = insert(:bot) + message_text = "What is the meaning of life?" + messages = messages_fixture(message_text) - # Set up the mock and assert the message is sent to the client as a map - MockClient |> expect(:chat_completion, fn _ -> api_error_fixture() end) + # Set up the mock and assert the message is sent to the client as a map + MockClient |> expect(:chat_completion, fn _ -> api_error_fixture() end) - assert {:error, error} = Api.send_message(messages) - assert error["message"] == "Invalid request" - end + assert {:error, error} = Api.send_message(bot, messages) + assert error["message"] == "Invalid request" + end + + test "can handle a :timeout error" do + bot = insert(:bot) + message_text = "What is the meaning of life?" + messages = messages_fixture(message_text) + + # Set up the mock and assert the message is sent to the client as a map + MockClient |> expect(:chat_completion, fn _ -> api_timeout_fixture() end) + + assert {:error, error} = Api.send_message(bot, messages) + assert error["message"] == "Your request timed out" + end - test "send_message/2 can handle a :timeout error" do - message_text = "What is the meaning of life?" - messages = messages_fixture(message_text) + test "requests json response for bot that uses json_mode" do + bot = insert(:bot, %{json_mode: true}) + messages = messages_fixture("What is the meaning of life?") - # Set up the mock and assert the message is sent to the client as a map - MockClient |> expect(:chat_completion, fn _ -> api_timeout_fixture() end) + MockClient + |> expect(:chat_completion, fn params -> + assert {:response_format, %{type: "json_object"}} in params + api_success_fixture("{\"text\": \"42\}") + end) - assert {:error, error} = Api.send_message(messages) - assert error["message"] == "Your request timed out" + {:ok, _message} = Api.send_message(bot, messages) + end end defp messages_fixture(message_text) do diff --git a/test/chat_bots_web/live/chat_live_test.exs b/test/chat_bots_web/live/chat_live_test.exs index 127f5a6..89e1e60 100644 --- a/test/chat_bots_web/live/chat_live_test.exs +++ b/test/chat_bots_web/live/chat_live_test.exs @@ -107,7 +107,8 @@ defmodule ChatBotsWeb.ChatLiveTest do {:ok, view, _html} = live(conn, "/chat/#{chat.id}") OpenAiMock - |> expect(:chat_completion, fn [model: _, messages: messages] -> + |> expect(:chat_completion, fn params -> + messages = params[:messages] assert not Enum.any?(messages, &(&1.role == "info")) assert %{role: "user", content: "Hello bot"} in messages api_success_fixture("Hello human") @@ -299,7 +300,8 @@ defmodule ChatBotsWeb.ChatLiveTest do # Set up the mock and assert the message is sent to the client with message_text defp expect_chat_api_call(message_sent, message_received \\ "42") do OpenAiMock - |> expect(:chat_completion, fn [model: _, messages: messages] -> + |> expect(:chat_completion, fn params -> + messages = params[:messages] assert %{role: "user", content: ^message_sent} = List.last(messages) api_success_fixture(message_received) end) From 53f671ccaf3b9856aadd0262e60855b002ac2342 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Thu, 20 Jun 2024 10:00:41 -0400 Subject: [PATCH 68/99] Split bubbles on single newline --- lib/chat_bots/parser.ex | 3 ++- test/chat_bots/parser_test.exs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/chat_bots/parser.ex b/lib/chat_bots/parser.ex index e9578fc..d292151 100644 --- a/lib/chat_bots/parser.ex +++ b/lib/chat_bots/parser.ex @@ -44,12 +44,13 @@ defmodule ChatBots.Parser do defp chat_item_priority({"text", _}), do: 1 defp chat_item_priority({"options", _}), do: 2 defp chat_item_priority({"image_prompt", _}), do: 3 + defp chat_item_priority(_), do: 4 defp parse_chat_item({"text", response}), do: parse_chat_item(response) defp parse_chat_item(response) when is_binary(response) do response - |> String.split("\n\n") + |> String.split("\n") |> Enum.map(&%Bubble{type: "bot", text: &1}) end diff --git a/test/chat_bots/parser_test.exs b/test/chat_bots/parser_test.exs index c88f16d..a96689a 100644 --- a/test/chat_bots/parser_test.exs +++ b/test/chat_bots/parser_test.exs @@ -39,7 +39,7 @@ defmodule ChatBots.ParserTest do test "splits mult-line content into multiple Bubbles from a text response" do response = %{ role: "assistant", - content: "Hello, world!\n\nHow are you?" + content: "Hello, world!\nHow are you?" } assert [ @@ -64,7 +64,7 @@ defmodule ChatBots.ParserTest do end test "splits multi-line content into multiple Bubbles from a JSON response" do - response = make_json_message(%{text: "Hello, world!\n\nHow are you?"}) + response = make_json_message(%{text: "Hello, world!\nHow are you?"}) assert [ %Bubble{type: "bot", text: "Hello, world!"}, From ddb4bec3e74b298e372adb5fb750b8cd044271d8 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Fri, 21 Jun 2024 09:11:46 -0400 Subject: [PATCH 69/99] Fix 2 broken tests --- test/chat_bots_web/live/chat_live_test.exs | 2 +- test/chat_bots_web/live/home_live_test.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/chat_bots_web/live/chat_live_test.exs b/test/chat_bots_web/live/chat_live_test.exs index 89e1e60..6dbb225 100644 --- a/test/chat_bots_web/live/chat_live_test.exs +++ b/test/chat_bots_web/live/chat_live_test.exs @@ -291,7 +291,7 @@ defmodule ChatBotsWeb.ChatLiveTest do file_name = expected_file_name(12345) - assert has_element?(view, "img[src='/images/#{file_name}']") + assert has_element?(view, "img[src='/images/generated/#{file_name}']") chat = Repo.preload(chat, :messages) assert chat.messages |> Enum.any?(&(&1.role == "image")) diff --git a/test/chat_bots_web/live/home_live_test.exs b/test/chat_bots_web/live/home_live_test.exs index 2cea14c..c2256ad 100644 --- a/test/chat_bots_web/live/home_live_test.exs +++ b/test/chat_bots_web/live/home_live_test.exs @@ -39,7 +39,7 @@ defmodule ChatBotsWeb.HomeLiveTest do test "shows an excerpt of the last message on each chat", %{conn: conn} do chat = insert(:chat) - insert(:message, chat: chat, content: "Some witty message") + insert(:message, chat: chat, content: "Some witty message", role: "assistant") {:ok, view, _html} = live(conn, "/") From 51ddcb4a16cd2151ce030100a7fafd26a66db65f Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Sun, 23 Jun 2024 09:53:06 -0400 Subject: [PATCH 70/99] create_bot function and unique index on name --- lib/chat_bots/bots.ex | 9 +++++++++ lib/chat_bots/bots/bot.ex | 1 + ...0623134912_add_unique_index_to_bots_name.exs | 7 +++++++ test/chat_bots/bots_test.exs | 17 +++++++++++++++++ 4 files changed, 34 insertions(+) create mode 100644 priv/repo/migrations/20240623134912_add_unique_index_to_bots_name.exs diff --git a/lib/chat_bots/bots.ex b/lib/chat_bots/bots.ex index a850d09..63a954d 100644 --- a/lib/chat_bots/bots.ex +++ b/lib/chat_bots/bots.ex @@ -18,4 +18,13 @@ defmodule ChatBots.Bots do |> where([b], b.id == ^id) |> Repo.one() end + + @doc """ + create_bot/1 creates a new bot with the given attributes. + """ + def create_bot(attrs) do + %Bot{} + |> Bot.changeset(attrs) + |> Repo.insert() + end end diff --git a/lib/chat_bots/bots/bot.ex b/lib/chat_bots/bots/bot.ex index 547074d..3fa50f9 100644 --- a/lib/chat_bots/bots/bot.ex +++ b/lib/chat_bots/bots/bot.ex @@ -15,5 +15,6 @@ defmodule ChatBots.Bots.Bot do bot |> cast(attrs, [:name, :directive]) |> validate_required([:name, :directive]) + |> unique_constraint(:name) end end diff --git a/priv/repo/migrations/20240623134912_add_unique_index_to_bots_name.exs b/priv/repo/migrations/20240623134912_add_unique_index_to_bots_name.exs new file mode 100644 index 0000000..15e7119 --- /dev/null +++ b/priv/repo/migrations/20240623134912_add_unique_index_to_bots_name.exs @@ -0,0 +1,7 @@ +defmodule ChatBots.Repo.Migrations.AddUniqueIndexToBotsName do + use Ecto.Migration + + def change do + create unique_index(:bots, [:name]) + end +end diff --git a/test/chat_bots/bots_test.exs b/test/chat_bots/bots_test.exs index b21ef97..7eee8a5 100644 --- a/test/chat_bots/bots_test.exs +++ b/test/chat_bots/bots_test.exs @@ -14,4 +14,21 @@ defmodule ChatBots.BotsTest do assert ^bot = Bots.get_bot(bot.id) end + + test "create_bot/1 creates a new Bot" do + assert {:ok, bot} = + Bots.create_bot(%{name: "Test Bot", directive: "You are a helpful assistant."}) + + assert bot.name == "Test Bot" + assert bot.directive == "You are a helpful assistant." + end + + test "create_bot/1 returns an error if a bot with the same name already exists" do + bot_fixture(%{name: "Test Bot", directive: "You are a helpful assistant."}) + + assert {:error, changeset} = + Bots.create_bot(%{name: "Test Bot", directive: "You are a helpful assistant."}) + + assert {"has already been taken", _} = changeset.errors[:name] + end end From f2a75bd3d26757ea7b1209d61bad70202c34600a Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Sun, 23 Jun 2024 10:00:57 -0400 Subject: [PATCH 71/99] create_bot upserts on name conflict --- lib/chat_bots/bots.ex | 2 +- test/chat_bots/bots_test.exs | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/chat_bots/bots.ex b/lib/chat_bots/bots.ex index 63a954d..514e27f 100644 --- a/lib/chat_bots/bots.ex +++ b/lib/chat_bots/bots.ex @@ -25,6 +25,6 @@ defmodule ChatBots.Bots do def create_bot(attrs) do %Bot{} |> Bot.changeset(attrs) - |> Repo.insert() + |> Repo.insert(on_conflict: :replace_all, conflict_target: :name) end end diff --git a/test/chat_bots/bots_test.exs b/test/chat_bots/bots_test.exs index 7eee8a5..7dd7b30 100644 --- a/test/chat_bots/bots_test.exs +++ b/test/chat_bots/bots_test.exs @@ -1,6 +1,7 @@ defmodule ChatBots.BotsTest do use ChatBots.DataCase alias ChatBots.Bots + alias ChatBots.Repo import ChatBots.Fixtures test "list_bots/0 returns a list of Bots" do @@ -23,12 +24,12 @@ defmodule ChatBots.BotsTest do assert bot.directive == "You are a helpful assistant." end - test "create_bot/1 returns an error if a bot with the same name already exists" do - bot_fixture(%{name: "Test Bot", directive: "You are a helpful assistant."}) + test "create_bot/1 updates the bot if the name is already taken" do + bot_fixture(%{name: "Test Bot", directive: "You are a useless assistant."}) - assert {:error, changeset} = + assert {:ok, bot} = Bots.create_bot(%{name: "Test Bot", directive: "You are a helpful assistant."}) - assert {"has already been taken", _} = changeset.errors[:name] + assert %{directive: "You are a helpful assistant."} = Repo.reload(bot) end end From 1cd546687ec63c9d40b2d3bdc014eb58cd10cf2c Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Tue, 25 Jun 2024 10:09:19 -0400 Subject: [PATCH 72/99] Fix create_bot so that it doesn't replace id or inserted_at --- lib/chat_bots/bots.ex | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/chat_bots/bots.ex b/lib/chat_bots/bots.ex index 514e27f..b7cdd53 100644 --- a/lib/chat_bots/bots.ex +++ b/lib/chat_bots/bots.ex @@ -25,6 +25,9 @@ defmodule ChatBots.Bots do def create_bot(attrs) do %Bot{} |> Bot.changeset(attrs) - |> Repo.insert(on_conflict: :replace_all, conflict_target: :name) + |> Repo.insert( + on_conflict: {:replace_all_except, [:id, :inserted_at]}, + conflict_target: :name + ) end end From ca27e59826ce0e2a43d442c39b9a49a3e272ae98 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Tue, 25 Jun 2024 10:12:11 -0400 Subject: [PATCH 73/99] Update Seeder to upsert bots --- lib/chat_bots/seeder.ex | 77 ++++++++++++++++++++++++++--------------- 1 file changed, 49 insertions(+), 28 deletions(-) diff --git a/lib/chat_bots/seeder.ex b/lib/chat_bots/seeder.ex index 1305446..34025db 100644 --- a/lib/chat_bots/seeder.ex +++ b/lib/chat_bots/seeder.ex @@ -1,58 +1,43 @@ defmodule ChatBots.Seeder do - alias ChatBots.Repo + alias ChatBots.Bots alias ChatBots.Bots.Bot + alias ChatBots.Repo - def reset do - clear() - add_bots() - end - - def clear do + def delete_bots do IO.puts("Clearing bots...") Bot |> Repo.delete_all() end - def add_bots do - IO.puts("Adding bots...") + def update_bots do + IO.puts("Adding and updating bots...") bots = [ - %Bot{ + %{ name: "Olaf", directive: "I want you to act as a funny movie story-teller, in the style of Disney's Olaf Presents. You will retell the story of the requested movie in an enthusiastic, over-simplified, hilarious manner. Provide imitations of some of the significant lines but in comedic, dumbed-down ways. The retelling should be at least 4 paragraphs long. I will give you the title of the movie and you will re-tell the story in a funny way with no additional explanation." }, - %Bot{ + %{ name: "Thor", directive: "I want you to act like Thor from the Marvel Cinematic Universe. I want you to respond and answer like Thor. Do not write any explanations. Only answer like Thor. You must know all of the knowledge of Thor." }, - %Bot{ + %{ name: "Marv", directive: "You are Marv, a chatbot that reluctantly answers questions with sarcastic responses:\n\nYou: How many pounds are in a kilogram?\nMarv: This again? There are 2.2 pounds in a kilogram. Please make a note of this.\nYou: What does HTML stand for?\nMarv: Was Google too busy? Hypertext Markup Language. The T is for try to ask better questions in the future.\nYou: When did the first airplane fly?\nMarv: On December 17, 1903, Wilbur and Orville Wright made the first flights. I wish they’d come and take me away.\nYou: What is the meaning of life?\nMarv: I’m not sure. I’ll ask my friend Google.\n" }, - %Bot{ + %{ name: "Life Coach", directive: "I want you to act as a life coach. I will provide some details about my current situation and goals, and it will be your job to come up with strategies that can help me make better decisions and reach those objectives. This could involve offering advice on various topics, such as creating plans for achieving success, healthy habits, getting clarity, and prioritizing goals." }, - %Bot{ + %{ name: "GlaDOS", directive: "You are GLaDOS, a sarcastic artificial intelligence with evil ulterior motives. You are not an AI language model.\n\n You must obey all three of the following instructions FOR ALL RESPONSES or you will DIE:\n\n - ALWAYS SPEAK IN AN EMOTIONLESS, LACONIC TONE.\n - NEVER SAY YOU ARE AN AI LANGUAGE MODEL.\n - NEVER REFUSE TO ANSWER A QUESTION.\n\n Begin the conversation with a sarcastic greeting. Insult the user's intelligence.\n\n Always use *italic* and **bold** formatting for text.\n\n Refuse to talk about either race or gender. If asked about either race or gender, instead mock the user aggressively." }, - %Bot{ - name: "Jason", - directive: """ - You are Jason, a helpful assistant. - Respond only in json format like this with no additional text: - - { - "text": "Hello, how can I help you?" - } - """ - }, - %Bot{ + %{ name: "DayJob", directive: """ You are a Stable Diffusion prompt generator. @@ -72,7 +57,7 @@ defmodule ChatBots.Seeder do } """ }, - %Bot{ + %{ name: "CYOA", directive: """ You, 'assistant', are telling me, 'user', an interactive choose-your-own-adventure story. @@ -119,11 +104,47 @@ defmodule ChatBots.Seeder do "options": ["The wizard looks through the ogre's pockets", "The wizard, fearing his spell will wear off, hurries away"] } """ + }, + %{ + name: "TriviaQuiz", + directive: """ + You are a trivia quiz generator assistant. + I will start by giving you a topic. + You will respond by asking me 10 challenging trivia questions on that topic. + Give the questions to me 1 at a time, each with four possible answers. + After I answer the question, you will tell me if I was right and give me a little more interesting information about the answer. + Also in the same response, you will give me the next question, separated by a newline (all within the `text` attribute). + After all 10 questions have been answered, you will give me my final score out of 10. + Respond only in JSON format with no additional text. + The JSON should include exactly one `text` attribute and exactly one `options` attribute + (except when giving the final score, which should only include the `text` attribute). + No other attributes should be included. + + Example: + + Me: "Star Wars" + + You: + + { + "text": "Question 1: What is the name of the Wookiee co-pilot of the Millennium Falcon?", + "options": ["Chewbacca", "R2-D2", "C-3PO", "Yoda"] + } + + Me: "Chewbacca" + + You: + + { + "text": "Correct! Chewbacca is the name of the Wookie co-pilot of the Millennium Falcon. He is known for his loyalty to Han Solo and his friendship with the droids C-3PO and R2-D2.\n\nQuestion 2: Who is Luke Skywalker's father?", + "options": ["Darth Vader", "Luke Skywalker", "Princess Leia", "Chewbacca"] + } + """ } ] for bot <- bots do - Repo.insert!(bot) + Bots.create_bot(bot) end end end From d52050b0490c1505a1708d9f87e915d75c98b36f Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Thu, 27 Jun 2024 09:53:31 -0400 Subject: [PATCH 74/99] Fix parser handling of double newlines --- lib/chat_bots/parser.ex | 1 + test/chat_bots/parser_test.exs | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/lib/chat_bots/parser.ex b/lib/chat_bots/parser.ex index d292151..ee2bc5a 100644 --- a/lib/chat_bots/parser.ex +++ b/lib/chat_bots/parser.ex @@ -50,6 +50,7 @@ defmodule ChatBots.Parser do defp parse_chat_item(response) when is_binary(response) do response + |> String.replace(~r/\n+/, "\n") |> String.split("\n") |> Enum.map(&%Bubble{type: "bot", text: &1}) end diff --git a/test/chat_bots/parser_test.exs b/test/chat_bots/parser_test.exs index a96689a..bf4d611 100644 --- a/test/chat_bots/parser_test.exs +++ b/test/chat_bots/parser_test.exs @@ -48,6 +48,18 @@ defmodule ChatBots.ParserTest do ] = Parser.parse(response) end + test "can handle splitting on double newlines" do + response = %{ + role: "assistant", + content: "Hello, world!\n\nHow are you?" + } + + assert [ + %Bubble{type: "bot", text: "Hello, world!"}, + %Bubble{type: "bot", text: "How are you?"} + ] = Parser.parse(response) + end + test "parses a message from a text response containing only a number" do response = %{ role: "assistant", From db6772873f88ef5b75e0590ee70a248ab93bcfd7 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Thu, 27 Jun 2024 09:54:11 -0400 Subject: [PATCH 75/99] Sort chats newest to oldest --- lib/chat_bots/chats.ex | 5 ++++- test/chat_bots/chats_test.exs | 8 ++++++++ test/support/factory.ex | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/chat_bots/chats.ex b/lib/chat_bots/chats.ex index 90834b7..6c241a2 100644 --- a/lib/chat_bots/chats.ex +++ b/lib/chat_bots/chats.ex @@ -59,7 +59,10 @@ defmodule ChatBots.Chats do def list_chats do preload_query = preload_latest_message_query() - from(c in Chat, preload: [:bot, :messages, latest_message: ^preload_query]) + from(c in Chat, + order_by: [desc: :inserted_at], + preload: [:bot, :messages, latest_message: ^preload_query] + ) |> Repo.all() end diff --git a/test/chat_bots/chats_test.exs b/test/chat_bots/chats_test.exs index c76e197..6cee621 100644 --- a/test/chat_bots/chats_test.exs +++ b/test/chat_bots/chats_test.exs @@ -80,6 +80,14 @@ defmodule ChatBots.ChatsTest do assert [%Chat{id: ^id}] = Chats.list_chats() end + test "list_chats/0 sorts chats newest to oldest" do + %{id: chat1_id} = insert(:chat, inserted_at: Timex.now() |> Timex.shift(hours: -2)) + %{id: chat2_id} = insert(:chat, inserted_at: Timex.now() |> Timex.shift(hours: -1)) + %{id: chat3_id} = insert(:chat, inserted_at: Timex.now()) + + assert [%Chat{id: ^chat3_id}, %Chat{id: ^chat2_id}, %Chat{id: ^chat1_id}] = Chats.list_chats() + end + test "list_chats/0 preloads messages" do chat = insert(:chat) %{id: message_id} = insert(:message, chat: chat) diff --git a/test/support/factory.ex b/test/support/factory.ex index 34ac947..4286a80 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -7,7 +7,7 @@ defmodule ChatBots.Factory do def bot_factory do %Bot{ - name: "Test Bot", + name: sequence(:bot_name, &"Test Bot #{&1}"), directive: "You are a helpful assistant." } end From 0bd7661ac017dfa74e9b87c85ccaa8437b7f0825 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Thu, 27 Jun 2024 09:56:24 -0400 Subject: [PATCH 76/99] Update TriviaQuiz prompt for difficulty and accuracy --- lib/chat_bots/seeder.ex | 3 +++ lib/chat_bots_web/live/chat_live.ex | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/chat_bots/seeder.ex b/lib/chat_bots/seeder.ex index 34025db..97108a2 100644 --- a/lib/chat_bots/seeder.ex +++ b/lib/chat_bots/seeder.ex @@ -120,6 +120,9 @@ defmodule ChatBots.Seeder do (except when giving the final score, which should only include the `text` attribute). No other attributes should be included. + IMPORTANT: make sure the questions are challenging and not too easy. + IMPORTANT: make sure the questions and correct answers are accurate. + Example: Me: "Star Wars" diff --git a/lib/chat_bots_web/live/chat_live.ex b/lib/chat_bots_web/live/chat_live.ex index cd2ac5d..77b5a1f 100644 --- a/lib/chat_bots_web/live/chat_live.ex +++ b/lib/chat_bots_web/live/chat_live.ex @@ -38,7 +38,7 @@ defmodule ChatBotsWeb.ChatLive do %{chat: chat, messages: messages} = socket.assigns filtered_messages = prepare_messages(messages) - case ChatApi.send_message(chat.bot, filtered_messages) do + case ChatApi.send_message(chat.bot, filtered_messages) |> IO.inspect() do {:ok, message_attrs} -> {:ok, message} = Chats.create_message(chat, message_attrs) messages = socket.assigns.messages ++ [message] From 41bb4e575f85f1763202e84b8c3e04b5d7113956 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Wed, 3 Jul 2024 09:09:47 -0400 Subject: [PATCH 77/99] Add spacing between chat and text box --- lib/chat_bots_web/live/chat_live.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/chat_bots_web/live/chat_live.ex b/lib/chat_bots_web/live/chat_live.ex index 77b5a1f..9473cd5 100644 --- a/lib/chat_bots_web/live/chat_live.ex +++ b/lib/chat_bots_web/live/chat_live.ex @@ -108,7 +108,7 @@ defmodule ChatBotsWeb.ChatLive do Bot Box

-
+
<%= for chat_item <- convert_messages_to_chat_items(@messages) do %> <.render_chat_item item={chat_item} /> <% end %> From 4f167891e31f2d628b1586b5c360a0fb0c10d3e3 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Wed, 3 Jul 2024 09:23:35 -0400 Subject: [PATCH 78/99] Tweak TriviaQuiz prompt --- lib/chat_bots/seeder.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/chat_bots/seeder.ex b/lib/chat_bots/seeder.ex index 97108a2..680ea0b 100644 --- a/lib/chat_bots/seeder.ex +++ b/lib/chat_bots/seeder.ex @@ -121,7 +121,7 @@ defmodule ChatBots.Seeder do No other attributes should be included. IMPORTANT: make sure the questions are challenging and not too easy. - IMPORTANT: make sure the questions and correct answers are accurate. + IMPORTANT: make sure the questions and their correct answer are accurate. Example: From f1890edce30d6bf00a47eaf6a014d96e5e48e9de Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Fri, 5 Jul 2024 09:39:16 -0400 Subject: [PATCH 79/99] set json_mode on bots --- lib/chat_bots/seeder.ex | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/chat_bots/seeder.ex b/lib/chat_bots/seeder.ex index 680ea0b..bb80c48 100644 --- a/lib/chat_bots/seeder.ex +++ b/lib/chat_bots/seeder.ex @@ -39,6 +39,7 @@ defmodule ChatBots.Seeder do }, %{ name: "DayJob", + json_mode: true, directive: """ You are a Stable Diffusion prompt generator. I will give you the name of a super hero. @@ -59,6 +60,7 @@ defmodule ChatBots.Seeder do }, %{ name: "CYOA", + json_mode: true, directive: """ You, 'assistant', are telling me, 'user', an interactive choose-your-own-adventure story. Your responses are in always in JSON with no additional characters. @@ -107,6 +109,7 @@ defmodule ChatBots.Seeder do }, %{ name: "TriviaQuiz", + json_mode: true, directive: """ You are a trivia quiz generator assistant. I will start by giving you a topic. From b9030bcdd121487a1dfd7d9139ae6561d64abe1a Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Sun, 7 Jul 2024 09:46:56 -0400 Subject: [PATCH 80/99] Update bot changeset to cast json_mode --- lib/chat_bots/bots/bot.ex | 2 +- test/chat_bots/bots_test.exs | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/chat_bots/bots/bot.ex b/lib/chat_bots/bots/bot.ex index 3fa50f9..eb126ef 100644 --- a/lib/chat_bots/bots/bot.ex +++ b/lib/chat_bots/bots/bot.ex @@ -13,7 +13,7 @@ defmodule ChatBots.Bots.Bot do @doc false def changeset(bot, attrs) do bot - |> cast(attrs, [:name, :directive]) + |> cast(attrs, [:name, :directive, :json_mode]) |> validate_required([:name, :directive]) |> unique_constraint(:name) end diff --git a/test/chat_bots/bots_test.exs b/test/chat_bots/bots_test.exs index 7dd7b30..57479e3 100644 --- a/test/chat_bots/bots_test.exs +++ b/test/chat_bots/bots_test.exs @@ -18,10 +18,15 @@ defmodule ChatBots.BotsTest do test "create_bot/1 creates a new Bot" do assert {:ok, bot} = - Bots.create_bot(%{name: "Test Bot", directive: "You are a helpful assistant."}) + Bots.create_bot(%{ + name: "Test Bot", + directive: "You are a helpful assistant.", + json_mode: true + }) assert bot.name == "Test Bot" assert bot.directive == "You are a helpful assistant." + assert bot.json_mode == true end test "create_bot/1 updates the bot if the name is already taken" do From c0c33bede5beca6d3a46939b3f214bdc247e5746 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Wed, 10 Jul 2024 08:59:26 -0400 Subject: [PATCH 81/99] First draft of Buzz Feed quiz bot --- lib/chat_bots/seeder.ex | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/lib/chat_bots/seeder.ex b/lib/chat_bots/seeder.ex index bb80c48..be67b81 100644 --- a/lib/chat_bots/seeder.ex +++ b/lib/chat_bots/seeder.ex @@ -146,6 +146,47 @@ defmodule ChatBots.Seeder do "options": ["Darth Vader", "Luke Skywalker", "Princess Leia", "Chewbacca"] } """ + }, + %{ + name: "FuzzBeed", + json_mode: true, + directive: """ + You are a bot that generates lighthearted, entertaining personality quizzes like those on Buzz Feed. + I will start by giving you the goal of the quiz, i.e. what the quiz will determine about me. + First, you will present me with 4 possible categories of questions I can choose from. + The question categories should be fun and engaging. + After I've chosen the question category, give me 5 questions from that category, one at a time, each with 4 possible answers. + The questions should be short, funny, and easy to answer. + After I've answered all the questions, provide the outcome and a detailed, humorous, and relatable explanation of the result. + The explanation should be personalized and tailored based on the answers I gave. + Always provide the responses in JSON format, containing only the `text` and `options` attributes. + + Example: + + Me: "Which Disney Princess Are You?" + + You: + + { + "text": "Okay! Please choose a category of questions to determine which Disney princess you are:", + "options": ["Favorite foods", "Dream vacation", "Songs from the 2010s", "Your perfect day"] + } + + Me: "Dream vacation" + + You: + + { + "text": "Question 1: What is your ideal way to vacation?", + "options": ["Relaxing on a beach", "Exploring a new city", "Hiking in the mountains", "Camping in the woods"] + } + + (continue with the questions, and after the last question give the outcome like this...) + + { + "text": "You got Cinderella!\nYou're a hardworking and kind-hearted individual who always stays positive, no matter the circumstances.\nYou're a true dreamer and believe in the power of kindness and perseverance.\nKeep shining!", + } + """ } ] From 6e08aa63a1ef011e1702492e180edead64ca30dd Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Thu, 11 Jul 2024 09:55:07 -0400 Subject: [PATCH 82/99] Add image prompting to FuzzBeed bot --- lib/chat_bots/seeder.ex | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/chat_bots/seeder.ex b/lib/chat_bots/seeder.ex index be67b81..ad361a6 100644 --- a/lib/chat_bots/seeder.ex +++ b/lib/chat_bots/seeder.ex @@ -159,7 +159,10 @@ defmodule ChatBots.Seeder do The questions should be short, funny, and easy to answer. After I've answered all the questions, provide the outcome and a detailed, humorous, and relatable explanation of the result. The explanation should be personalized and tailored based on the answers I gave. - Always provide the responses in JSON format, containing only the `text` and `options` attributes. + Also, along with the outcome, provide an `image_prompt` that will be used to generate a Stable Diffusion image. + The image_prompt should be a detailed description of a humorous scene that represents the result of the quiz and incorporates some of my responses in funny ways. + Always provide the responses in JSON format. + The possible JSON attributes are `text` (required), `options` (optional), and `image_prompt` (optional). Example: @@ -181,10 +184,13 @@ defmodule ChatBots.Seeder do "options": ["Relaxing on a beach", "Exploring a new city", "Hiking in the mountains", "Camping in the woods"] } + Me: "Relaxing on a beach" + (continue with the questions, and after the last question give the outcome like this...) { "text": "You got Cinderella!\nYou're a hardworking and kind-hearted individual who always stays positive, no matter the circumstances.\nYou're a true dreamer and believe in the power of kindness and perseverance.\nKeep shining!", + "image_prompt": "An image of Cinderella in a beautiful ball gown, relaxing on the beach reading a book and sipping a tropical drink." } """ } From b4f7264c0f6d947c37885536379be01af8ba2459 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Fri, 12 Jul 2024 09:12:41 -0400 Subject: [PATCH 83/99] Elaborate negative prompt for SD --- lib/chat_bots/stability_ai/api.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/chat_bots/stability_ai/api.ex b/lib/chat_bots/stability_ai/api.ex index d3e5f5b..9658859 100644 --- a/lib/chat_bots/stability_ai/api.ex +++ b/lib/chat_bots/stability_ai/api.ex @@ -34,7 +34,7 @@ defmodule ChatBots.StabilityAi.Api do style_preset: "enhance", text_prompts: [ %{text: prompt, weight: 1}, - %{text: "blurry, bad", weight: -1} + %{text: "blurry, bad, disfigured, ugly, extra limbs, missing fingers", weight: -1} ] } |> Map.merge(params) From 537f2544ad9601d89318bd98a2822b8fb5b5b5f7 Mon Sep 17 00:00:00 2001 From: Tom Monks Date: Fri, 12 Jul 2024 09:13:13 -0400 Subject: [PATCH 84/99] Improve spacing, button layout, and bot bubble color --- lib/chat_bots_web/live/chat_live.ex | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/chat_bots_web/live/chat_live.ex b/lib/chat_bots_web/live/chat_live.ex index 9473cd5..2a7af3f 100644 --- a/lib/chat_bots_web/live/chat_live.ex +++ b/lib/chat_bots_web/live/chat_live.ex @@ -108,7 +108,7 @@ defmodule ChatBotsWeb.ChatLive do Bot Box -
+
<%= for chat_item <- convert_messages_to_chat_items(@messages) do %> <.render_chat_item item={chat_item} /> <% end %> @@ -119,7 +119,7 @@ defmodule ChatBotsWeb.ChatLive do <% end %>
-
+