This commit is contained in:
@@ -40,11 +40,11 @@ defmodule ElixirAi.ChatUtils do
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def request_ai_response(server, messages, tools) do
|
def request_ai_response(server, messages, tools, provider) do
|
||||||
Task.start(fn ->
|
Task.start(fn ->
|
||||||
api_url = Application.fetch_env!(:elixir_ai, :ai_endpoint)
|
api_url = provider.completions_url
|
||||||
api_key = Application.fetch_env!(:elixir_ai, :ai_token)
|
api_key = provider.api_token
|
||||||
model = Application.fetch_env!(:elixir_ai, :ai_model)
|
model = provider.model_name
|
||||||
|
|
||||||
if is_nil(api_url) or api_url == "" do
|
if is_nil(api_url) or api_url == "" do
|
||||||
Logger.warning("AI endpoint is empty or nil")
|
Logger.warning("AI endpoint is empty or nil")
|
||||||
|
|||||||
@@ -38,12 +38,18 @@ defmodule ElixirAi.ChatRunner do
|
|||||||
|
|
||||||
last_message = List.last(messages)
|
last_message = List.last(messages)
|
||||||
|
|
||||||
|
provider =
|
||||||
|
case Conversation.find_provider(name) do
|
||||||
|
{:ok, p} -> p
|
||||||
|
_ -> nil
|
||||||
|
end
|
||||||
|
|
||||||
if last_message && last_message.role == :user do
|
if last_message && last_message.role == :user do
|
||||||
Logger.info(
|
Logger.info(
|
||||||
"Last message role was #{last_message.role}, requesting AI response for conversation #{name}"
|
"Last message role was #{last_message.role}, requesting AI response for conversation #{name}"
|
||||||
)
|
)
|
||||||
|
|
||||||
ElixirAi.ChatUtils.request_ai_response(self(), messages, tools(self(), name))
|
ElixirAi.ChatUtils.request_ai_response(self(), messages, tools(self(), name), provider)
|
||||||
end
|
end
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
@@ -53,9 +59,7 @@ defmodule ElixirAi.ChatRunner do
|
|||||||
streaming_response: nil,
|
streaming_response: nil,
|
||||||
pending_tool_calls: [],
|
pending_tool_calls: [],
|
||||||
tools: tools(self(), name),
|
tools: tools(self(), name),
|
||||||
ai_provider_url: Application.get_env(:elixir_ai, :ai_provider_url),
|
provider: provider
|
||||||
ai_model: Application.get_env(:elixir_ai, :ai_model),
|
|
||||||
ai_token: Application.get_env(:elixir_ai, :ai_token)
|
|
||||||
}}
|
}}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -107,7 +111,13 @@ defmodule ElixirAi.ChatRunner do
|
|||||||
store_message(state.name, new_message)
|
store_message(state.name, new_message)
|
||||||
new_state = %{state | messages: state.messages ++ [new_message]}
|
new_state = %{state | messages: state.messages ++ [new_message]}
|
||||||
|
|
||||||
ElixirAi.ChatUtils.request_ai_response(self(), new_state.messages, state.tools)
|
ElixirAi.ChatUtils.request_ai_response(
|
||||||
|
self(),
|
||||||
|
new_state.messages,
|
||||||
|
state.tools,
|
||||||
|
state.provider
|
||||||
|
)
|
||||||
|
|
||||||
{:noreply, new_state}
|
{:noreply, new_state}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -296,7 +306,13 @@ defmodule ElixirAi.ChatRunner do
|
|||||||
|
|
||||||
if new_pending_tool_calls == [] do
|
if new_pending_tool_calls == [] do
|
||||||
broadcast_ui(state.name, :tool_calls_finished)
|
broadcast_ui(state.name, :tool_calls_finished)
|
||||||
ElixirAi.ChatUtils.request_ai_response(self(), state.messages ++ [new_message], state.tools)
|
|
||||||
|
ElixirAi.ChatUtils.request_ai_response(
|
||||||
|
self(),
|
||||||
|
state.messages ++ [new_message],
|
||||||
|
state.tools,
|
||||||
|
state.provider
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
{:noreply,
|
{:noreply,
|
||||||
|
|||||||
@@ -52,25 +52,16 @@ defmodule ElixirAi.ConversationManager do
|
|||||||
def handle_call(
|
def handle_call(
|
||||||
{:create, name, ai_provider_id},
|
{:create, name, ai_provider_id},
|
||||||
_from,
|
_from,
|
||||||
%{conversations: conversations, subscriptions: subscriptions} = state
|
%{conversations: conversations} = state
|
||||||
) do
|
) do
|
||||||
if Map.has_key?(conversations, name) do
|
if Map.has_key?(conversations, name) do
|
||||||
{:reply, {:error, :already_exists}, state}
|
{:reply, {:error, :already_exists}, state}
|
||||||
else
|
else
|
||||||
case Conversation.create(name, ai_provider_id) do
|
case Conversation.create(name, ai_provider_id) do
|
||||||
:ok ->
|
:ok ->
|
||||||
case start_and_subscribe(name, subscriptions) do
|
reply_with_started(name, state, fn new_state ->
|
||||||
{:ok, pid, new_subscriptions} ->
|
%{new_state | conversations: Map.put(new_state.conversations, name, [])}
|
||||||
{:reply, {:ok, pid},
|
end)
|
||||||
%{
|
|
||||||
state
|
|
||||||
| conversations: Map.put(conversations, name, []),
|
|
||||||
subscriptions: new_subscriptions
|
|
||||||
}}
|
|
||||||
|
|
||||||
{:error, _reason} = error ->
|
|
||||||
{:reply, error, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
{:error, _} = error ->
|
{:error, _} = error ->
|
||||||
{:reply, error, state}
|
{:reply, error, state}
|
||||||
@@ -81,16 +72,10 @@ defmodule ElixirAi.ConversationManager do
|
|||||||
def handle_call(
|
def handle_call(
|
||||||
{:open, name},
|
{:open, name},
|
||||||
_from,
|
_from,
|
||||||
%{conversations: conversations, subscriptions: subscriptions} = state
|
%{conversations: conversations} = state
|
||||||
) do
|
) do
|
||||||
if Map.has_key?(conversations, name) do
|
if Map.has_key?(conversations, name) do
|
||||||
case start_and_subscribe(name, subscriptions) do
|
reply_with_started(name, state)
|
||||||
{:ok, pid, new_subscriptions} ->
|
|
||||||
{:reply, {:ok, pid}, %{state | subscriptions: new_subscriptions}}
|
|
||||||
|
|
||||||
{:error, _reason} = error ->
|
|
||||||
{:reply, error, state}
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
{:reply, {:error, :not_found}, state}
|
{:reply, {:error, :not_found}, state}
|
||||||
end
|
end
|
||||||
@@ -148,6 +133,17 @@ defmodule ElixirAi.ConversationManager do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp reply_with_started(name, state, update_state \\ fn s -> s end) do
|
||||||
|
case start_and_subscribe(name, state.subscriptions) do
|
||||||
|
{:ok, pid, new_subscriptions} ->
|
||||||
|
new_state = update_state.(%{state | subscriptions: new_subscriptions})
|
||||||
|
{:reply, {:ok, pid}, new_state}
|
||||||
|
|
||||||
|
{:error, _reason} = error ->
|
||||||
|
{:reply, error, state}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp start_and_subscribe(name, subscriptions) do
|
defp start_and_subscribe(name, subscriptions) do
|
||||||
result =
|
result =
|
||||||
case Horde.DynamicSupervisor.start_child(
|
case Horde.DynamicSupervisor.start_child(
|
||||||
|
|||||||
@@ -113,5 +113,23 @@ defmodule ElixirAi.Conversation do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def find_provider(name) do
|
||||||
|
sql = """
|
||||||
|
SELECT p.name, p.model_name, p.api_token, p.completions_url
|
||||||
|
FROM conversations c
|
||||||
|
JOIN ai_providers p ON c.ai_provider_id = p.id
|
||||||
|
WHERE c.name = $(name)
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
|
||||||
|
params = %{"name" => name}
|
||||||
|
|
||||||
|
case DbHelpers.run_sql(sql, params, "conversations", Provider.schema()) do
|
||||||
|
{:error, _} -> {:error, :db_error}
|
||||||
|
[] -> {:error, :not_found}
|
||||||
|
[row | _] -> {:ok, struct(Provider, row)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp now, do: DateTime.truncate(DateTime.utc_now(), :second)
|
defp now, do: DateTime.truncate(DateTime.utc_now(), :second)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -48,7 +48,10 @@ defmodule ElixirAi.MessageStorageTest do
|
|||||||
|
|
||||||
test "run_sql is called with user message params" do
|
test "run_sql is called with user message params" do
|
||||||
conv_name = setup_conversation()
|
conv_name = setup_conversation()
|
||||||
stub(ElixirAi.ChatUtils, :request_ai_response, fn _server, _messages, _tools -> :ok end)
|
|
||||||
|
stub(ElixirAi.ChatUtils, :request_ai_response, fn _server, _messages, _tools, _provider ->
|
||||||
|
:ok
|
||||||
|
end)
|
||||||
|
|
||||||
ElixirAi.ChatRunner.new_user_message(conv_name, "hello world")
|
ElixirAi.ChatRunner.new_user_message(conv_name, "hello world")
|
||||||
|
|
||||||
@@ -60,7 +63,7 @@ defmodule ElixirAi.MessageStorageTest do
|
|||||||
test "run_sql is called with assistant message params" do
|
test "run_sql is called with assistant message params" do
|
||||||
conv_name = setup_conversation()
|
conv_name = setup_conversation()
|
||||||
|
|
||||||
stub(ElixirAi.ChatUtils, :request_ai_response, fn server, _messages, _tools ->
|
stub(ElixirAi.ChatUtils, :request_ai_response, fn server, _messages, _tools, _provider ->
|
||||||
id = make_ref()
|
id = make_ref()
|
||||||
send(server, {:start_new_ai_response, id})
|
send(server, {:start_new_ai_response, id})
|
||||||
send(server, {:ai_text_chunk, id, "Hello from AI"})
|
send(server, {:ai_text_chunk, id, "Hello from AI"})
|
||||||
@@ -80,7 +83,7 @@ defmodule ElixirAi.MessageStorageTest do
|
|||||||
conv_name = setup_conversation()
|
conv_name = setup_conversation()
|
||||||
|
|
||||||
# First AI call triggers the tool; subsequent calls (after tool completes) are no-ops.
|
# First AI call triggers the tool; subsequent calls (after tool completes) are no-ops.
|
||||||
expect(ElixirAi.ChatUtils, :request_ai_response, fn server, _messages, _tools ->
|
expect(ElixirAi.ChatUtils, :request_ai_response, fn server, _messages, _tools, _provider ->
|
||||||
id = make_ref()
|
id = make_ref()
|
||||||
send(server, {:start_new_ai_response, id})
|
send(server, {:start_new_ai_response, id})
|
||||||
|
|
||||||
@@ -93,7 +96,9 @@ defmodule ElixirAi.MessageStorageTest do
|
|||||||
:ok
|
:ok
|
||||||
end)
|
end)
|
||||||
|
|
||||||
stub(ElixirAi.ChatUtils, :request_ai_response, fn _server, _messages, _tools -> :ok end)
|
stub(ElixirAi.ChatUtils, :request_ai_response, fn _server, _messages, _tools, _provider ->
|
||||||
|
:ok
|
||||||
|
end)
|
||||||
|
|
||||||
ElixirAi.ChatRunner.new_user_message(conv_name, "store something")
|
ElixirAi.ChatRunner.new_user_message(conv_name, "store something")
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,11 @@ defmodule ElixirAiWeb.ConnCase do
|
|||||||
setup _tags do
|
setup _tags do
|
||||||
stub(ElixirAi.Data.DbHelpers, :run_sql, fn _sql, _params, _topic -> [] end)
|
stub(ElixirAi.Data.DbHelpers, :run_sql, fn _sql, _params, _topic -> [] end)
|
||||||
stub(ElixirAi.Data.DbHelpers, :run_sql, fn _sql, _params, _topic, _schema -> [] end)
|
stub(ElixirAi.Data.DbHelpers, :run_sql, fn _sql, _params, _topic, _schema -> [] end)
|
||||||
stub(ElixirAi.ChatUtils, :request_ai_response, fn _server, _messages, _tools -> :ok end)
|
|
||||||
|
stub(ElixirAi.ChatUtils, :request_ai_response, fn _server, _messages, _tools, _provider ->
|
||||||
|
:ok
|
||||||
|
end)
|
||||||
|
|
||||||
{:ok, conn: Phoenix.ConnTest.build_conn()}
|
{:ok, conn: Phoenix.ConnTest.build_conn()}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ defmodule ElixirAi.TestCase do
|
|||||||
setup do
|
setup do
|
||||||
stub(ElixirAi.Data.DbHelpers, :run_sql, fn _sql, _params, _topic -> [] end)
|
stub(ElixirAi.Data.DbHelpers, :run_sql, fn _sql, _params, _topic -> [] end)
|
||||||
stub(ElixirAi.Data.DbHelpers, :run_sql, fn _sql, _params, _topic, _schema -> [] end)
|
stub(ElixirAi.Data.DbHelpers, :run_sql, fn _sql, _params, _topic, _schema -> [] end)
|
||||||
stub(ElixirAi.ChatUtils, :request_ai_response, fn _server, _messages, _tools -> :ok end)
|
|
||||||
|
stub(ElixirAi.ChatUtils, :request_ai_response, fn _server, _messages, _tools, _provider ->
|
||||||
|
:ok
|
||||||
|
end)
|
||||||
|
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user