more tool calling working

This commit is contained in:
2026-03-06 09:30:19 -07:00
parent 7c7e763809
commit 3a065871a9
6 changed files with 109 additions and 36 deletions

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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",

View File

@@ -24,7 +24,7 @@ defmodule ElixirAiWeb.ChatLive do
<div class="px-4 py-3 font-semibold ">
Live Chat
</div>
<div class="flex-1 overflow-y-auto p-4 bg-cyan-950/30 rounded-lg">
<div id="chat-messages" phx-hook="ScrollBottom" class="flex-1 overflow-y-auto p-4 bg-cyan-950/30 rounded-lg">
<%= if @messages == [] do %>
<p class="text-sm text-center mt-4">No messages yet.</p>
<% end %>