async tool results
This commit is contained in:
@@ -6,7 +6,8 @@ defmodule ElixirAi.ChatUtils do
|
|||||||
name: name,
|
name: name,
|
||||||
description: description,
|
description: description,
|
||||||
function: function,
|
function: function,
|
||||||
parameters: parameters
|
parameters: parameters,
|
||||||
|
server: server
|
||||||
) do
|
) do
|
||||||
schema = %{
|
schema = %{
|
||||||
"type" => "function",
|
"type" => "function",
|
||||||
@@ -25,10 +26,18 @@ defmodule ElixirAi.ChatUtils do
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
run_function = fn current_message_id, tool_call_id, args ->
|
||||||
|
Task.start(fn ->
|
||||||
|
result = function.(args)
|
||||||
|
send(server, {:tool_response, current_message_id, tool_call_id, result})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
%{
|
%{
|
||||||
name: name,
|
name: name,
|
||||||
definition: schema,
|
definition: schema,
|
||||||
function: function
|
# function: function,
|
||||||
|
run_function: run_function
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ defmodule ElixirAi.AiUtils.StreamLineUtils do
|
|||||||
}) do
|
}) do
|
||||||
send(
|
send(
|
||||||
server,
|
server,
|
||||||
{:ai_stream_finish, id}
|
{:ai_text_stream_finish, id}
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ defmodule ElixirAi.ChatRunner do
|
|||||||
%{
|
%{
|
||||||
messages: [],
|
messages: [],
|
||||||
streaming_response: nil,
|
streaming_response: nil,
|
||||||
|
pending_tool_calls: [],
|
||||||
tools: tools()
|
tools: tools()
|
||||||
},
|
},
|
||||||
name: __MODULE__
|
name: __MODULE__
|
||||||
@@ -36,20 +37,23 @@ defmodule ElixirAi.ChatRunner do
|
|||||||
name: "store_thing",
|
name: "store_thing",
|
||||||
description: "store a key value pair in memory",
|
description: "store a key value pair in memory",
|
||||||
function: &ElixirAi.ToolTesting.hold_thing/1,
|
function: &ElixirAi.ToolTesting.hold_thing/1,
|
||||||
parameters: ElixirAi.ToolTesting.hold_thing_params()
|
parameters: ElixirAi.ToolTesting.hold_thing_params(),
|
||||||
|
server: __MODULE__
|
||||||
),
|
),
|
||||||
ai_tool(
|
ai_tool(
|
||||||
name: "read_thing",
|
name: "read_thing",
|
||||||
description: "read a key value pair that was previously stored with store_thing",
|
description: "read a key value pair that was previously stored with store_thing",
|
||||||
function: &ElixirAi.ToolTesting.get_thing/1,
|
function: &ElixirAi.ToolTesting.get_thing/1,
|
||||||
parameters: ElixirAi.ToolTesting.get_thing_params()
|
parameters: ElixirAi.ToolTesting.get_thing_params(),
|
||||||
|
server: __MODULE__
|
||||||
),
|
),
|
||||||
ai_tool(
|
ai_tool(
|
||||||
name: "set_background_color",
|
name: "set_background_color",
|
||||||
description:
|
description:
|
||||||
"set the background color of the chat interface, accepts specified tailwind colors",
|
"set the background color of the chat interface, accepts specified tailwind colors",
|
||||||
function: &ElixirAi.ToolTesting.set_background_color/1,
|
function: &ElixirAi.ToolTesting.set_background_color/1,
|
||||||
parameters: ElixirAi.ToolTesting.set_background_color_params()
|
parameters: ElixirAi.ToolTesting.set_background_color_params(),
|
||||||
|
server: __MODULE__
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
@@ -108,13 +112,11 @@ defmodule ElixirAi.ChatRunner do
|
|||||||
}}
|
}}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info({:ai_stream_finish, _id}, state) do
|
def handle_info({:ai_text_stream_finish, _id}, state) do
|
||||||
Logger.info(
|
Logger.info(
|
||||||
"AI stream finished for id #{state.streaming_response.id}, broadcasting end of AI response"
|
"AI stream finished for id #{state.streaming_response.id}, broadcasting end of AI response"
|
||||||
)
|
)
|
||||||
|
|
||||||
broadcast(:end_ai_response)
|
|
||||||
|
|
||||||
final_message = %{
|
final_message = %{
|
||||||
role: :assistant,
|
role: :assistant,
|
||||||
content: state.streaming_response.content,
|
content: state.streaming_response.content,
|
||||||
@@ -122,6 +124,8 @@ defmodule ElixirAi.ChatRunner do
|
|||||||
tool_calls: state.streaming_response.tool_calls
|
tool_calls: state.streaming_response.tool_calls
|
||||||
}
|
}
|
||||||
|
|
||||||
|
broadcast({:end_ai_response, final_message})
|
||||||
|
|
||||||
{:noreply,
|
{:noreply,
|
||||||
%{
|
%{
|
||||||
state
|
state
|
||||||
@@ -175,20 +179,17 @@ defmodule ElixirAi.ChatRunner do
|
|||||||
{:noreply, %{state | streaming_response: new_streaming_response}}
|
{:noreply, %{state | streaming_response: new_streaming_response}}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info({:ai_tool_call_end, _id}, state) do
|
def handle_info({:ai_tool_call_end, id}, state) do
|
||||||
Logger.info("ending tool call with tools: #{inspect(state.streaming_response.tool_calls)}")
|
Logger.info("ending tool call with tools: #{inspect(state.streaming_response.tool_calls)}")
|
||||||
|
|
||||||
tool_calls =
|
parsed_tool_calls =
|
||||||
Enum.map(state.streaming_response.tool_calls, fn tool_call ->
|
Enum.map(state.streaming_response.tool_calls, fn tool_call ->
|
||||||
case Jason.decode(tool_call.arguments) do
|
case Jason.decode(tool_call.arguments) do
|
||||||
{:ok, decoded_args} ->
|
{:ok, decoded_args} ->
|
||||||
tool = state.tools |> Enum.find(fn t -> t.name == tool_call.name end)
|
{:ok, tool_call, decoded_args}
|
||||||
|
|
||||||
res = tool.function.(decoded_args)
|
|
||||||
Map.put(tool_call, :result, res)
|
|
||||||
|
|
||||||
{:error, e} ->
|
{:error, e} ->
|
||||||
Map.put(tool_call, :error, "Failed to decode tool arguments: #{inspect(e)}")
|
{:error, tool_call, "Failed to decode tool arguments: #{inspect(e)}"}
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -196,26 +197,79 @@ defmodule ElixirAi.ChatRunner do
|
|||||||
role: :assistant,
|
role: :assistant,
|
||||||
content: state.streaming_response.content,
|
content: state.streaming_response.content,
|
||||||
reasoning_content: state.streaming_response.reasoning_content,
|
reasoning_content: state.streaming_response.reasoning_content,
|
||||||
tool_calls: tool_calls
|
tool_calls: state.streaming_response.tool_calls
|
||||||
}
|
}
|
||||||
|
|
||||||
result_messages =
|
broadcast({:tool_request_message, tool_request_message})
|
||||||
Enum.map(tool_calls, fn call ->
|
|
||||||
if Map.has_key?(call, :result) do
|
failed_call_messages =
|
||||||
%{role: :tool, content: "#{inspect(call.result)}", tool_call_id: call.id}
|
parsed_tool_calls
|
||||||
else
|
|> Enum.filter(fn
|
||||||
%{role: :tool, content: "Error in #{call.name}: #{call.error}", tool_call_id: call.id}
|
{:error, _tool_call, _error_msg} -> true
|
||||||
end
|
_ -> false
|
||||||
|
end)
|
||||||
|
|> Enum.map(fn {:error, tool_call, error_msg} ->
|
||||||
|
Logger.error("Tool call #{tool_call.name} failed with error: #{error_msg}")
|
||||||
|
%{role: :tool, content: error_msg, tool_call_id: tool_call.id}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
new_messages = [tool_request_message] ++ result_messages
|
pending_call_ids =
|
||||||
|
parsed_tool_calls
|
||||||
|
|> Enum.filter(fn
|
||||||
|
{:ok, _tool_call, _decoded_args} -> true
|
||||||
|
_ -> false
|
||||||
|
end)
|
||||||
|
|> Enum.map(fn {:ok, tool_call, decoded_args} ->
|
||||||
|
case Enum.find(state.tools, fn t -> t.name == tool_call.name end) do
|
||||||
|
nil ->
|
||||||
|
Logger.error("No tool definition found for #{tool_call.name}")
|
||||||
|
nil
|
||||||
|
|
||||||
Logger.info("All tool calls finished, broadcasting updated tool calls with results")
|
tool ->
|
||||||
broadcast({:tool_calls_finished, new_messages})
|
tool.run_function.(id, tool_call.id, decoded_args)
|
||||||
|
|
||||||
request_ai_response(self(), state.messages ++ new_messages, state.tools)
|
tool_call.id
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|> Enum.filter(& &1)
|
||||||
|
|
||||||
{:noreply, %{state | messages: state.messages ++ new_messages, streaming_response: nil}}
|
{:noreply,
|
||||||
|
%{
|
||||||
|
state
|
||||||
|
| messages: state.messages ++ [tool_request_message] ++ failed_call_messages,
|
||||||
|
pending_tool_calls: pending_call_ids
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({:tool_response, _id, tool_call_id, result}, state) do
|
||||||
|
new_message = %{role: :tool, content: inspect(result), tool_call_id: tool_call_id}
|
||||||
|
|
||||||
|
broadcast({:one_tool_finished, new_message})
|
||||||
|
|
||||||
|
new_pending_tool_calls =
|
||||||
|
Enum.filter(state.pending_tool_calls, fn id -> id != tool_call_id end)
|
||||||
|
|
||||||
|
new_streaming_response =
|
||||||
|
case new_pending_tool_calls do
|
||||||
|
[] ->
|
||||||
|
nil
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
state.streaming_response
|
||||||
|
end
|
||||||
|
|
||||||
|
if new_pending_tool_calls == [] do
|
||||||
|
broadcast(:tool_calls_finished)
|
||||||
|
request_ai_response(self(), state.messages ++ [new_message], state.tools)
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
%{
|
||||||
|
state
|
||||||
|
| pending_tool_calls: new_pending_tool_calls,
|
||||||
|
streaming_response: new_streaming_response,
|
||||||
|
messages: state.messages ++ [new_message]
|
||||||
|
}}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_call(:get_conversation, _from, state) do
|
def handle_call(:get_conversation, _from, state) do
|
||||||
|
|||||||
@@ -44,15 +44,7 @@ defmodule ElixirAi.ToolTesting do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def set_background_color_params do
|
def set_background_color_params do
|
||||||
valid_tailwind_colors = [
|
valid_tailwind_colors = ElixirAiWeb.ChatLive.valid_background_colors()
|
||||||
"bg-cyan-950/30",
|
|
||||||
"bg-red-950/30",
|
|
||||||
"bg-green-950/30",
|
|
||||||
"bg-blue-950/30",
|
|
||||||
"bg-yellow-950/30",
|
|
||||||
"bg-purple-950/30",
|
|
||||||
"bg-pink-950/30"
|
|
||||||
]
|
|
||||||
|
|
||||||
%{
|
%{
|
||||||
"type" => "object",
|
"type" => "object",
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ defmodule ElixirAiWeb.ChatMessage do
|
|||||||
def assistant_message(assigns) do
|
def assistant_message(assigns) do
|
||||||
assigns =
|
assigns =
|
||||||
assigns
|
assigns
|
||||||
|> assign(:_reasoning_id, "reasoning-#{:erlang.phash2(assigns.content)}")
|
|> assign(:_reasoning_id, "reasoning-#{:erlang.phash2({assigns.content, assigns.reasoning_content, assigns.tool_calls})}")
|
||||||
|> assign(:_expanded, false)
|
|> assign(:_expanded, false)
|
||||||
|
|
||||||
~H"""
|
~H"""
|
||||||
|
|||||||
@@ -7,6 +7,18 @@ defmodule ElixirAiWeb.ChatLive do
|
|||||||
|
|
||||||
@topic "ai_chat"
|
@topic "ai_chat"
|
||||||
|
|
||||||
|
def valid_background_colors do
|
||||||
|
[
|
||||||
|
"bg-cyan-950/30",
|
||||||
|
"bg-red-950/30",
|
||||||
|
"bg-green-950/30",
|
||||||
|
"bg-blue-950/30",
|
||||||
|
"bg-yellow-950/30",
|
||||||
|
"bg-purple-950/30",
|
||||||
|
"bg-pink-950/30"
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
if connected?(socket), do: Phoenix.PubSub.subscribe(ElixirAi.PubSub, @topic)
|
if connected?(socket), do: Phoenix.PubSub.subscribe(ElixirAi.PubSub, @topic)
|
||||||
conversation = ChatRunner.get_conversation()
|
conversation = ChatRunner.get_conversation()
|
||||||
@@ -111,35 +123,39 @@ defmodule ElixirAiWeb.ChatLive do
|
|||||||
{:noreply, assign(socket, streaming_response: updated_response)}
|
{:noreply, assign(socket, streaming_response: updated_response)}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info({:tool_calls_finished, tool_messages}, socket) do
|
def handle_info(:tool_calls_finished, socket) do
|
||||||
Logger.info("Received tool_calls_finished with #{inspect(tool_messages)}")
|
Logger.info("Received tool_calls_finished")
|
||||||
|
|
||||||
{:noreply,
|
{:noreply,
|
||||||
socket
|
socket
|
||||||
|> update(:messages, &(&1 ++ tool_messages))
|
|
||||||
|> assign(streaming_response: nil)}
|
|> assign(streaming_response: nil)}
|
||||||
end
|
end
|
||||||
|
|
||||||
# tool_calls_finished already cleared streaming_response and committed messages — ignore
|
def handle_info({:tool_request_message, tool_request_message}, socket) do
|
||||||
def handle_info(:end_ai_response, %{assigns: %{streaming_response: nil}} = socket) do
|
Logger.info("tool request message: #{inspect(tool_request_message)}")
|
||||||
{:noreply, socket}
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_info(:end_ai_response, socket) do
|
|
||||||
final_response = %{
|
|
||||||
role: :assistant,
|
|
||||||
content: socket.assigns.streaming_response.content,
|
|
||||||
reasoning_content: socket.assigns.streaming_response.reasoning_content,
|
|
||||||
tool_calls: socket.assigns.streaming_response.tool_calls
|
|
||||||
}
|
|
||||||
|
|
||||||
{:noreply,
|
{:noreply,
|
||||||
socket
|
socket
|
||||||
|> update(:messages, &(&1 ++ [final_response]))
|
|> update(:messages, &(&1 ++ [tool_request_message]))}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({:one_tool_finished, tool_response}, socket) do
|
||||||
|
Logger.info("Received one_tool_finished with #{inspect(tool_response)}")
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> update(:messages, &(&1 ++ [tool_response]))}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({:end_ai_response, final_message}, socket) do
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> update(:messages, &(&1 ++ [final_message]))
|
||||||
|> assign(streaming_response: nil)}
|
|> assign(streaming_response: nil)}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info({:set_background_color, color}, socket) do
|
def handle_info({:set_background_color, color}, socket) do
|
||||||
|
Logger.info("setting background color to #{color}")
|
||||||
{:noreply, assign(socket, background_color: color)}
|
{:noreply, assign(socket, background_color: color)}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user