fix: more fixes to AI chat
This commit is contained in:
@@ -479,10 +479,44 @@ defmodule BDS.AI.Chat do
|
|||||||
|
|
||||||
case Catalog.decode_nullable_json(message.tool_calls) do
|
case Catalog.decode_nullable_json(message.tool_calls) do
|
||||||
nil -> base
|
nil -> base
|
||||||
tool_calls -> Map.put(base, "tool_calls", tool_calls)
|
tool_calls -> Map.put(base, "tool_calls", tool_calls_for_runtime(tool_calls))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp tool_calls_for_runtime(tool_calls) when is_list(tool_calls) do
|
||||||
|
Enum.map(tool_calls, &tool_call_for_runtime/1)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp tool_calls_for_runtime(tool_calls), do: tool_calls
|
||||||
|
|
||||||
|
defp tool_call_for_runtime(%{"type" => "function", "function" => %{} = _function} = tool_call) do
|
||||||
|
tool_call
|
||||||
|
end
|
||||||
|
|
||||||
|
defp tool_call_for_runtime(%{"id" => id, "name" => name} = tool_call) do
|
||||||
|
%{
|
||||||
|
"id" => id,
|
||||||
|
"type" => "function",
|
||||||
|
"function" => %{
|
||||||
|
"name" => name,
|
||||||
|
"arguments" => Jason.encode!(tool_call["arguments"] || %{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp tool_call_for_runtime(%{id: id, name: name} = tool_call) do
|
||||||
|
%{
|
||||||
|
"id" => id,
|
||||||
|
"type" => "function",
|
||||||
|
"function" => %{
|
||||||
|
"name" => name,
|
||||||
|
"arguments" => Jason.encode!(Map.get(tool_call, :arguments) || %{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp tool_call_for_runtime(tool_call), do: tool_call
|
||||||
|
|
||||||
defp truncate_chat_messages(messages, model, tools) do
|
defp truncate_chat_messages(messages, model, tools) do
|
||||||
context_window = model_context_window(model)
|
context_window = model_context_window(model)
|
||||||
reserve = min(@default_max_output_tokens, max(div(context_window, 4), 512))
|
reserve = min(@default_max_output_tokens, max(div(context_window, 4), 512))
|
||||||
|
|||||||
@@ -62,6 +62,16 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.MessageBuild do
|
|||||||
next_turn_index = turn_index + 1
|
next_turn_index = turn_index + 1
|
||||||
{entries, start_entry(message, next_turn_index, assigns), next_turn_index}
|
{entries, start_entry(message, next_turn_index, assigns), next_turn_index}
|
||||||
|
|
||||||
|
:assistant ->
|
||||||
|
next_entry = start_entry(message, turn_index, assigns)
|
||||||
|
|
||||||
|
if tool_only_assistant_entry?(current_entry) do
|
||||||
|
{entries, merge_tool_only_entry(current_entry, next_entry), turn_index}
|
||||||
|
else
|
||||||
|
entries = finalize_entry(entries, current_entry)
|
||||||
|
{entries, next_entry, turn_index}
|
||||||
|
end
|
||||||
|
|
||||||
_other ->
|
_other ->
|
||||||
entries = finalize_entry(entries, current_entry)
|
entries = finalize_entry(entries, current_entry)
|
||||||
{entries, start_entry(message, turn_index, assigns), turn_index}
|
{entries, start_entry(message, turn_index, assigns), turn_index}
|
||||||
@@ -99,6 +109,22 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.MessageBuild do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp tool_only_assistant_entry?(%{role: :assistant, content: content} = entry) do
|
||||||
|
String.trim(content || "") == "" and
|
||||||
|
(entry.tool_markers != [] or entry.inline_surfaces != [] or entry.tool_surfaces != [])
|
||||||
|
end
|
||||||
|
|
||||||
|
defp tool_only_assistant_entry?(_entry), do: false
|
||||||
|
|
||||||
|
defp merge_tool_only_entry(tool_entry, assistant_entry) do
|
||||||
|
%{
|
||||||
|
assistant_entry
|
||||||
|
| tool_markers: tool_entry.tool_markers ++ assistant_entry.tool_markers,
|
||||||
|
inline_surfaces: tool_entry.inline_surfaces ++ assistant_entry.inline_surfaces,
|
||||||
|
tool_surfaces: tool_entry.tool_surfaces ++ assistant_entry.tool_surfaces
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
defp pending_user_message(_messages, nil), do: nil
|
defp pending_user_message(_messages, nil), do: nil
|
||||||
|
|
||||||
defp pending_user_message(messages, %{message: message}) when is_binary(message) do
|
defp pending_user_message(messages, %{message: message}) when is_binary(message) do
|
||||||
|
|||||||
@@ -602,6 +602,17 @@ defmodule BDS.AITest do
|
|||||||
assert get_in(render_chart_schema, ["series", "items", "properties", "segments"]) != nil
|
assert get_in(render_chart_schema, ["series", "items", "properties", "segments"]) != nil
|
||||||
|
|
||||||
assert Enum.any?(second_request.messages, fn message -> message["role"] == "tool" end)
|
assert Enum.any?(second_request.messages, fn message -> message["role"] == "tool" end)
|
||||||
|
|
||||||
|
assert Enum.any?(second_request.messages, fn message ->
|
||||||
|
message["role"] == "assistant" and
|
||||||
|
message["tool_calls"] == [
|
||||||
|
%{
|
||||||
|
"id" => "call-blog-stats",
|
||||||
|
"type" => "function",
|
||||||
|
"function" => %{"name" => "blog_stats", "arguments" => "{}"}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "chat does not prompt models to emit textual tool calls when tools are unavailable" do
|
test "chat does not prompt models to emit textual tool calls when tools are unavailable" do
|
||||||
|
|||||||
@@ -2216,6 +2216,73 @@ defmodule BDS.Desktop.ShellLiveTest do
|
|||||||
assert html =~ "Posts"
|
assert html =~ "Posts"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "chat editor folds tool-only assistant steps into the final assistant answer" do
|
||||||
|
assert {:ok, conversation} = AI.start_chat(%{title: "Tool Chat", model: "gpt-4.1"})
|
||||||
|
|
||||||
|
now = Persistence.now_ms()
|
||||||
|
|
||||||
|
Repo.insert!(
|
||||||
|
BDS.AI.ChatMessage.changeset(%BDS.AI.ChatMessage{}, %{
|
||||||
|
conversation_id: conversation.id,
|
||||||
|
role: :user,
|
||||||
|
content: "Show posts per month",
|
||||||
|
created_at: now
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
Repo.insert!(
|
||||||
|
BDS.AI.ChatMessage.changeset(%BDS.AI.ChatMessage{}, %{
|
||||||
|
conversation_id: conversation.id,
|
||||||
|
role: :assistant,
|
||||||
|
content: nil,
|
||||||
|
tool_calls:
|
||||||
|
Jason.encode!([
|
||||||
|
%{
|
||||||
|
"id" => "call-count-posts",
|
||||||
|
"name" => "count_posts",
|
||||||
|
"arguments" => %{"groupBy" => ["month"], "year" => 2026}
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
created_at: now + 1
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
Repo.insert!(
|
||||||
|
BDS.AI.ChatMessage.changeset(%BDS.AI.ChatMessage{}, %{
|
||||||
|
conversation_id: conversation.id,
|
||||||
|
role: :tool,
|
||||||
|
tool_call_id: "call-count-posts",
|
||||||
|
content: Jason.encode!([%{"month" => 5, "count" => 3}]),
|
||||||
|
created_at: now + 2
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
Repo.insert!(
|
||||||
|
BDS.AI.ChatMessage.changeset(%BDS.AI.ChatMessage{}, %{
|
||||||
|
conversation_id: conversation.id,
|
||||||
|
role: :assistant,
|
||||||
|
content: "Here is the chart.",
|
||||||
|
created_at: now + 3
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
|
||||||
|
|
||||||
|
html =
|
||||||
|
render_click(view, "pin_sidebar_item", %{
|
||||||
|
"route" => "chat",
|
||||||
|
"id" => conversation.id,
|
||||||
|
"title" => conversation.title,
|
||||||
|
"subtitle" => conversation.model || "chat"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert html =~ "count_posts"
|
||||||
|
assert html =~ "Here is the chart."
|
||||||
|
assert html =~ ~s(<span class="chat-message-role">Assistant</span>)
|
||||||
|
|
||||||
|
assert length(:binary.matches(html, ~s(<span class="chat-message-role">Assistant</span>))) == 1
|
||||||
|
end
|
||||||
|
|
||||||
test "chat editor marks user message text as compact" do
|
test "chat editor marks user message text as compact" do
|
||||||
assert {:ok, conversation} = AI.start_chat(%{title: "Compact Chat", model: "gpt-4.1"})
|
assert {:ok, conversation} = AI.start_chat(%{title: "Compact Chat", model: "gpt-4.1"})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user