190 lines
7.2 KiB
Elixir
190 lines
7.2 KiB
Elixir
defmodule BDS.AI do
|
|
@moduledoc """
|
|
Public interface for AI features — endpoint configuration, secret management,
|
|
model catalog access, and dispatching chat and one-shot inference requests.
|
|
"""
|
|
|
|
alias BDS.AI.Catalog
|
|
alias BDS.AI.Chat
|
|
alias BDS.AI.OneShot
|
|
alias BDS.AI.Runtime
|
|
alias BDS.AI.SecretBackend
|
|
alias BDS.MapUtils
|
|
|
|
import BDS.AI.SettingsStore,
|
|
only: [
|
|
get_setting: 1,
|
|
put_setting: 2,
|
|
delete_setting: 1,
|
|
put_secret: 3,
|
|
get_secret: 2,
|
|
encrypted_key: 1
|
|
]
|
|
|
|
@typedoc "Endpoint kind such as :chat, :airplane_chat, :embedding, etc."
|
|
@type endpoint_kind :: atom()
|
|
|
|
@typedoc "Endpoint configuration map."
|
|
@type endpoint :: %{
|
|
kind: endpoint_kind(),
|
|
url: String.t() | nil,
|
|
api_key: String.t() | nil,
|
|
model: String.t() | nil
|
|
}
|
|
|
|
@typedoc "Attribute map for endpoint operations."
|
|
@type endpoint_attrs :: %{optional(atom()) => term(), optional(String.t()) => term()}
|
|
|
|
@spec put_endpoint(endpoint_kind(), endpoint_attrs(), keyword()) ::
|
|
{:ok, endpoint()} | {:error, term()}
|
|
def put_endpoint(kind, attrs, opts \\ [])
|
|
when is_atom(kind) and is_map(attrs) and is_list(opts) do
|
|
backend = Keyword.get(opts, :secret_backend, SecretBackend)
|
|
kind_key = Atom.to_string(kind)
|
|
|
|
url = MapUtils.attr(attrs, :url)
|
|
model = MapUtils.attr(attrs, :model)
|
|
api_key = MapUtils.attr(attrs, :api_key)
|
|
|
|
with :ok <- put_setting("ai.#{kind_key}.url", url),
|
|
:ok <- put_setting("ai.#{kind_key}.model", model),
|
|
:ok <- put_secret("ai.#{kind_key}.api_key", api_key, backend) do
|
|
{:ok, %{kind: kind, url: url, api_key: api_key, model: model}}
|
|
end
|
|
end
|
|
|
|
@spec get_endpoint(endpoint_kind(), keyword()) ::
|
|
{:ok, endpoint() | nil} | {:error, term()}
|
|
def get_endpoint(kind, opts \\ []) when is_atom(kind) and is_list(opts) do
|
|
backend = Keyword.get(opts, :secret_backend, SecretBackend)
|
|
kind_key = Atom.to_string(kind)
|
|
url = get_setting("ai.#{kind_key}.url")
|
|
model = get_setting("ai.#{kind_key}.model")
|
|
encrypted_api_key = get_setting(encrypted_key("ai.#{kind_key}.api_key"))
|
|
|
|
if is_nil(url) and is_nil(model) and is_nil(encrypted_api_key) do
|
|
{:ok, nil}
|
|
else
|
|
with {:ok, api_key} <- get_secret(encrypted_api_key, backend) do
|
|
{:ok, %{kind: kind, url: url, api_key: api_key, model: model}}
|
|
end
|
|
end
|
|
end
|
|
|
|
@spec delete_endpoint(endpoint_kind()) :: :ok
|
|
def delete_endpoint(kind) when is_atom(kind) do
|
|
kind_key = Atom.to_string(kind)
|
|
delete_setting("ai.#{kind_key}.url")
|
|
delete_setting("ai.#{kind_key}.model")
|
|
delete_setting(encrypted_key("ai.#{kind_key}.api_key"))
|
|
:ok
|
|
end
|
|
|
|
@spec list_endpoint_models(map(), keyword()) :: {:ok, [map()]} | {:error, term()}
|
|
defdelegate list_endpoint_models(endpoint, opts \\ []), to: Catalog
|
|
|
|
@spec refresh_model_catalog(keyword()) ::
|
|
{:ok, %{success: boolean(), models_updated: non_neg_integer(), not_modified: boolean()}}
|
|
| {:error, term()}
|
|
defdelegate refresh_model_catalog(opts \\ []), to: Catalog
|
|
|
|
@spec list_catalog_providers() :: [map()]
|
|
defdelegate list_catalog_providers(), to: Catalog
|
|
|
|
@spec get_catalog_model(String.t(), String.t() | nil) :: {:ok, map()} | {:error, :not_found}
|
|
defdelegate get_catalog_model(model_id, provider_id \\ nil), to: Catalog
|
|
|
|
@spec catalog_meta(String.t()) :: {:ok, String.t() | nil}
|
|
defdelegate catalog_meta(key), to: Catalog
|
|
|
|
@spec set_airplane_mode(boolean()) :: :ok | {:error, term()}
|
|
def set_airplane_mode(enabled) when is_boolean(enabled) do
|
|
put_setting("ai.airplane_mode_enabled", Atom.to_string(enabled))
|
|
end
|
|
|
|
@spec airplane_mode?(boolean()) :: boolean()
|
|
def airplane_mode?(default \\ false) when is_boolean(default) do
|
|
case get_setting("ai.airplane_mode_enabled") do
|
|
nil -> default
|
|
"false" -> false
|
|
_other -> true
|
|
end
|
|
end
|
|
|
|
@spec put_model_preference(atom(), String.t()) ::
|
|
:ok | {:error, :unknown_model_preference | term()}
|
|
def put_model_preference(key, model) when is_atom(key) and is_binary(model) do
|
|
case Map.fetch(Runtime.model_preference_keys(), key) do
|
|
{:ok, setting_key} -> put_setting(setting_key, model)
|
|
:error -> {:error, :unknown_model_preference}
|
|
end
|
|
end
|
|
|
|
@spec get_model_preference(atom()) ::
|
|
{:ok, String.t() | nil} | {:error, :unknown_model_preference}
|
|
def get_model_preference(key) when is_atom(key) do
|
|
case Map.fetch(Runtime.model_preference_keys(), key) do
|
|
{:ok, setting_key} -> {:ok, get_setting(setting_key)}
|
|
:error -> {:error, :unknown_model_preference}
|
|
end
|
|
end
|
|
|
|
@spec put_model_capabilities(String.t(), map()) :: :ok | {:error, term()}
|
|
defdelegate put_model_capabilities(model_id, attrs), to: Catalog
|
|
|
|
@spec detect_language(String.t(), keyword()) :: {:ok, map()} | {:error, term()}
|
|
defdelegate detect_language(text, opts \\ []), to: OneShot
|
|
|
|
@spec analyze_taxonomy(map() | String.t(), keyword()) :: {:ok, map()} | {:error, term()}
|
|
defdelegate analyze_taxonomy(post_input, opts \\ []), to: OneShot
|
|
|
|
@spec analyze_import_taxonomy(map(), map(), keyword()) :: {:ok, map()} | {:error, term()}
|
|
defdelegate analyze_import_taxonomy(import_terms, existing_terms, opts \\ []), to: OneShot
|
|
|
|
@spec analyze_post(map() | String.t(), keyword()) :: {:ok, map()} | {:error, term()}
|
|
defdelegate analyze_post(post_input, opts \\ []), to: OneShot
|
|
|
|
@spec translate_post(map() | String.t(), String.t(), keyword()) ::
|
|
{:ok, map()} | {:error, term()}
|
|
defdelegate translate_post(post_input, target_language, opts \\ []), to: OneShot
|
|
|
|
@spec analyze_image(map() | String.t(), keyword()) :: {:ok, map()} | {:error, term()}
|
|
defdelegate analyze_image(media_input, opts \\ []), to: OneShot
|
|
|
|
@spec translate_media(map() | String.t(), String.t(), keyword()) ::
|
|
{:ok, map()} | {:error, term()}
|
|
defdelegate translate_media(media_input, target_language, opts \\ []), to: OneShot
|
|
|
|
@spec start_chat(map()) :: {:ok, map()} | {:error, Ecto.Changeset.t()}
|
|
defdelegate start_chat(attrs \\ %{}), to: Chat
|
|
|
|
@spec list_chat_conversations() :: [map()]
|
|
defdelegate list_chat_conversations(), to: Chat
|
|
|
|
@spec get_chat_conversation(String.t()) :: BDS.AI.ChatConversation.t() | nil
|
|
defdelegate get_chat_conversation(conversation_id), to: Chat
|
|
|
|
@spec delete_chat_conversation(String.t()) :: {:ok, :deleted} | {:error, :not_found | term()}
|
|
defdelegate delete_chat_conversation(conversation_id), to: Chat
|
|
|
|
@spec available_chat_models(String.t() | nil) :: [map()]
|
|
defdelegate available_chat_models(current_model \\ nil), to: Chat
|
|
|
|
@spec effective_chat_model(BDS.AI.ChatConversation.t() | map() | nil) :: String.t() | nil
|
|
defdelegate effective_chat_model(conversation), to: Chat
|
|
|
|
@spec set_conversation_model(String.t(), String.t()) ::
|
|
{:ok, map()} | {:error, :not_found | Ecto.Changeset.t()}
|
|
defdelegate set_conversation_model(conversation_id, model_id), to: Chat
|
|
|
|
@spec list_chat_messages(String.t()) :: [map()]
|
|
defdelegate list_chat_messages(conversation_id), to: Chat
|
|
|
|
@spec send_chat_message(String.t(), String.t(), keyword()) ::
|
|
{:ok, map()} | {:error, :not_found | term()}
|
|
defdelegate send_chat_message(conversation_id, content, opts \\ []), to: Chat
|
|
|
|
@spec cancel_chat(String.t()) :: :ok
|
|
defdelegate cancel_chat(conversation_id), to: Chat
|
|
end
|