feat: added tool support setup for models
This commit is contained in:
@@ -452,7 +452,7 @@ defmodule BDS.AI.Chat do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp build_chat_request(conversation, messages, model, project_id, tools) do
|
defp build_chat_request(conversation, messages, model, project_id, tools) do
|
||||||
system_message = %{"role" => "system", "content" => chat_system_prompt(project_id)}
|
system_message = %{"role" => "system", "content" => chat_system_prompt(project_id, tools)}
|
||||||
|
|
||||||
%{
|
%{
|
||||||
operation: :chat,
|
operation: :chat,
|
||||||
@@ -511,15 +511,14 @@ defmodule BDS.AI.Chat do
|
|||||||
ChatTools.available_specs(project_id, Catalog.model_capabilities(model))
|
ChatTools.available_specs(project_id, Catalog.model_capabilities(model))
|
||||||
end
|
end
|
||||||
|
|
||||||
defp chat_system_prompt(project_id) do
|
defp chat_system_prompt(project_id, tools) do
|
||||||
base = get_setting("ai.system_prompt") || @default_system_prompt
|
base = get_setting("ai.system_prompt") || @default_system_prompt
|
||||||
|
|
||||||
case project_stats_summary(project_id) do
|
with true <- tools != [],
|
||||||
nil ->
|
summary when is_binary(summary) <- project_stats_summary(project_id) do
|
||||||
base
|
base <> "\n\nCurrent blog statistics:\n" <> summary <> "\n\n" <> blog_tool_guidance()
|
||||||
|
else
|
||||||
summary ->
|
_other -> base
|
||||||
base <> "\n\nCurrent blog statistics:\n" <> summary <> "\n\n" <> blog_tool_guidance()
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
|
|||||||
"online_api_key" => Map.get(online_endpoint || %{}, :api_key, ""),
|
"online_api_key" => Map.get(online_endpoint || %{}, :api_key, ""),
|
||||||
"online_chat_model" =>
|
"online_chat_model" =>
|
||||||
get_model_preference(:chat) || Map.get(online_endpoint || %{}, :model, ""),
|
get_model_preference(:chat) || Map.get(online_endpoint || %{}, :model, ""),
|
||||||
|
"online_chat_tools" =>
|
||||||
|
model_supports_tool_calls?(
|
||||||
|
get_model_preference(:chat) || Map.get(online_endpoint || %{}, :model, "")
|
||||||
|
),
|
||||||
"online_title_model" => get_model_preference(:title),
|
"online_title_model" => get_model_preference(:title),
|
||||||
"online_image_analysis_model" => get_model_preference(:image_analysis),
|
"online_image_analysis_model" => get_model_preference(:image_analysis),
|
||||||
"offline_url" => Map.get(airplane_endpoint || %{}, :url, ""),
|
"offline_url" => Map.get(airplane_endpoint || %{}, :url, ""),
|
||||||
@@ -25,6 +29,10 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
|
|||||||
"offline_chat_model" =>
|
"offline_chat_model" =>
|
||||||
get_model_preference(:airplane_chat) ||
|
get_model_preference(:airplane_chat) ||
|
||||||
Map.get(airplane_endpoint || %{}, :model, ""),
|
Map.get(airplane_endpoint || %{}, :model, ""),
|
||||||
|
"offline_chat_tools" =>
|
||||||
|
model_supports_tool_calls?(
|
||||||
|
get_model_preference(:airplane_chat) || Map.get(airplane_endpoint || %{}, :model, "")
|
||||||
|
),
|
||||||
"offline_title_model" => get_model_preference(:airplane_title),
|
"offline_title_model" => get_model_preference(:airplane_title),
|
||||||
"offline_image_analysis_model" => get_model_preference(:airplane_image_analysis),
|
"offline_image_analysis_model" => get_model_preference(:airplane_image_analysis),
|
||||||
"system_prompt" => EditorSettings.get_global_setting("ai.system_prompt") || ""
|
"system_prompt" => EditorSettings.get_global_setting("ai.system_prompt") || ""
|
||||||
@@ -90,9 +98,13 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
|
|||||||
:ok <- AI.delete_endpoint(:mistral),
|
:ok <- AI.delete_endpoint(:mistral),
|
||||||
:ok <- AI.set_airplane_mode(attrs.offline_mode),
|
:ok <- AI.set_airplane_mode(attrs.offline_mode),
|
||||||
:ok <- maybe_put_model_preference(:chat, attrs.online_chat_model),
|
:ok <- maybe_put_model_preference(:chat, attrs.online_chat_model),
|
||||||
|
:ok <-
|
||||||
|
maybe_put_chat_model_capabilities(attrs.online_chat_model, attrs.online_chat_tools),
|
||||||
:ok <- maybe_put_model_preference(:title, attrs.online_title_model),
|
:ok <- maybe_put_model_preference(:title, attrs.online_title_model),
|
||||||
:ok <- maybe_put_model_preference(:image_analysis, attrs.online_image_analysis_model),
|
:ok <- maybe_put_model_preference(:image_analysis, attrs.online_image_analysis_model),
|
||||||
:ok <- maybe_put_model_preference(:airplane_chat, attrs.offline_chat_model),
|
:ok <- maybe_put_model_preference(:airplane_chat, attrs.offline_chat_model),
|
||||||
|
:ok <-
|
||||||
|
maybe_put_chat_model_capabilities(attrs.offline_chat_model, attrs.offline_chat_tools),
|
||||||
:ok <- maybe_put_model_preference(:airplane_title, attrs.offline_title_model),
|
:ok <- maybe_put_model_preference(:airplane_title, attrs.offline_title_model),
|
||||||
:ok <-
|
:ok <-
|
||||||
maybe_put_model_preference(
|
maybe_put_model_preference(
|
||||||
@@ -134,12 +146,14 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
|
|||||||
online_url: blank_to_nil(Map.get(draft, "online_url")),
|
online_url: blank_to_nil(Map.get(draft, "online_url")),
|
||||||
online_api_key: blank_to_nil(Map.get(draft, "online_api_key")),
|
online_api_key: blank_to_nil(Map.get(draft, "online_api_key")),
|
||||||
online_chat_model: blank_to_nil(Map.get(draft, "online_chat_model")),
|
online_chat_model: blank_to_nil(Map.get(draft, "online_chat_model")),
|
||||||
|
online_chat_tools: truthy?(Map.get(draft, "online_chat_tools")),
|
||||||
online_title_model: blank_to_nil(Map.get(draft, "online_title_model")),
|
online_title_model: blank_to_nil(Map.get(draft, "online_title_model")),
|
||||||
online_image_analysis_model: blank_to_nil(Map.get(draft, "online_image_analysis_model")),
|
online_image_analysis_model: blank_to_nil(Map.get(draft, "online_image_analysis_model")),
|
||||||
offline_url: blank_to_nil(Map.get(draft, "offline_url")),
|
offline_url: blank_to_nil(Map.get(draft, "offline_url")),
|
||||||
offline_api_key: blank_to_nil(Map.get(draft, "offline_api_key")),
|
offline_api_key: blank_to_nil(Map.get(draft, "offline_api_key")),
|
||||||
offline_mode: truthy?(Map.get(draft, "offline_mode")),
|
offline_mode: truthy?(Map.get(draft, "offline_mode")),
|
||||||
offline_chat_model: blank_to_nil(Map.get(draft, "offline_chat_model")),
|
offline_chat_model: blank_to_nil(Map.get(draft, "offline_chat_model")),
|
||||||
|
offline_chat_tools: truthy?(Map.get(draft, "offline_chat_tools")),
|
||||||
offline_title_model: blank_to_nil(Map.get(draft, "offline_title_model")),
|
offline_title_model: blank_to_nil(Map.get(draft, "offline_title_model")),
|
||||||
offline_image_analysis_model: blank_to_nil(Map.get(draft, "offline_image_analysis_model")),
|
offline_image_analysis_model: blank_to_nil(Map.get(draft, "offline_image_analysis_model")),
|
||||||
system_prompt: Map.get(draft, "system_prompt", "")
|
system_prompt: Map.get(draft, "system_prompt", "")
|
||||||
@@ -151,12 +165,14 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
|
|||||||
"online_url" => Map.get(params, "online_url", ""),
|
"online_url" => Map.get(params, "online_url", ""),
|
||||||
"online_api_key" => Map.get(params, "online_api_key", ""),
|
"online_api_key" => Map.get(params, "online_api_key", ""),
|
||||||
"online_chat_model" => Map.get(params, "online_chat_model", ""),
|
"online_chat_model" => Map.get(params, "online_chat_model", ""),
|
||||||
|
"online_chat_tools" => truthy?(Map.get(params, "online_chat_tools")),
|
||||||
"online_title_model" => Map.get(params, "online_title_model", ""),
|
"online_title_model" => Map.get(params, "online_title_model", ""),
|
||||||
"online_image_analysis_model" => Map.get(params, "online_image_analysis_model", ""),
|
"online_image_analysis_model" => Map.get(params, "online_image_analysis_model", ""),
|
||||||
"offline_url" => Map.get(params, "offline_url", ""),
|
"offline_url" => Map.get(params, "offline_url", ""),
|
||||||
"offline_api_key" => Map.get(params, "offline_api_key", ""),
|
"offline_api_key" => Map.get(params, "offline_api_key", ""),
|
||||||
"offline_mode" => truthy?(Map.get(params, "offline_mode")),
|
"offline_mode" => truthy?(Map.get(params, "offline_mode")),
|
||||||
"offline_chat_model" => Map.get(params, "offline_chat_model", ""),
|
"offline_chat_model" => Map.get(params, "offline_chat_model", ""),
|
||||||
|
"offline_chat_tools" => truthy?(Map.get(params, "offline_chat_tools")),
|
||||||
"offline_title_model" => Map.get(params, "offline_title_model", ""),
|
"offline_title_model" => Map.get(params, "offline_title_model", ""),
|
||||||
"offline_image_analysis_model" => Map.get(params, "offline_image_analysis_model", ""),
|
"offline_image_analysis_model" => Map.get(params, "offline_image_analysis_model", ""),
|
||||||
"system_prompt" => Map.get(params, "system_prompt", "")
|
"system_prompt" => Map.get(params, "system_prompt", "")
|
||||||
@@ -174,6 +190,24 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
|
|||||||
defp maybe_put_model_preference(_key, ""), do: :ok
|
defp maybe_put_model_preference(_key, ""), do: :ok
|
||||||
defp maybe_put_model_preference(key, value), do: AI.put_model_preference(key, value)
|
defp maybe_put_model_preference(key, value), do: AI.put_model_preference(key, value)
|
||||||
|
|
||||||
|
defp maybe_put_chat_model_capabilities(nil, _supports_tool_calls), do: :ok
|
||||||
|
defp maybe_put_chat_model_capabilities("", _supports_tool_calls), do: :ok
|
||||||
|
|
||||||
|
defp maybe_put_chat_model_capabilities(model, supports_tool_calls) do
|
||||||
|
existing = BDS.AI.Catalog.model_capabilities(model)
|
||||||
|
|
||||||
|
AI.put_model_capabilities(model, %{
|
||||||
|
supports_attachment: existing.supports_attachment,
|
||||||
|
supports_tool_calls: supports_tool_calls
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp model_supports_tool_calls?(nil), do: false
|
||||||
|
defp model_supports_tool_calls?(""), do: false
|
||||||
|
|
||||||
|
defp model_supports_tool_calls?(model),
|
||||||
|
do: BDS.AI.Catalog.model_capabilities(model).supports_tool_calls
|
||||||
|
|
||||||
defp put_endpoint_preferences(kind, url, api_key, primary_model) do
|
defp put_endpoint_preferences(kind, url, api_key, primary_model) do
|
||||||
if Enum.all?([url, api_key, primary_model], &(blank_to_nil(&1) == nil)) do
|
if Enum.all?([url, api_key, primary_model], &(blank_to_nil(&1) == nil)) do
|
||||||
AI.delete_endpoint(kind)
|
AI.delete_endpoint(kind)
|
||||||
|
|||||||
@@ -229,6 +229,10 @@
|
|||||||
<div class="setting-info"><label class="setting-label"><%= translated("Online Chat Model") %></label></div>
|
<div class="setting-info"><label class="setting-label"><%= translated("Online Chat Model") %></label></div>
|
||||||
<div class="setting-control"><input type="text" list="settings-ai-online-models" name="settings_ai[online_chat_model]" value={@settings_editor.ai["online_chat_model"]} /></div>
|
<div class="setting-control"><input type="text" list="settings-ai-online-models" name="settings_ai[online_chat_model]" value={@settings_editor.ai["online_chat_model"]} /></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-info"><label class="setting-label"><%= translated("Online Chat Tools") %></label></div>
|
||||||
|
<div class="setting-control"><label><input type="checkbox" name="settings_ai[online_chat_tools]" checked={@settings_editor.ai["online_chat_tools"]} /> <%= translated("Enable tool calls for the online chat model") %></label></div>
|
||||||
|
</div>
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<div class="setting-info"><label class="setting-label"><%= translated("Online Title Model") %></label></div>
|
<div class="setting-info"><label class="setting-label"><%= translated("Online Title Model") %></label></div>
|
||||||
<div class="setting-control"><input type="text" list="settings-ai-online-models" name="settings_ai[online_title_model]" value={@settings_editor.ai["online_title_model"]} /></div>
|
<div class="setting-control"><input type="text" list="settings-ai-online-models" name="settings_ai[online_title_model]" value={@settings_editor.ai["online_title_model"]} /></div>
|
||||||
@@ -258,6 +262,10 @@
|
|||||||
<div class="setting-info"><label class="setting-label"><%= translated("Offline Chat Model") %></label></div>
|
<div class="setting-info"><label class="setting-label"><%= translated("Offline Chat Model") %></label></div>
|
||||||
<div class="setting-control"><input type="text" list="settings-ai-offline-models" name="settings_ai[offline_chat_model]" value={@settings_editor.ai["offline_chat_model"]} /></div>
|
<div class="setting-control"><input type="text" list="settings-ai-offline-models" name="settings_ai[offline_chat_model]" value={@settings_editor.ai["offline_chat_model"]} /></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-info"><label class="setting-label"><%= translated("Offline Chat Tools") %></label></div>
|
||||||
|
<div class="setting-control"><label><input type="checkbox" name="settings_ai[offline_chat_tools]" checked={@settings_editor.ai["offline_chat_tools"]} /> <%= translated("Enable tool calls for the offline chat model") %></label></div>
|
||||||
|
</div>
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<div class="setting-info"><label class="setting-label"><%= translated("Offline Title Model") %></label></div>
|
<div class="setting-info"><label class="setting-label"><%= translated("Offline Title Model") %></label></div>
|
||||||
<div class="setting-control"><input type="text" list="settings-ai-offline-models" name="settings_ai[offline_title_model]" value={@settings_editor.ai["offline_title_model"]} /></div>
|
<div class="setting-control"><input type="text" list="settings-ai-offline-models" name="settings_ai[offline_title_model]" value={@settings_editor.ai["offline_title_model"]} /></div>
|
||||||
|
|||||||
@@ -604,6 +604,42 @@ defmodule BDS.AITest do
|
|||||||
assert Enum.any?(second_request.messages, fn message -> message["role"] == "tool" end)
|
assert Enum.any?(second_request.messages, fn message -> message["role"] == "tool" end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "chat does not prompt models to emit textual tool calls when tools are unavailable" do
|
||||||
|
{:ok, project} = create_project_fixture("No Tool Chat")
|
||||||
|
_fixtures = seed_project_content(project.id)
|
||||||
|
|
||||||
|
assert {:ok, _endpoint} =
|
||||||
|
BDS.AI.put_endpoint(
|
||||||
|
:airplane,
|
||||||
|
%{
|
||||||
|
url: "http://localhost:11434/v1",
|
||||||
|
api_key: nil,
|
||||||
|
model: "llama-plain"
|
||||||
|
},
|
||||||
|
secret_backend: FakeSecretBackend
|
||||||
|
)
|
||||||
|
|
||||||
|
assert :ok = BDS.AI.set_airplane_mode(true)
|
||||||
|
assert :ok = BDS.AI.put_model_preference(:airplane_chat, "llama-plain")
|
||||||
|
assert {:ok, conversation} = BDS.AI.start_chat(%{model: "llama-plain"})
|
||||||
|
|
||||||
|
assert {:ok, _reply} =
|
||||||
|
BDS.AI.send_chat_message(conversation.id, "Show posts per month",
|
||||||
|
runtime: FakeRuntime,
|
||||||
|
test_pid: self(),
|
||||||
|
project_id: project.id,
|
||||||
|
secret_backend: FakeSecretBackend
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_received {:runtime_request, _endpoint, first_request}
|
||||||
|
assert first_request.tools == []
|
||||||
|
|
||||||
|
refute Enum.any?(first_request.messages, fn message ->
|
||||||
|
message["role"] == "system" and
|
||||||
|
String.contains?(message["content"], "Available blog data tools")
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
test "non-stat chat tools expose concrete project data" do
|
test "non-stat chat tools expose concrete project data" do
|
||||||
{:ok, project} = create_project_fixture("Concrete Tools")
|
{:ok, project} = create_project_fixture("Concrete Tools")
|
||||||
%{post: post, media: media} = seed_project_content(project.id)
|
%{post: post, media: media} = seed_project_content(project.id)
|
||||||
|
|||||||
@@ -770,11 +770,13 @@ defmodule BDS.Desktop.ShellLiveTest do
|
|||||||
"online_url" => "https://api.example.test/v1",
|
"online_url" => "https://api.example.test/v1",
|
||||||
"online_api_key" => "online-secret",
|
"online_api_key" => "online-secret",
|
||||||
"online_chat_model" => "gpt-4.1",
|
"online_chat_model" => "gpt-4.1",
|
||||||
|
"online_chat_tools" => "true",
|
||||||
"online_title_model" => "gpt-4.1-mini",
|
"online_title_model" => "gpt-4.1-mini",
|
||||||
"online_image_analysis_model" => "gpt-4.1-vision",
|
"online_image_analysis_model" => "gpt-4.1-vision",
|
||||||
"offline_url" => "http://localhost:11434/v1",
|
"offline_url" => "http://localhost:11434/v1",
|
||||||
"offline_api_key" => "",
|
"offline_api_key" => "",
|
||||||
"offline_chat_model" => "llama3.3",
|
"offline_chat_model" => "llama3.3",
|
||||||
|
"offline_chat_tools" => "true",
|
||||||
"offline_title_model" => "llama3.2",
|
"offline_title_model" => "llama3.2",
|
||||||
"offline_image_analysis_model" => "llava:latest",
|
"offline_image_analysis_model" => "llava:latest",
|
||||||
"offline_mode" => "true",
|
"offline_mode" => "true",
|
||||||
@@ -802,6 +804,9 @@ defmodule BDS.Desktop.ShellLiveTest do
|
|||||||
assert {:ok, "llama3.3"} = AI.get_model_preference(:airplane_chat)
|
assert {:ok, "llama3.3"} = AI.get_model_preference(:airplane_chat)
|
||||||
assert {:ok, "llama3.2"} = AI.get_model_preference(:airplane_title)
|
assert {:ok, "llama3.2"} = AI.get_model_preference(:airplane_title)
|
||||||
assert {:ok, "llava:latest"} = AI.get_model_preference(:airplane_image_analysis)
|
assert {:ok, "llava:latest"} = AI.get_model_preference(:airplane_image_analysis)
|
||||||
|
|
||||||
|
assert %{supports_tool_calls: true} = BDS.AI.Catalog.model_capabilities("gpt-4.1")
|
||||||
|
assert %{supports_tool_calls: true} = BDS.AI.Catalog.model_capabilities("llama3.3")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "ai settings refresh models from the configured endpoints" do
|
test "ai settings refresh models from the configured endpoints" do
|
||||||
|
|||||||
Reference in New Issue
Block a user