more improvements on tool calling
This commit is contained in:
@@ -19,6 +19,7 @@ defmodule ElixirAi.ChatUtils do
|
||||
|
||||
headers = [{"authorization", "Bearer #{api_key}"}]
|
||||
|
||||
Logger.info("sending AI request with body: #{inspect(body)}")
|
||||
case Req.post(api_url,
|
||||
json: body,
|
||||
headers: headers,
|
||||
@@ -39,6 +40,28 @@ defmodule ElixirAi.ChatUtils do
|
||||
end)
|
||||
end
|
||||
|
||||
def api_message(%{role: :assistant, tool_calls: [_ | _] = tool_calls} = msg) do
|
||||
%{
|
||||
role: "assistant",
|
||||
content: Map.get(msg, :content, ""),
|
||||
tool_calls:
|
||||
Enum.map(tool_calls, fn call ->
|
||||
%{
|
||||
id: call.id,
|
||||
type: "function",
|
||||
function: %{
|
||||
name: call.name,
|
||||
arguments: call.arguments
|
||||
}
|
||||
}
|
||||
end)
|
||||
}
|
||||
end
|
||||
|
||||
def api_message(%{role: :tool, tool_call_id: tool_call_id, content: content}) do
|
||||
%{role: "tool", tool_call_id: tool_call_id, content: content}
|
||||
end
|
||||
|
||||
def api_message(%{role: role, content: content}) do
|
||||
%{role: Atom.to_string(role), content: content}
|
||||
end
|
||||
|
||||
@@ -76,72 +76,49 @@ defmodule ElixirAi.AiUtils.StreamLineUtils do
|
||||
)
|
||||
end
|
||||
|
||||
# start tool call
|
||||
# start and middle tool call
|
||||
def handle_stream_line(server, %{
|
||||
"choices" => [
|
||||
%{
|
||||
"delta" => %{
|
||||
"tool_calls" => [
|
||||
%{
|
||||
"function" => %{
|
||||
"name" => tool_name,
|
||||
"arguments" => tool_args_start
|
||||
}
|
||||
}
|
||||
]
|
||||
"tool_calls" => tool_calls
|
||||
},
|
||||
"finish_reason" => nil,
|
||||
"index" => tool_index
|
||||
"finish_reason" => nil
|
||||
}
|
||||
],
|
||||
"id" => id
|
||||
}) do
|
||||
send(
|
||||
server,
|
||||
{:ai_tool_call_start, id, {tool_name, tool_args_start, tool_index}}
|
||||
)
|
||||
end
|
||||
})
|
||||
when is_list(tool_calls) do
|
||||
Enum.each(tool_calls, fn
|
||||
%{
|
||||
"id" => tool_call_id,
|
||||
"index" => tool_index,
|
||||
"type" => "function",
|
||||
"function" => %{"name" => tool_name, "arguments" => tool_args_start}
|
||||
} ->
|
||||
Logger.info("Received tool call start for tool #{tool_name}")
|
||||
|
||||
# middle tool call
|
||||
def handle_stream_line(server, %{
|
||||
"choices" => [
|
||||
%{
|
||||
"delta" => %{
|
||||
"tool_calls" => [
|
||||
%{
|
||||
"function" => %{
|
||||
"arguments" => tool_args_diff
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"finish_reason" => nil,
|
||||
"index" => tool_index
|
||||
}
|
||||
],
|
||||
"id" => id
|
||||
}) do
|
||||
send(
|
||||
server,
|
||||
{:ai_tool_call_middle, id, {tool_args_diff, tool_index}}
|
||||
)
|
||||
send(
|
||||
server,
|
||||
{:ai_tool_call_start, id, {tool_name, tool_args_start, tool_index, tool_call_id}}
|
||||
)
|
||||
|
||||
%{"index" => tool_index, "function" => %{"arguments" => tool_args_diff}} ->
|
||||
Logger.info("Received tool call middle for index #{tool_index}")
|
||||
send(server, {:ai_tool_call_middle, id, {tool_args_diff, tool_index}})
|
||||
|
||||
other ->
|
||||
Logger.warning("Unmatched tool call item: #{inspect(other)}")
|
||||
end)
|
||||
end
|
||||
|
||||
# end tool call
|
||||
def handle_stream_line(server, %{
|
||||
"choices" => [
|
||||
%{
|
||||
"delta" => %{},
|
||||
"finish_reason" => "tool_calls",
|
||||
"index" => tool_index
|
||||
}
|
||||
],
|
||||
"choices" => [%{"finish_reason" => "tool_calls"}],
|
||||
"id" => id
|
||||
}) do
|
||||
send(
|
||||
server,
|
||||
{:ai_tool_call_end, id, tool_index}
|
||||
)
|
||||
Logger.info("Received tool call end")
|
||||
send(server, {:ai_tool_call_end, id})
|
||||
end
|
||||
|
||||
def handle_stream_line(_server, %{"error" => error_info}) do
|
||||
|
||||
@@ -9,6 +9,7 @@ defmodule ElixirAi.Application do
|
||||
{DNSCluster, query: Application.get_env(:elixir_ai, :dns_cluster_query) || :ignore},
|
||||
{Phoenix.PubSub, name: ElixirAi.PubSub},
|
||||
ElixirAi.ChatRunner,
|
||||
ElixirAi.ToolTesting,
|
||||
ElixirAiWeb.Endpoint
|
||||
]
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ defmodule ElixirAi.ChatRunner do
|
||||
},
|
||||
"read_thing" => %{
|
||||
definition: ElixirAi.ToolTesting.read_thing_definition("read_thing"),
|
||||
function: &ElixirAi.ToolTesting.get_thing/0
|
||||
function: &ElixirAi.ToolTesting.get_thing/1
|
||||
}
|
||||
}
|
||||
end
|
||||
@@ -102,6 +102,7 @@ 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")
|
||||
broadcast(:end_ai_response)
|
||||
|
||||
final_message = %{
|
||||
@@ -120,7 +121,10 @@ defmodule ElixirAi.ChatRunner do
|
||||
}}
|
||||
end
|
||||
|
||||
def handle_info({:ai_tool_call_start, _id, {tool_name, tool_args_start, tool_index}}, state) do
|
||||
def handle_info(
|
||||
{:ai_tool_call_start, _id, {tool_name, tool_args_start, tool_index, tool_call_id}},
|
||||
state
|
||||
) do
|
||||
Logger.info("AI started tool call #{tool_name}")
|
||||
|
||||
new_streaming_response = %{
|
||||
@@ -131,7 +135,8 @@ defmodule ElixirAi.ChatRunner do
|
||||
%{
|
||||
name: tool_name,
|
||||
arguments: tool_args_start,
|
||||
index: tool_index
|
||||
index: tool_index,
|
||||
id: tool_call_id
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -161,68 +166,45 @@ defmodule ElixirAi.ChatRunner do
|
||||
{:noreply, %{state | streaming_response: new_streaming_response}}
|
||||
end
|
||||
|
||||
def handle_info({:ai_tool_call_end, _id, tool_index}, 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)}")
|
||||
|
||||
tool_calls =
|
||||
Enum.map(state.streaming_response.tool_calls, fn
|
||||
%{
|
||||
arguments: existing_args,
|
||||
index: ^tool_index
|
||||
} = tool_call ->
|
||||
case Jason.decode(existing_args) do
|
||||
{:ok, decoded_args} ->
|
||||
tool_function = tools()[tool_call.name].function
|
||||
res = tool_function.(decoded_args)
|
||||
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)
|
||||
Map.put(tool_call, :result, res)
|
||||
|
||||
Map.put(tool_call, :result, res)
|
||||
|
||||
{:error, e} ->
|
||||
Map.put(tool_call, :error, "Failed to decode tool arguments: #{inspect(e)}")
|
||||
end
|
||||
|
||||
other ->
|
||||
other
|
||||
{:error, e} ->
|
||||
Map.put(tool_call, :error, "Failed to decode tool arguments: #{inspect(e)}")
|
||||
end
|
||||
end)
|
||||
|
||||
all_tool_calls_finished =
|
||||
Enum.all?(tool_calls, fn call ->
|
||||
Map.has_key?(call, :result) or Map.has_key?(call, :error)
|
||||
tool_request_message = %{
|
||||
role: :assistant,
|
||||
content: state.streaming_response.content,
|
||||
reasoning_content: state.streaming_response.reasoning_content,
|
||||
tool_calls: tool_calls
|
||||
}
|
||||
|
||||
result_messages =
|
||||
Enum.map(tool_calls, fn call ->
|
||||
if Map.has_key?(call, :result) do
|
||||
%{role: :tool, content: "#{inspect(call.result)}", tool_call_id: call.id}
|
||||
else
|
||||
%{role: :tool, content: "Error in #{call.name}: #{call.error}", tool_call_id: call.id}
|
||||
end
|
||||
end)
|
||||
|
||||
state =
|
||||
case all_tool_calls_finished do
|
||||
true ->
|
||||
Logger.info("All tool calls finished, broadcasting updated tool calls with results")
|
||||
new_messages = [tool_request_message] ++ result_messages
|
||||
|
||||
new_message = %{
|
||||
role: :assistant,
|
||||
content: state.streaming_response.content,
|
||||
reasoning_content: state.streaming_response.reasoning_content,
|
||||
tool_calls: tool_calls
|
||||
}
|
||||
Logger.info("All tool calls finished, broadcasting updated tool calls with results")
|
||||
broadcast({:tool_calls_finished, new_messages})
|
||||
|
||||
new_state = %{
|
||||
state
|
||||
| messages:
|
||||
state.messages ++
|
||||
[
|
||||
new_message
|
||||
],
|
||||
streaming_response: nil
|
||||
}
|
||||
|
||||
broadcast({:tool_calls_finished, new_message})
|
||||
|
||||
false ->
|
||||
%{
|
||||
state
|
||||
| streaming_response: %{
|
||||
state.streaming_response
|
||||
| tool_calls: tool_calls
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
{:noreply, state}
|
||||
{:noreply,
|
||||
%{state | messages: state.messages ++ new_messages, streaming_response: nil}}
|
||||
end
|
||||
|
||||
def handle_call(:get_conversation, _from, state) do
|
||||
|
||||
@@ -9,6 +9,10 @@ defmodule ElixirAi.ToolTesting do
|
||||
GenServer.call(__MODULE__, :get_thing)
|
||||
end
|
||||
|
||||
def get_thing(_) do
|
||||
GenServer.call(__MODULE__, :get_thing)
|
||||
end
|
||||
|
||||
def store_thing_definition(name) do
|
||||
%{
|
||||
"type" => "function",
|
||||
|
||||
@@ -3,6 +3,26 @@ defmodule ElixirAiWeb.ChatMessage do
|
||||
alias ElixirAiWeb.Markdown
|
||||
alias Phoenix.LiveView.JS
|
||||
|
||||
attr :content, :string, required: true
|
||||
attr :tool_call_id, :string, required: true
|
||||
|
||||
def tool_result_message(assigns) do
|
||||
~H"""
|
||||
<div class="mb-1 max-w-prose rounded-lg border border-cyan-900/40 bg-cyan-950/20 text-xs font-mono overflow-hidden">
|
||||
<div class="flex items-center gap-2 px-3 py-1.5 border-b border-cyan-900/40 bg-cyan-900/10 text-cyan-600">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-3 h-3 shrink-0">
|
||||
<path fill-rule="evenodd" d="M10 2a.75.75 0 0 1 .75.75v.258a33.186 33.186 0 0 1 6.668 2.372.75.75 0 1 1-.636 1.354 31.66 31.66 0 0 0-1.598-.632l1.44 7.402a.75.75 0 0 1-.26.726A18.698 18.698 0 0 1 10 15.75a18.698 18.698 0 0 1-6.364-1.518.75.75 0 0 1-.26-.726l1.44-7.402a31.66 31.66 0 0 0-1.598.632.75.75 0 1 1-.636-1.354 33.186 33.186 0 0 1 6.668-2.372V2.75A.75.75 0 0 1 10 2Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<span class="text-cyan-600/70 flex-1 truncate">tool result</span>
|
||||
<span class="text-cyan-800 text-[10px] truncate max-w-[12rem]">{@tool_call_id}</span>
|
||||
</div>
|
||||
<div class="px-3 py-2">
|
||||
<pre class="text-cyan-500/70 whitespace-pre-wrap break-all">{@content}</pre>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :content, :string, required: true
|
||||
|
||||
def user_message(assigns) do
|
||||
@@ -104,29 +124,7 @@ defmodule ElixirAiWeb.ChatMessage do
|
||||
</div>
|
||||
<% end %>
|
||||
<%= for tool_call <- @tool_calls do %>
|
||||
<div class="mb-1 max-w-prose rounded-lg border border-cyan-900 bg-cyan-950/40 text-xs font-mono overflow-hidden">
|
||||
<div class="flex items-center gap-2 px-3 py-1 border-b border-cyan-900 bg-cyan-900/30 text-cyan-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-3 h-3">
|
||||
<path fill-rule="evenodd" d="M6.28 5.22a.75.75 0 0 1 0 1.06L2.56 10l3.72 3.72a.75.75 0 0 1-1.06 1.06L.97 10.53a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Zm7.44 0a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L17.44 10l-3.72-3.72a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<span class="text-cyan-300 font-semibold">{tool_call.name}</span>
|
||||
</div>
|
||||
<%= if tool_call[:arguments] && tool_call[:arguments] != "" do %>
|
||||
<div class="px-3 py-2 text-cyan-500 border-b border-cyan-900/50">
|
||||
<span class="text-cyan-700 mr-1">args</span>{tool_call.arguments}
|
||||
</div>
|
||||
<% end %>
|
||||
<%= if Map.has_key?(tool_call, :result) do %>
|
||||
<div class="px-3 py-2 text-cyan-200">
|
||||
<span class="text-cyan-700 mr-1">result</span>{inspect(tool_call.result)}
|
||||
</div>
|
||||
<% end %>
|
||||
<%= if Map.has_key?(tool_call, :error) do %>
|
||||
<div class="px-3 py-2 text-red-400">
|
||||
<span class="text-red-600 mr-1">error</span>{tool_call.error}
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<.tool_call_item tool_call={tool_call} />
|
||||
<% end %>
|
||||
<%= if @content && @content != "" do %>
|
||||
<div class="inline-block px-3 py-2 rounded-lg max-w-prose markdown bg-cyan-950/50">
|
||||
@@ -136,4 +134,141 @@ defmodule ElixirAiWeb.ChatMessage do
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
# Dispatches to the appropriate tool call component based on result state
|
||||
attr :tool_call, :map, required: true
|
||||
|
||||
defp tool_call_item(%{tool_call: tool_call} = assigns) do
|
||||
cond do
|
||||
Map.has_key?(tool_call, :error) ->
|
||||
assigns =
|
||||
assigns
|
||||
|> assign(:name, tool_call.name)
|
||||
|> assign(:arguments, tool_call[:arguments] || "")
|
||||
|> assign(:error, tool_call.error)
|
||||
|
||||
~H"<.error_tool_call name={@name} arguments={@arguments} error={@error} />"
|
||||
|
||||
Map.has_key?(tool_call, :result) ->
|
||||
assigns =
|
||||
assigns
|
||||
|> assign(:name, tool_call.name)
|
||||
|> assign(:arguments, tool_call[:arguments] || "")
|
||||
|> assign(:result, tool_call.result)
|
||||
|
||||
~H"<.success_tool_call name={@name} arguments={@arguments} result={@result} />"
|
||||
|
||||
true ->
|
||||
assigns =
|
||||
assigns
|
||||
|> assign(:name, tool_call.name)
|
||||
|> assign(:arguments, tool_call[:arguments] || "")
|
||||
|
||||
~H"<.pending_tool_call name={@name} arguments={@arguments} />"
|
||||
end
|
||||
end
|
||||
|
||||
attr :name, :string, required: true
|
||||
attr :arguments, :string, default: ""
|
||||
|
||||
defp pending_tool_call(assigns) do
|
||||
~H"""
|
||||
<div class="mb-1 max-w-prose rounded-lg border border-cyan-900 bg-cyan-950/40 text-xs font-mono overflow-hidden">
|
||||
<div class="flex items-center gap-2 px-3 py-1.5 border-b border-cyan-900 bg-cyan-900/30 text-cyan-400">
|
||||
<.tool_call_icon />
|
||||
<span class="text-cyan-300 font-semibold flex-1">{@name}</span>
|
||||
<span class="flex items-center gap-1 text-cyan-600">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-cyan-600 animate-pulse inline-block"></span>
|
||||
<span class="text-[10px]">running</span>
|
||||
</span>
|
||||
</div>
|
||||
<.tool_call_args arguments={@arguments} />
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :name, :string, required: true
|
||||
attr :arguments, :string, default: ""
|
||||
attr :result, :any, required: true
|
||||
|
||||
defp success_tool_call(assigns) do
|
||||
assigns =
|
||||
assign(assigns, :result_str, case assigns.result do
|
||||
s when is_binary(s) -> s
|
||||
other -> inspect(other, pretty: true, limit: :infinity)
|
||||
end)
|
||||
|
||||
~H"""
|
||||
<div class="mb-1 max-w-prose rounded-lg border border-cyan-900 bg-cyan-950/40 text-xs font-mono overflow-hidden">
|
||||
<div class="flex items-center gap-2 px-3 py-1.5 border-b border-cyan-900 bg-cyan-900/30 text-cyan-400">
|
||||
<.tool_call_icon />
|
||||
<span class="text-cyan-300 font-semibold flex-1">{@name}</span>
|
||||
<span class="flex items-center gap-1 text-emerald-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-3 h-3">
|
||||
<path fill-rule="evenodd" d="M12.416 3.376a.75.75 0 0 1 .208 1.04l-5 7.5a.75.75 0 0 1-1.154.114l-3-3a.75.75 0 0 1 1.06-1.06l2.353 2.353 4.493-6.74a.75.75 0 0 1 1.04-.207Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<span class="text-[10px]">done</span>
|
||||
</span>
|
||||
</div>
|
||||
<.tool_call_args arguments={@arguments} />
|
||||
<div class="px-3 py-2">
|
||||
<div class="text-cyan-700 mb-1 uppercase tracking-wider text-[10px]">result</div>
|
||||
<pre class="text-emerald-300/80 whitespace-pre-wrap break-all">{@result_str}</pre>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :name, :string, required: true
|
||||
attr :arguments, :string, default: ""
|
||||
attr :error, :string, required: true
|
||||
|
||||
defp error_tool_call(assigns) do
|
||||
~H"""
|
||||
<div class="mb-1 max-w-prose rounded-lg border border-red-900/50 bg-cyan-950/40 text-xs font-mono overflow-hidden">
|
||||
<div class="flex items-center gap-2 px-3 py-1.5 border-b border-red-900/50 bg-red-900/20 text-cyan-400">
|
||||
<.tool_call_icon />
|
||||
<span class="text-cyan-300 font-semibold flex-1">{@name}</span>
|
||||
<span class="flex items-center gap-1 text-red-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-3 h-3">
|
||||
<path d="M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14Zm0-10a.75.75 0 0 1 .75.75v3a.75.75 0 0 1-1.5 0v-3A.75.75 0 0 1 8 5Zm0 6.5a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Z" />
|
||||
</svg>
|
||||
<span class="text-[10px]">error</span>
|
||||
</span>
|
||||
</div>
|
||||
<.tool_call_args arguments={@arguments} />
|
||||
<div class="px-3 py-2 bg-red-950/20">
|
||||
<div class="text-red-700 mb-1 uppercase tracking-wider text-[10px]">error</div>
|
||||
<pre class="text-red-400 whitespace-pre-wrap break-all">{@error}</pre>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :arguments, :string, default: ""
|
||||
|
||||
defp tool_call_args(%{arguments: args} = assigns) when args != "" do
|
||||
assigns =
|
||||
assign(assigns, :pretty_args, case Jason.decode(args) do
|
||||
{:ok, decoded} -> Jason.encode!(decoded, pretty: true)
|
||||
_ -> args
|
||||
end)
|
||||
|
||||
~H"""
|
||||
<div class="px-3 py-2 border-b border-cyan-900/50">
|
||||
<div class="text-cyan-700 mb-1 uppercase tracking-wider text-[10px]">arguments</div>
|
||||
<pre class="text-cyan-400 whitespace-pre-wrap break-all">{@pretty_args}</pre>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp tool_call_args(assigns), do: ~H""
|
||||
|
||||
defp tool_call_icon(assigns) do
|
||||
~H"""
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-3 h-3 shrink-0">
|
||||
<path fill-rule="evenodd" d="M6.28 5.22a.75.75 0 0 1 0 1.06L2.56 10l3.72 3.72a.75.75 0 0 1-1.06 1.06L.97 10.53a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Zm7.44 0a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L17.44 10l-3.72-3.72a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
defmodule ElixirAiWeb.ChatLive do
|
||||
use ElixirAiWeb, :live_view
|
||||
require Logger
|
||||
import ElixirAiWeb.Spinner
|
||||
import ElixirAiWeb.ChatMessage
|
||||
alias ElixirAi.ChatRunner
|
||||
@@ -28,14 +29,17 @@ defmodule ElixirAiWeb.ChatLive do
|
||||
<p class="text-sm text-center mt-4">No messages yet.</p>
|
||||
<% end %>
|
||||
<%= for msg <- @messages do %>
|
||||
<%= if msg.role == :user do %>
|
||||
<.user_message content={msg.content} />
|
||||
<% else %>
|
||||
<.assistant_message
|
||||
content={msg.content}
|
||||
reasoning_content={msg.reasoning_content}
|
||||
tool_calls={Map.get(msg, :tool_calls, [])}
|
||||
/>
|
||||
<%= cond do %>
|
||||
<% msg.role == :user -> %>
|
||||
<.user_message content={msg.content} />
|
||||
<% msg.role == :tool -> %>
|
||||
<.tool_result_message content={msg.content} tool_call_id={msg.tool_call_id} />
|
||||
<% true -> %>
|
||||
<.assistant_message
|
||||
content={msg.content}
|
||||
reasoning_content={msg.reasoning_content}
|
||||
tool_calls={Map.get(msg, :tool_calls, [])}
|
||||
/>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%= if @streaming_response do %>
|
||||
@@ -102,18 +106,25 @@ defmodule ElixirAiWeb.ChatLive do
|
||||
{:noreply, assign(socket, streaming_response: updated_response)}
|
||||
end
|
||||
|
||||
def handle_info({:tool_calls_finished, final_message}, socket) do
|
||||
def handle_info({:tool_calls_finished, tool_messages}, socket) do
|
||||
Logger.info("Received tool_calls_finished with #{inspect(tool_messages)}")
|
||||
{:noreply,
|
||||
socket
|
||||
|> update(:messages, &(&1 ++ [final_message]))
|
||||
|> update(:messages, &(&1 ++ tool_messages))
|
||||
|> assign(streaming_response: nil)}
|
||||
end
|
||||
|
||||
# tool_calls_finished already cleared streaming_response and committed messages — ignore
|
||||
def handle_info(:end_ai_response, %{assigns: %{streaming_response: nil}} = socket) do
|
||||
{: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
|
||||
reasoning_content: socket.assigns.streaming_response.reasoning_content,
|
||||
tool_calls: socket.assigns.streaming_response.tool_calls
|
||||
}
|
||||
|
||||
{:noreply,
|
||||
|
||||
Reference in New Issue
Block a user