fix: more work on chat and chat titles
This commit is contained in:
@@ -111,7 +111,8 @@ defmodule BDS.AI.Catalog do
|
||||
def put_model_capabilities(model_id, attrs) when is_binary(model_id) and is_map(attrs) do
|
||||
capabilities = %{
|
||||
supports_attachment: truthy?(BDS.MapUtils.attr(attrs, :supports_attachment)),
|
||||
supports_tool_calls: truthy?(BDS.MapUtils.attr(attrs, :supports_tool_calls))
|
||||
supports_tool_calls: truthy?(BDS.MapUtils.attr(attrs, :supports_tool_calls)),
|
||||
disables_reasoning: truthy?(BDS.MapUtils.attr(attrs, :disables_reasoning))
|
||||
}
|
||||
|
||||
put_setting("ai.model_capabilities.#{model_id}", Jason.encode!(capabilities))
|
||||
@@ -163,7 +164,8 @@ defmodule BDS.AI.Catalog do
|
||||
|
||||
@spec model_capabilities(String.t()) :: %{
|
||||
supports_attachment: boolean(),
|
||||
supports_tool_calls: boolean()
|
||||
supports_tool_calls: boolean(),
|
||||
disables_reasoning: boolean()
|
||||
}
|
||||
def model_capabilities(model_id) do
|
||||
overrides = decode_model_capabilities_override(model_id)
|
||||
@@ -173,7 +175,8 @@ defmodule BDS.AI.Catalog do
|
||||
{:ok, model} ->
|
||||
%{
|
||||
supports_attachment: model.supports_attachment or "image" in model.input_modalities,
|
||||
supports_tool_calls: model.supports_tool_calls
|
||||
supports_tool_calls: model.supports_tool_calls,
|
||||
disables_reasoning: false
|
||||
}
|
||||
|
||||
_other ->
|
||||
@@ -196,7 +199,8 @@ defmodule BDS.AI.Catalog do
|
||||
String.contains?(normalized, "llava"),
|
||||
supports_tool_calls:
|
||||
String.contains?(normalized, "gpt") or String.contains?(normalized, "claude") or
|
||||
String.contains?(normalized, "tool")
|
||||
String.contains?(normalized, "tool"),
|
||||
disables_reasoning: false
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ defmodule BDS.AI.Chat do
|
||||
@moduledoc false
|
||||
|
||||
import Ecto.Query
|
||||
require Logger
|
||||
|
||||
alias BDS.AI
|
||||
alias BDS.AI.Catalog
|
||||
@@ -23,7 +24,7 @@ defmodule BDS.AI.Chat do
|
||||
|
||||
@default_system_prompt "You are the bDS AI backend. Be precise, prefer structured JSON when asked, and avoid inventing blog facts."
|
||||
@default_max_output_tokens 16_384
|
||||
@title_max_output_tokens 20
|
||||
@title_max_output_tokens 256
|
||||
@chat_title_max_length 30
|
||||
@chat_max_tool_rounds 10
|
||||
@default_context_window 128_000
|
||||
@@ -383,7 +384,7 @@ defmodule BDS.AI.Chat do
|
||||
conversation = Repo.get!(ChatConversation, conversation_id)
|
||||
|
||||
cond do
|
||||
chat_user_message_count(conversation_id) != 1 ->
|
||||
chat_user_message_count(conversation_id) < 1 ->
|
||||
{:ok, reply}
|
||||
|
||||
not generated_chat_title?(conversation.title, conversation.model) ->
|
||||
@@ -418,7 +419,25 @@ defmodule BDS.AI.Chat do
|
||||
:ok <- Runtime.validate_target(:chat_title, model, mode),
|
||||
request <- build_chat_title_request(user_content, model),
|
||||
{:ok, response} <- runtime.generate(Runtime.endpoint_with_model(endpoint, model), request, opts) do
|
||||
{:ok, sanitize_chat_title(Map.get(response, :content))}
|
||||
title = sanitize_chat_title(Map.get(response, :content))
|
||||
|
||||
if title == "" do
|
||||
Logger.warning("Chat title generation returned an empty title",
|
||||
model: model,
|
||||
content: inspect(Map.get(response, :content)),
|
||||
usage: inspect(Map.get(response, :usage))
|
||||
)
|
||||
end
|
||||
|
||||
{:ok, title}
|
||||
else
|
||||
{:error, reason} = error ->
|
||||
Logger.warning("Chat title generation failed", reason: inspect(reason))
|
||||
error
|
||||
|
||||
other ->
|
||||
Logger.warning("Chat title generation failed", reason: inspect(other))
|
||||
other
|
||||
end
|
||||
end
|
||||
|
||||
@@ -431,7 +450,7 @@ defmodule BDS.AI.Chat do
|
||||
%{
|
||||
"role" => "system",
|
||||
"content" =>
|
||||
"Generate an ultra-short title (2-3 words, max 25 characters) for this conversation. Focus ONLY on the topic. Ignore any capability disclaimers. Output ONLY the title text."
|
||||
"Generate an ultra-short title (2-3 words, max 25 characters) for this conversation. Focus ONLY on the topic. Ignore any capability disclaimers. Do not include reasoning. Output ONLY the title text."
|
||||
},
|
||||
%{"role" => "user", "content" => "Topic: #{String.slice(user_content, 0, 100)}"}
|
||||
]
|
||||
|
||||
@@ -36,6 +36,7 @@ defmodule BDS.AI.OpenAICompatibleRuntime do
|
||||
"messages" => request.messages,
|
||||
"max_tokens" => request.max_output_tokens
|
||||
}
|
||||
|> maybe_disable_thinking(request.model)
|
||||
|> maybe_put_tools(Map.get(request, :tools, []))
|
||||
|
||||
with {:ok, response} <- HttpClient.post(url, headers, Jason.encode!(payload)),
|
||||
@@ -136,6 +137,18 @@ defmodule BDS.AI.OpenAICompatibleRuntime do
|
||||
|> Map.put("tool_choice", "auto")
|
||||
end
|
||||
|
||||
defp maybe_disable_thinking(payload, model) when is_binary(model) do
|
||||
if BDS.AI.Catalog.model_capabilities(model).disables_reasoning do
|
||||
Map.update(payload, "chat_template_kwargs", %{"enable_thinking" => false}, fn kwargs ->
|
||||
Map.put(kwargs || %{}, "enable_thinking", false)
|
||||
end)
|
||||
else
|
||||
payload
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_disable_thinking(payload, _model), do: payload
|
||||
|
||||
defp normalize_tool_calls(tool_calls) do
|
||||
Enum.map(tool_calls, fn tool_call ->
|
||||
%{
|
||||
|
||||
@@ -5,6 +5,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
||||
import Phoenix.HTML, only: [raw: 1]
|
||||
|
||||
alias BDS.AI
|
||||
alias BDS.MapUtils
|
||||
alias BDS.Desktop.ShellData
|
||||
alias BDS.Desktop.ShellLive.ChatEditor.{MessageBuild, ModelSelection, ToolTracking}
|
||||
|
||||
@@ -249,8 +250,10 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
||||
)
|
||||
|
||||
case result do
|
||||
{:ok, _reply} ->
|
||||
reload.(socket, socket.assigns.workbench)
|
||||
{:ok, reply} ->
|
||||
socket
|
||||
|> update_tab_meta_from_reply(conversation_id, reply)
|
||||
|> reload.(socket.assigns.workbench)
|
||||
|
||||
{:error, :cancelled} ->
|
||||
reload.(socket, socket.assigns.workbench)
|
||||
@@ -266,6 +269,23 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
||||
end
|
||||
end
|
||||
|
||||
defp update_tab_meta_from_reply(socket, conversation_id, reply) do
|
||||
title =
|
||||
reply
|
||||
|> MapUtils.attr(:conversation, %{})
|
||||
|> MapUtils.attr(:title)
|
||||
|
||||
if is_binary(title) and String.trim(title) != "" do
|
||||
key = {:chat, conversation_id}
|
||||
|
||||
assign(socket, :tab_meta, Map.update(socket.assigns.tab_meta, key, %{title: title}, fn meta ->
|
||||
Map.put(meta, :title, title)
|
||||
end))
|
||||
else
|
||||
socket
|
||||
end
|
||||
end
|
||||
|
||||
# ── HEEx-callable helpers ─────────────────────────────────────────────────
|
||||
|
||||
@spec message_role_label(term()) :: term()
|
||||
|
||||
@@ -21,6 +21,10 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
|
||||
model_supports_tool_calls?(
|
||||
get_model_preference(:chat) || Map.get(online_endpoint || %{}, :model, "")
|
||||
),
|
||||
"online_chat_disable_reasoning" =>
|
||||
model_disables_reasoning?(
|
||||
get_model_preference(:chat) || Map.get(online_endpoint || %{}, :model, "")
|
||||
),
|
||||
"online_title_model" => get_model_preference(:title),
|
||||
"online_image_analysis_model" => get_model_preference(:image_analysis),
|
||||
"offline_url" => Map.get(airplane_endpoint || %{}, :url, ""),
|
||||
@@ -33,6 +37,10 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
|
||||
model_supports_tool_calls?(
|
||||
get_model_preference(:airplane_chat) || Map.get(airplane_endpoint || %{}, :model, "")
|
||||
),
|
||||
"offline_chat_disable_reasoning" =>
|
||||
model_disables_reasoning?(
|
||||
get_model_preference(:airplane_chat) || Map.get(airplane_endpoint || %{}, :model, "")
|
||||
),
|
||||
"offline_title_model" => get_model_preference(:airplane_title),
|
||||
"offline_image_analysis_model" => get_model_preference(:airplane_image_analysis),
|
||||
"system_prompt" => EditorSettings.get_global_setting("ai.system_prompt") || ""
|
||||
@@ -99,12 +107,20 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
|
||||
:ok <- AI.set_airplane_mode(attrs.offline_mode),
|
||||
:ok <- maybe_put_model_preference(:chat, attrs.online_chat_model),
|
||||
:ok <-
|
||||
maybe_put_chat_model_capabilities(attrs.online_chat_model, attrs.online_chat_tools),
|
||||
maybe_put_chat_model_capabilities(
|
||||
attrs.online_chat_model,
|
||||
attrs.online_chat_tools,
|
||||
attrs.online_chat_disable_reasoning
|
||||
),
|
||||
: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(:airplane_chat, attrs.offline_chat_model),
|
||||
:ok <-
|
||||
maybe_put_chat_model_capabilities(attrs.offline_chat_model, attrs.offline_chat_tools),
|
||||
maybe_put_chat_model_capabilities(
|
||||
attrs.offline_chat_model,
|
||||
attrs.offline_chat_tools,
|
||||
attrs.offline_chat_disable_reasoning
|
||||
),
|
||||
:ok <- maybe_put_model_preference(:airplane_title, attrs.offline_title_model),
|
||||
:ok <-
|
||||
maybe_put_model_preference(
|
||||
@@ -147,6 +163,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
|
||||
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_tools: truthy?(Map.get(draft, "online_chat_tools")),
|
||||
online_chat_disable_reasoning: truthy?(Map.get(draft, "online_chat_disable_reasoning")),
|
||||
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")),
|
||||
offline_url: blank_to_nil(Map.get(draft, "offline_url")),
|
||||
@@ -154,6 +171,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
|
||||
offline_mode: truthy?(Map.get(draft, "offline_mode")),
|
||||
offline_chat_model: blank_to_nil(Map.get(draft, "offline_chat_model")),
|
||||
offline_chat_tools: truthy?(Map.get(draft, "offline_chat_tools")),
|
||||
offline_chat_disable_reasoning: truthy?(Map.get(draft, "offline_chat_disable_reasoning")),
|
||||
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")),
|
||||
system_prompt: Map.get(draft, "system_prompt", "")
|
||||
@@ -166,6 +184,8 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
|
||||
"online_api_key" => Map.get(params, "online_api_key", ""),
|
||||
"online_chat_model" => Map.get(params, "online_chat_model", ""),
|
||||
"online_chat_tools" => truthy?(Map.get(params, "online_chat_tools")),
|
||||
"online_chat_disable_reasoning" =>
|
||||
truthy?(Map.get(params, "online_chat_disable_reasoning")),
|
||||
"online_title_model" => Map.get(params, "online_title_model", ""),
|
||||
"online_image_analysis_model" => Map.get(params, "online_image_analysis_model", ""),
|
||||
"offline_url" => Map.get(params, "offline_url", ""),
|
||||
@@ -173,6 +193,8 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
|
||||
"offline_mode" => truthy?(Map.get(params, "offline_mode")),
|
||||
"offline_chat_model" => Map.get(params, "offline_chat_model", ""),
|
||||
"offline_chat_tools" => truthy?(Map.get(params, "offline_chat_tools")),
|
||||
"offline_chat_disable_reasoning" =>
|
||||
truthy?(Map.get(params, "offline_chat_disable_reasoning")),
|
||||
"offline_title_model" => Map.get(params, "offline_title_model", ""),
|
||||
"offline_image_analysis_model" => Map.get(params, "offline_image_analysis_model", ""),
|
||||
"system_prompt" => Map.get(params, "system_prompt", "")
|
||||
@@ -190,15 +212,16 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
|
||||
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_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(nil, _supports_tool_calls, _disables_reasoning), do: :ok
|
||||
defp maybe_put_chat_model_capabilities("", _supports_tool_calls, _disables_reasoning), do: :ok
|
||||
|
||||
defp maybe_put_chat_model_capabilities(model, supports_tool_calls) do
|
||||
defp maybe_put_chat_model_capabilities(model, supports_tool_calls, disables_reasoning) do
|
||||
existing = BDS.AI.Catalog.model_capabilities(model)
|
||||
|
||||
AI.put_model_capabilities(model, %{
|
||||
supports_attachment: existing.supports_attachment,
|
||||
supports_tool_calls: supports_tool_calls
|
||||
supports_tool_calls: supports_tool_calls,
|
||||
disables_reasoning: disables_reasoning
|
||||
})
|
||||
end
|
||||
|
||||
@@ -208,6 +231,12 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
|
||||
defp model_supports_tool_calls?(model),
|
||||
do: BDS.AI.Catalog.model_capabilities(model).supports_tool_calls
|
||||
|
||||
defp model_disables_reasoning?(nil), do: false
|
||||
defp model_disables_reasoning?(""), do: false
|
||||
|
||||
defp model_disables_reasoning?(model),
|
||||
do: BDS.AI.Catalog.model_capabilities(model).disables_reasoning
|
||||
|
||||
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
|
||||
AI.delete_endpoint(kind)
|
||||
|
||||
@@ -233,6 +233,10 @@
|
||||
<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-info"><label class="setting-label"><%= translated("Online Chat Reasoning") %></label></div>
|
||||
<div class="setting-control"><label><input type="checkbox" name="settings_ai[online_chat_disable_reasoning]" checked={@settings_editor.ai["online_chat_disable_reasoning"]} /> <%= translated("Disable reasoning output for the online chat model") %></label></div>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<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>
|
||||
@@ -266,6 +270,10 @@
|
||||
<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-info"><label class="setting-label"><%= translated("Offline Chat Reasoning") %></label></div>
|
||||
<div class="setting-control"><label><input type="checkbox" name="settings_ai[offline_chat_disable_reasoning]" checked={@settings_editor.ai["offline_chat_disable_reasoning"]} /> <%= translated("Disable reasoning output for the offline chat model") %></label></div>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user