diff --git a/assets/js/app.js b/assets/js/app.js index 6ecae74..7b114fd 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -22,9 +22,35 @@ import {Socket} from "phoenix" import {LiveSocket} from "phoenix_live_view" import topbar from "../vendor/topbar" +let Hooks = {} + +Hooks.ScrollBottom = { + mounted() { + this.scrollToBottom() + this.observer = new MutationObserver(() => { + if (this.isNearBottom()) this.scrollToBottom() + }) + this.observer.observe(this.el, {childList: true, subtree: true}) + }, + updated() { + if (this.isNearBottom()) this.scrollToBottom() + }, + destroyed() { + this.observer.disconnect() + }, + isNearBottom() { + const closeToBottomThreshold = 200 + return this.el.scrollHeight - this.el.scrollTop - this.el.clientHeight <= closeToBottomThreshold + }, + scrollToBottom() { + this.el.scrollTop = this.el.scrollHeight + } +} + let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") let liveSocket = new LiveSocket("/live", Socket, { - params: {_csrf_token: csrfToken} + params: {_csrf_token: csrfToken}, + hooks: Hooks }) // Show progress bar on live navigation and form submits diff --git a/lib/elixir_ai/ai_utils/chat_utils.ex b/lib/elixir_ai/ai_utils/chat_utils.ex index f9e32f3..e266f9f 100644 --- a/lib/elixir_ai/ai_utils/chat_utils.ex +++ b/lib/elixir_ai/ai_utils/chat_utils.ex @@ -2,24 +2,52 @@ defmodule ElixirAi.ChatUtils do require Logger import ElixirAi.AiUtils.StreamLineUtils + def ai_tool( + name: name, + description: description, + function: function, + parameters: parameters + ) do + schema = %{ + "type" => "function", + "function" => %{ + "name" => name, + "description" => description, + "parameters" => parameters + # %{ + # "type" => "object", + # "properties" => %{ + # "name" => %{"type" => "string"}, + # "value" => %{"type" => "string"} + # }, + # "required" => ["name", "value"] + # } + } + } + + %{ + name: name, + definition: schema, + function: function + } + end + def request_ai_response(server, messages, tools) do Task.start(fn -> api_url = Application.fetch_env!(:elixir_ai, :ai_endpoint) api_key = Application.fetch_env!(:elixir_ai, :ai_token) model = Application.fetch_env!(:elixir_ai, :ai_model) - tool_definition = tools |> Enum.map(fn {_name, definition} -> definition end) - body = %{ model: model, stream: true, messages: messages |> Enum.map(&api_message/1), - tools: tool_definition + tools: Enum.map(tools, & &1.definition) } headers = [{"authorization", "Bearer #{api_key}"}] - Logger.info("sending AI request with body: #{inspect(body)}") + # Logger.info("sending AI request with body: #{inspect(body)}") case Req.post(api_url, json: body, headers: headers, diff --git a/lib/elixir_ai/ai_utils/stream_line_utils.ex b/lib/elixir_ai/ai_utils/stream_line_utils.ex index f3c6827..564bff5 100644 --- a/lib/elixir_ai/ai_utils/stream_line_utils.ex +++ b/lib/elixir_ai/ai_utils/stream_line_utils.ex @@ -96,7 +96,7 @@ defmodule ElixirAi.AiUtils.StreamLineUtils do "type" => "function", "function" => %{"name" => tool_name, "arguments" => tool_args_start} } -> - Logger.info("Received tool call start for tool #{tool_name}") + # Logger.info("Received tool call start for tool #{tool_name}") send( server, @@ -104,7 +104,7 @@ defmodule ElixirAi.AiUtils.StreamLineUtils do ) %{"index" => tool_index, "function" => %{"arguments" => tool_args_diff}} -> - Logger.info("Received tool call middle for index #{tool_index}") + # Logger.info("Received tool call middle for index #{tool_index}") send(server, {:ai_tool_call_middle, id, {tool_args_diff, tool_index}}) other -> @@ -117,7 +117,7 @@ defmodule ElixirAi.AiUtils.StreamLineUtils do "choices" => [%{"finish_reason" => "tool_calls"}], "id" => id }) do - Logger.info("Received tool call end") + # Logger.info("Received tool call end") send(server, {:ai_tool_call_end, id}) end diff --git a/lib/elixir_ai/chat_runner.ex b/lib/elixir_ai/chat_runner.ex index 4ebab77..902fe59 100644 --- a/lib/elixir_ai/chat_runner.ex +++ b/lib/elixir_ai/chat_runner.ex @@ -9,6 +9,7 @@ defmodule ElixirAi.ChatRunner do GenServer.cast(__MODULE__, {:user_message, text_content}) end + @spec get_conversation() :: any() def get_conversation do GenServer.call(__MODULE__, :get_conversation) end @@ -19,7 +20,7 @@ defmodule ElixirAi.ChatRunner do %{ messages: [], streaming_response: nil, - turn: :user + tools: tools() }, name: __MODULE__ ) @@ -30,29 +31,28 @@ defmodule ElixirAi.ChatRunner do end def tools do - %{ - "store_thing" => %{ - definition: ElixirAi.ToolTesting.store_thing_definition("store_thing"), - function: &ElixirAi.ToolTesting.hold_thing/1 - }, - "read_thing" => %{ - definition: ElixirAi.ToolTesting.read_thing_definition("read_thing"), - function: &ElixirAi.ToolTesting.get_thing/1 - } - } + [ + ai_tool( + name: "store_thing", + description: "store a key value pair in memory", + function: &ElixirAi.ToolTesting.hold_thing/1, + parameters: ElixirAi.ToolTesting.hold_thing_params() + ), + ai_tool( + name: "read_thing", + description: "read a key value pair that was previously stored with store_thing", + function: &ElixirAi.ToolTesting.get_thing/1, + parameters: ElixirAi.ToolTesting.get_thing_params() + ) + ] end def handle_cast({:user_message, text_content}, state) do new_message = %{role: :user, content: text_content} broadcast({:user_chat_message, new_message}) - new_state = %{state | messages: state.messages ++ [new_message], turn: :assistant} + new_state = %{state | messages: state.messages ++ [new_message]} - tools = - tools() - |> Enum.map(fn {name, %{definition: definition}} -> {name, definition} end) - |> Enum.into(%{}) - - request_ai_response(self(), new_state.messages, tools) + request_ai_response(self(), new_state.messages, state.tools) {:noreply, new_state} end @@ -102,7 +102,10 @@ defmodule ElixirAi.ChatRunner do end def handle_info({:ai_stream_finish, _id}, state) do - Logger.info("AI stream finished for id #{state.streaming_response.id}, broadcasting end of AI response") + Logger.info( + "AI stream finished for id #{state.streaming_response.id}, broadcasting end of AI response" + ) + broadcast(:end_ai_response) final_message = %{ @@ -116,8 +119,7 @@ defmodule ElixirAi.ChatRunner do %{ state | streaming_response: nil, - messages: state.messages ++ [final_message], - turn: :user + messages: state.messages ++ [final_message] }} end @@ -173,8 +175,9 @@ defmodule ElixirAi.ChatRunner do Enum.map(state.streaming_response.tool_calls, fn tool_call -> case Jason.decode(tool_call.arguments) do {:ok, decoded_args} -> - tool_function = tools()[tool_call.name].function - res = tool_function.(decoded_args) + tool = state.tools |> Enum.find(fn t -> t.name == tool_call.name end) + + res = tool.function.(decoded_args) Map.put(tool_call, :result, res) {:error, e} -> @@ -203,8 +206,9 @@ defmodule ElixirAi.ChatRunner do Logger.info("All tool calls finished, broadcasting updated tool calls with results") broadcast({:tool_calls_finished, new_messages}) - {:noreply, - %{state | messages: state.messages ++ new_messages, streaming_response: nil}} + request_ai_response(self(), state.messages ++ new_messages, state.tools) + + {:noreply, %{state | messages: state.messages ++ new_messages, streaming_response: nil}} end def handle_call(:get_conversation, _from, state) do diff --git a/lib/elixir_ai/tool_testing.ex b/lib/elixir_ai/tool_testing.ex index c60c54c..c57bc32 100644 --- a/lib/elixir_ai/tool_testing.ex +++ b/lib/elixir_ai/tool_testing.ex @@ -5,14 +5,29 @@ defmodule ElixirAi.ToolTesting do GenServer.cast(__MODULE__, {:hold_thing, thing}) end - def get_thing do - GenServer.call(__MODULE__, :get_thing) + def hold_thing_params do + %{ + "type" => "object", + "properties" => %{ + "name" => %{"type" => "string"}, + "value" => %{"type" => "string"} + }, + "required" => ["name", "value"] + } end def get_thing(_) do GenServer.call(__MODULE__, :get_thing) end + def get_thing_params do + %{ + "type" => "object", + "properties" => %{}, + "required" => [] + } + end + def store_thing_definition(name) do %{ "type" => "function", diff --git a/lib/elixir_ai_web/live/chat_live.ex b/lib/elixir_ai_web/live/chat_live.ex index 3c1c058..7dfdd04 100644 --- a/lib/elixir_ai_web/live/chat_live.ex +++ b/lib/elixir_ai_web/live/chat_live.ex @@ -24,7 +24,7 @@ defmodule ElixirAiWeb.ChatLive do
Live Chat
-
+
<%= if @messages == [] do %>

No messages yet.

<% end %>