246 lines
6.1 KiB
Elixir
246 lines
6.1 KiB
Elixir
defmodule ElixirAi.AiProvider do
|
|
alias ElixirAi.Data.DbHelpers
|
|
require Logger
|
|
import ElixirAi.PubsubTopics
|
|
|
|
defmodule AiProviderSchema do
|
|
defstruct [:id, :name, :model_name, :api_token, :completions_url]
|
|
|
|
def schema do
|
|
Zoi.object(%{
|
|
id: Zoi.optional(Zoi.string()),
|
|
name: Zoi.string(),
|
|
model_name: Zoi.string(),
|
|
api_token: Zoi.nullish(Zoi.string()),
|
|
completions_url: Zoi.nullish(Zoi.string())
|
|
})
|
|
end
|
|
|
|
def partial_schema do
|
|
Zoi.object(%{
|
|
id: Zoi.optional(Zoi.string()),
|
|
name: Zoi.string(),
|
|
model_name: Zoi.string()
|
|
})
|
|
end
|
|
end
|
|
|
|
def all do
|
|
sql = "SELECT id, name, model_name FROM ai_providers"
|
|
params = %{}
|
|
|
|
case DbHelpers.run_sql(sql, params, providers_topic(), AiProviderSchema.partial_schema()) do
|
|
{:error, _} ->
|
|
[]
|
|
|
|
rows ->
|
|
rows
|
|
|> Enum.map(fn row ->
|
|
row |> convert_uuid_to_string() |> then(&struct(AiProviderSchema, &1))
|
|
end)
|
|
|> tap(&Logger.debug("AiProvider.all() returning: #{inspect(&1)}"))
|
|
end
|
|
end
|
|
|
|
defp convert_uuid_to_string(%{id: id} = provider) when is_binary(id) do
|
|
%{provider | id: Ecto.UUID.cast!(id)}
|
|
end
|
|
|
|
defp convert_uuid_to_string(provider), do: provider
|
|
|
|
def create(attrs) do
|
|
now = DateTime.truncate(DateTime.utc_now(), :second)
|
|
|
|
sql = """
|
|
INSERT INTO ai_providers (
|
|
name,
|
|
model_name,
|
|
api_token,
|
|
completions_url,
|
|
inserted_at,
|
|
updated_at
|
|
) VALUES (
|
|
$(name),
|
|
$(model_name),
|
|
$(api_token),
|
|
$(completions_url),
|
|
$(inserted_at),
|
|
$(updated_at)
|
|
)
|
|
"""
|
|
|
|
params = %{
|
|
"name" => attrs.name,
|
|
"model_name" => attrs.model_name,
|
|
"api_token" => attrs.api_token,
|
|
"completions_url" => attrs.completions_url,
|
|
"inserted_at" => now,
|
|
"updated_at" => now
|
|
}
|
|
|
|
case DbHelpers.run_sql(sql, params, providers_topic()) do
|
|
{:error, :db_error} ->
|
|
{:error, :db_error}
|
|
|
|
_result ->
|
|
Logger.info(
|
|
"Provider created, broadcasting :provider_added message to topic #{providers_topic()}"
|
|
)
|
|
|
|
Phoenix.PubSub.broadcast(
|
|
ElixirAi.PubSub,
|
|
providers_topic(),
|
|
{:provider_added, attrs}
|
|
)
|
|
|
|
:ok
|
|
end
|
|
end
|
|
|
|
def find_by_name(name) do
|
|
sql = """
|
|
SELECT id, name, model_name, api_token, completions_url
|
|
FROM ai_providers
|
|
WHERE name = $(name)
|
|
LIMIT 1
|
|
"""
|
|
|
|
params = %{"name" => name}
|
|
|
|
case DbHelpers.run_sql(sql, params, providers_topic(), AiProviderSchema.schema()) do
|
|
{:error, _} -> {:error, :db_error}
|
|
[] -> {:error, :not_found}
|
|
[row | _] -> {:ok, row |> convert_uuid_to_string() |> then(&struct(AiProviderSchema, &1))}
|
|
end
|
|
end
|
|
|
|
def find_by_id(id) do
|
|
case Ecto.UUID.dump(id) do
|
|
{:ok, binary_id} ->
|
|
sql = """
|
|
SELECT id, name, model_name, api_token, completions_url
|
|
FROM ai_providers
|
|
WHERE id = $(id)
|
|
LIMIT 1
|
|
"""
|
|
|
|
params = %{"id" => binary_id}
|
|
|
|
case DbHelpers.run_sql(sql, params, providers_topic(), AiProviderSchema.schema()) do
|
|
{:error, _} ->
|
|
{:error, :db_error}
|
|
|
|
[] ->
|
|
{:error, :not_found}
|
|
|
|
[row | _] ->
|
|
{:ok, row |> convert_uuid_to_string() |> then(&struct(AiProviderSchema, &1))}
|
|
end
|
|
|
|
:error ->
|
|
{:error, :invalid_uuid}
|
|
end
|
|
end
|
|
|
|
def delete(id) do
|
|
sql = "DELETE FROM ai_providers WHERE id = $(id)::uuid"
|
|
params = %{"id" => id}
|
|
|
|
case DbHelpers.run_sql(sql, params, providers_topic()) do
|
|
{:error, :db_error} ->
|
|
{:error, :db_error}
|
|
|
|
_result ->
|
|
Logger.info(
|
|
"Provider deleted, broadcasting :provider_deleted message to topic #{providers_topic()}"
|
|
)
|
|
|
|
Phoenix.PubSub.broadcast(
|
|
ElixirAi.PubSub,
|
|
providers_topic(),
|
|
{:provider_deleted, id}
|
|
)
|
|
|
|
:ok
|
|
end
|
|
end
|
|
|
|
def ensure_default_provider do
|
|
endpoint = Application.get_env(:elixir_ai, :ai_endpoint)
|
|
token = Application.get_env(:elixir_ai, :ai_token)
|
|
model = Application.get_env(:elixir_ai, :ai_model)
|
|
|
|
if endpoint && token && model do
|
|
case find_by_name("default") do
|
|
{:error, :not_found} ->
|
|
attrs = %{
|
|
name: "default",
|
|
model_name: model,
|
|
api_token: token,
|
|
completions_url: endpoint
|
|
}
|
|
|
|
create(attrs)
|
|
|
|
{:ok, _} ->
|
|
:ok
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
else
|
|
Logger.info("AI env vars not configured, skipping default provider creation")
|
|
:ok
|
|
end
|
|
end
|
|
|
|
def ensure_providers_from_file do
|
|
case System.get_env("PROVIDERS_CONFIG_PATH") do
|
|
nil ->
|
|
:ok
|
|
|
|
path ->
|
|
case YamlElixir.read_from_file(path) do
|
|
{:ok, %{"providers" => providers}} when is_list(providers) ->
|
|
Enum.each(providers, &ensure_provider_from_yaml/1)
|
|
|
|
{:ok, _} ->
|
|
Logger.warning("providers.yml: expected a top-level 'providers' list, skipping")
|
|
|
|
{:error, reason} ->
|
|
Logger.warning("Could not read providers config from #{path}: #{inspect(reason)}")
|
|
end
|
|
end
|
|
end
|
|
|
|
def ensure_configured_providers do
|
|
ensure_default_provider()
|
|
ensure_providers_from_file()
|
|
end
|
|
|
|
defp ensure_provider_from_yaml(%{
|
|
"name" => name,
|
|
"model" => model,
|
|
"responses_endpoint" => endpoint,
|
|
"api_key" => api_key
|
|
}) do
|
|
case find_by_name(name) do
|
|
{:error, :not_found} ->
|
|
Logger.info("Creating provider '#{name}' from providers config file")
|
|
create(%{name: name, model_name: model, api_token: api_key, completions_url: endpoint})
|
|
|
|
{:ok, _} ->
|
|
Logger.debug("Provider '#{name}' already exists, skipping")
|
|
|
|
{:error, reason} ->
|
|
Logger.warning("Could not check existence of provider '#{name}': #{inspect(reason)}")
|
|
end
|
|
end
|
|
|
|
defp ensure_provider_from_yaml(entry) do
|
|
Logger.warning(
|
|
"Skipping invalid provider entry in providers config file (must have name, model, responses_endpoint, api_key): #{inspect(entry)}"
|
|
)
|
|
end
|
|
end
|