This commit is contained in:
149
lib/elixir_ai/ai_tools.ex
Normal file
149
lib/elixir_ai/ai_tools.ex
Normal file
@@ -0,0 +1,149 @@
|
||||
defmodule ElixirAi.AiTools do
|
||||
@moduledoc """
|
||||
Central registry of all AI tools available to conversations.
|
||||
|
||||
Tools are split into two categories:
|
||||
|
||||
- **Server tools** (`store_thing`, `read_thing`): functions are fully defined
|
||||
here and always execute regardless of whether a browser session is open.
|
||||
|
||||
- **LiveView tools** (`set_background_color`, `navigate_to`): functions
|
||||
dispatch to the registered LiveView pid. If no browser tab is connected
|
||||
the call still succeeds immediately with a descriptive result so the AI
|
||||
conversation is never blocked.
|
||||
|
||||
Tool names are stored in the `conversations` table (`allowed_tools` column)
|
||||
and act as the gate for which tools are active for a given conversation.
|
||||
"""
|
||||
|
||||
import ElixirAi.ChatUtils, only: [ai_tool: 1]
|
||||
|
||||
@server_tool_names ["store_thing", "read_thing"]
|
||||
@liveview_tool_names ["set_background_color", "navigate_to"]
|
||||
@all_tool_names @server_tool_names ++ @liveview_tool_names
|
||||
|
||||
def server_tool_names, do: @server_tool_names
|
||||
|
||||
def liveview_tool_names, do: @liveview_tool_names
|
||||
|
||||
def all_tool_names, do: @all_tool_names
|
||||
|
||||
def build_server_tools(server, allowed_names) do
|
||||
[store_thing(server), read_thing(server)]
|
||||
|> Enum.filter(&(&1.name in allowed_names))
|
||||
end
|
||||
|
||||
def build_liveview_tools(server, allowed_names) do
|
||||
[set_background_color(server), navigate_to(server)]
|
||||
|> Enum.filter(&(&1.name in allowed_names))
|
||||
end
|
||||
|
||||
@doc "Convenience wrapper — builds all allowed tools (server + liveview)."
|
||||
def build(server, allowed_names) do
|
||||
build_server_tools(server, allowed_names) ++ build_liveview_tools(server, allowed_names)
|
||||
end
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Server tools
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def store_thing(server) do
|
||||
ai_tool(
|
||||
name: "store_thing",
|
||||
description: "store a key value pair in memory",
|
||||
function: &ElixirAi.ToolTesting.hold_thing/1,
|
||||
parameters: ElixirAi.ToolTesting.hold_thing_params(),
|
||||
server: server
|
||||
)
|
||||
end
|
||||
|
||||
def read_thing(server) do
|
||||
ai_tool(
|
||||
name: "read_thing",
|
||||
description: "read a key value pair that was previously stored with store_thing",
|
||||
function: &ElixirAi.ToolTesting.get_thing/1,
|
||||
parameters: ElixirAi.ToolTesting.get_thing_params(),
|
||||
server: server
|
||||
)
|
||||
end
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# LiveView tools
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def set_background_color(server) do
|
||||
ai_tool(
|
||||
name: "set_background_color",
|
||||
description:
|
||||
"set the background color of the chat interface, accepts specified tailwind colors",
|
||||
function: fn args -> dispatch_to_liveview(server, "set_background_color", args) end,
|
||||
parameters: %{
|
||||
"type" => "object",
|
||||
"properties" => %{
|
||||
"color" => %{
|
||||
"type" => "string",
|
||||
"enum" => [
|
||||
"bg-cyan-950/30",
|
||||
"bg-red-950/30",
|
||||
"bg-green-950/30",
|
||||
"bg-blue-950/30",
|
||||
"bg-yellow-950/30",
|
||||
"bg-purple-950/30",
|
||||
"bg-pink-950/30"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required" => ["color"]
|
||||
},
|
||||
server: server
|
||||
)
|
||||
end
|
||||
|
||||
def navigate_to(server) do
|
||||
ai_tool(
|
||||
name: "navigate_to",
|
||||
description: """
|
||||
Navigate the user's browser to a page in the application.
|
||||
Only use paths that exist in the app:
|
||||
"/" — home page
|
||||
"/admin" — admin panel
|
||||
"/chat/:name" — a chat conversation, where :name is the conversation name
|
||||
Provide the exact path string including the leading slash.
|
||||
""",
|
||||
function: fn args -> dispatch_to_liveview(server, "navigate_to", args) end,
|
||||
parameters: %{
|
||||
"type" => "object",
|
||||
"properties" => %{
|
||||
"path" => %{
|
||||
"type" => "string",
|
||||
"description" =>
|
||||
"The application path to navigate to, e.g. \"/\", \"/admin\", \"/chat/my-chat\""
|
||||
}
|
||||
},
|
||||
"required" => ["path"]
|
||||
},
|
||||
server: server
|
||||
)
|
||||
end
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Private
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
defp dispatch_to_liveview(server, tool_name, args) do
|
||||
case GenServer.call(server, :get_liveview_pid) do
|
||||
nil ->
|
||||
{:ok, "no browser session active, #{tool_name} skipped"}
|
||||
|
||||
liveview_pid ->
|
||||
send(liveview_pid, {:liveview_tool_call, tool_name, args, self()})
|
||||
|
||||
receive do
|
||||
{:liveview_tool_result, result} -> result
|
||||
after
|
||||
5_000 -> {:ok, "browser session timed out, #{tool_name} skipped"}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -47,7 +47,7 @@ defmodule ElixirAi.ChatUtils do
|
||||
}
|
||||
end
|
||||
|
||||
def request_ai_response(server, messages, tools, provider) do
|
||||
def request_ai_response(server, messages, tools, provider, tool_choice \\ "auto") do
|
||||
Task.start_link(fn ->
|
||||
api_url = provider.completions_url
|
||||
api_key = provider.api_token
|
||||
@@ -69,7 +69,8 @@ defmodule ElixirAi.ChatUtils do
|
||||
model: model,
|
||||
stream: true,
|
||||
messages: messages |> Enum.map(&api_message/1),
|
||||
tools: Enum.map(tools, & &1.definition)
|
||||
tools: Enum.map(tools, & &1.definition),
|
||||
tool_choice: tool_choice
|
||||
}
|
||||
|
||||
headers = [{"authorization", "Bearer #{api_key}"}]
|
||||
|
||||
@@ -1,14 +1,30 @@
|
||||
defmodule ElixirAi.ChatRunner do
|
||||
require Logger
|
||||
use GenServer
|
||||
import ElixirAi.ChatUtils, only: [ai_tool: 1]
|
||||
alias ElixirAi.{Conversation, Message}
|
||||
alias ElixirAi.{AiTools, Conversation, Message}
|
||||
import ElixirAi.PubsubTopics
|
||||
|
||||
defp via(name), do: {:via, Horde.Registry, {ElixirAi.ChatRegistry, name}}
|
||||
|
||||
def new_user_message(name, text_content) do
|
||||
GenServer.cast(via(name), {:user_message, text_content})
|
||||
def new_user_message(name, text_content, opts \\ []) do
|
||||
tool_choice = Keyword.get(opts, :tool_choice)
|
||||
GenServer.cast(via(name), {:user_message, text_content, tool_choice})
|
||||
end
|
||||
|
||||
def set_allowed_tools(name, tool_names) when is_list(tool_names) do
|
||||
GenServer.call(via(name), {:set_allowed_tools, tool_names})
|
||||
end
|
||||
|
||||
def set_tool_choice(name, tool_choice) when tool_choice in ["auto", "none", "required"] do
|
||||
GenServer.call(via(name), {:set_tool_choice, tool_choice})
|
||||
end
|
||||
|
||||
def register_liveview_pid(name, liveview_pid) when is_pid(liveview_pid) do
|
||||
GenServer.call(via(name), {:register_liveview_pid, liveview_pid})
|
||||
end
|
||||
|
||||
def deregister_liveview_pid(name) do
|
||||
GenServer.call(via(name), :deregister_liveview_pid)
|
||||
end
|
||||
|
||||
@spec get_conversation(String.t()) :: any()
|
||||
@@ -44,13 +60,35 @@ defmodule ElixirAi.ChatRunner do
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
allowed_tools =
|
||||
case Conversation.find_allowed_tools(name) do
|
||||
{:ok, tools} -> tools
|
||||
_ -> AiTools.all_tool_names()
|
||||
end
|
||||
|
||||
tool_choice =
|
||||
case Conversation.find_tool_choice(name) do
|
||||
{:ok, tc} -> tc
|
||||
_ -> "auto"
|
||||
end
|
||||
|
||||
server_tools = AiTools.build_server_tools(self(), allowed_tools)
|
||||
liveview_tools = AiTools.build_liveview_tools(self(), allowed_tools)
|
||||
|
||||
if last_message && last_message.role == :user do
|
||||
Logger.info(
|
||||
"Last message role was #{last_message.role}, requesting AI response for conversation #{name}"
|
||||
)
|
||||
|
||||
broadcast_ui(name, :recovery_restart)
|
||||
ElixirAi.ChatUtils.request_ai_response(self(), messages, tools(self(), name), provider)
|
||||
|
||||
ElixirAi.ChatUtils.request_ai_response(
|
||||
self(),
|
||||
messages,
|
||||
server_tools ++ liveview_tools,
|
||||
provider,
|
||||
tool_choice
|
||||
)
|
||||
end
|
||||
|
||||
{:ok,
|
||||
@@ -59,63 +97,19 @@ defmodule ElixirAi.ChatRunner do
|
||||
messages: messages,
|
||||
streaming_response: nil,
|
||||
pending_tool_calls: [],
|
||||
tools: tools(self(), name),
|
||||
provider: provider
|
||||
allowed_tools: allowed_tools,
|
||||
tool_choice: tool_choice,
|
||||
server_tools: server_tools,
|
||||
liveview_tools: liveview_tools,
|
||||
provider: provider,
|
||||
liveview_pid: nil,
|
||||
liveview_monitor_ref: nil
|
||||
}}
|
||||
end
|
||||
|
||||
def tools(server, name) do
|
||||
[
|
||||
ai_tool(
|
||||
name: "store_thing",
|
||||
description: "store a key value pair in memory",
|
||||
function: &ElixirAi.ToolTesting.hold_thing/1,
|
||||
parameters: ElixirAi.ToolTesting.hold_thing_params(),
|
||||
server: server
|
||||
),
|
||||
ai_tool(
|
||||
name: "read_thing",
|
||||
description: "read a key value pair that was previously stored with store_thing",
|
||||
function: &ElixirAi.ToolTesting.get_thing/1,
|
||||
parameters: ElixirAi.ToolTesting.get_thing_params(),
|
||||
server: server
|
||||
),
|
||||
ai_tool(
|
||||
name: "set_background_color",
|
||||
description:
|
||||
"set the background color of the chat interface, accepts specified tailwind colors",
|
||||
function: fn %{"color" => color} ->
|
||||
Phoenix.PubSub.broadcast(
|
||||
ElixirAi.PubSub,
|
||||
chat_topic(name),
|
||||
{:set_background_color, color}
|
||||
)
|
||||
end,
|
||||
parameters: %{
|
||||
"type" => "object",
|
||||
"properties" => %{
|
||||
"color" => %{
|
||||
"type" => "string",
|
||||
"enum" => [
|
||||
"bg-cyan-950/30",
|
||||
"bg-red-950/30",
|
||||
"bg-green-950/30",
|
||||
"bg-blue-950/30",
|
||||
"bg-yellow-950/30",
|
||||
"bg-purple-950/30",
|
||||
"bg-pink-950/30"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required" => ["color"]
|
||||
},
|
||||
server: server
|
||||
)
|
||||
]
|
||||
end
|
||||
|
||||
def handle_cast({:user_message, text_content}, state) do
|
||||
new_message = %{role: :user, content: text_content}
|
||||
def handle_cast({:user_message, text_content, tool_choice_override}, state) do
|
||||
effective_tool_choice = tool_choice_override || state.tool_choice
|
||||
new_message = %{role: :user, content: text_content, tool_choice: tool_choice_override}
|
||||
broadcast_ui(state.name, {:user_chat_message, new_message})
|
||||
store_message(state.name, new_message)
|
||||
new_state = %{state | messages: state.messages ++ [new_message]}
|
||||
@@ -123,8 +117,9 @@ defmodule ElixirAi.ChatRunner do
|
||||
ElixirAi.ChatUtils.request_ai_response(
|
||||
self(),
|
||||
new_state.messages,
|
||||
state.tools,
|
||||
state.provider
|
||||
state.server_tools ++ state.liveview_tools,
|
||||
state.provider,
|
||||
effective_tool_choice
|
||||
)
|
||||
|
||||
{:noreply, new_state}
|
||||
@@ -269,7 +264,9 @@ defmodule ElixirAi.ChatRunner do
|
||||
{failed, pending} ->
|
||||
with {:ok, decoded_args} <- Jason.decode(tool_call.arguments),
|
||||
tool when not is_nil(tool) <-
|
||||
Enum.find(state.tools, fn t -> t.name == tool_call.name end) do
|
||||
Enum.find(state.server_tools ++ state.liveview_tools, fn t ->
|
||||
t.name == tool_call.name
|
||||
end) do
|
||||
tool.run_function.(id, tool_call.id, decoded_args)
|
||||
{failed, [tool_call.id | pending]}
|
||||
else
|
||||
@@ -319,8 +316,9 @@ defmodule ElixirAi.ChatRunner do
|
||||
ElixirAi.ChatUtils.request_ai_response(
|
||||
self(),
|
||||
state.messages ++ [new_message],
|
||||
state.tools,
|
||||
state.provider
|
||||
state.server_tools ++ state.liveview_tools,
|
||||
state.provider,
|
||||
state.tool_choice
|
||||
)
|
||||
end
|
||||
|
||||
@@ -362,6 +360,46 @@ defmodule ElixirAi.ChatRunner do
|
||||
{:reply, state.streaming_response, state}
|
||||
end
|
||||
|
||||
def handle_call(:get_liveview_pid, _from, state) do
|
||||
{:reply, state.liveview_pid, state}
|
||||
end
|
||||
|
||||
def handle_call({:register_liveview_pid, liveview_pid}, _from, state) do
|
||||
# Clear any previous monitor
|
||||
if state.liveview_monitor_ref, do: Process.demonitor(state.liveview_monitor_ref, [:flush])
|
||||
ref = Process.monitor(liveview_pid)
|
||||
{:reply, :ok, %{state | liveview_pid: liveview_pid, liveview_monitor_ref: ref}}
|
||||
end
|
||||
|
||||
def handle_call(:deregister_liveview_pid, _from, state) do
|
||||
if state.liveview_monitor_ref, do: Process.demonitor(state.liveview_monitor_ref, [:flush])
|
||||
{:reply, :ok, %{state | liveview_pid: nil, liveview_monitor_ref: nil}}
|
||||
end
|
||||
|
||||
def handle_call({:set_tool_choice, tool_choice}, _from, state) do
|
||||
Conversation.update_tool_choice(state.name, tool_choice)
|
||||
{:reply, :ok, %{state | tool_choice: tool_choice}}
|
||||
end
|
||||
|
||||
def handle_call({:set_allowed_tools, tool_names}, _from, state) do
|
||||
Conversation.update_allowed_tools(state.name, tool_names)
|
||||
server_tools = AiTools.build_server_tools(self(), tool_names)
|
||||
liveview_tools = AiTools.build_liveview_tools(self(), tool_names)
|
||||
|
||||
{:reply, :ok,
|
||||
%{
|
||||
state
|
||||
| allowed_tools: tool_names,
|
||||
server_tools: server_tools,
|
||||
liveview_tools: liveview_tools
|
||||
}}
|
||||
end
|
||||
|
||||
def handle_info({:DOWN, ref, :process, _pid, _reason}, %{liveview_monitor_ref: ref} = state) do
|
||||
Logger.info("ChatRunner #{state.name}: LiveView disconnected, clearing liveview_pid")
|
||||
{:noreply, %{state | liveview_pid: nil, liveview_monitor_ref: nil}}
|
||||
end
|
||||
|
||||
defp broadcast_ui(name, msg),
|
||||
do: Phoenix.PubSub.broadcast(ElixirAi.PubSub, chat_topic(name), msg)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
defmodule ElixirAi.ConversationManager do
|
||||
use GenServer
|
||||
alias ElixirAi.{Conversation, Message}
|
||||
alias ElixirAi.{Conversation, Message, AiTools}
|
||||
import ElixirAi.PubsubTopics, only: [conversation_message_topic: 1]
|
||||
require Logger
|
||||
|
||||
@@ -24,8 +24,9 @@ defmodule ElixirAi.ConversationManager do
|
||||
{:ok, %{conversations: :loading, subscriptions: MapSet.new(), runners: %{}}}
|
||||
end
|
||||
|
||||
def create_conversation(name, ai_provider_id) do
|
||||
GenServer.call(@name, {:create, name, ai_provider_id})
|
||||
def create_conversation(name, ai_provider_id, category \\ "user-web", allowed_tools \\ nil) do
|
||||
tools = allowed_tools || AiTools.all_tool_names()
|
||||
GenServer.call(@name, {:create, name, ai_provider_id, category, tools})
|
||||
end
|
||||
|
||||
def open_conversation(name) do
|
||||
@@ -54,14 +55,14 @@ defmodule ElixirAi.ConversationManager do
|
||||
end
|
||||
|
||||
def handle_call(
|
||||
{:create, name, ai_provider_id},
|
||||
{:create, name, ai_provider_id, category, allowed_tools},
|
||||
_from,
|
||||
%{conversations: conversations} = state
|
||||
) do
|
||||
if Map.has_key?(conversations, name) do
|
||||
{:reply, {:error, :already_exists}, state}
|
||||
else
|
||||
case Conversation.create(name, ai_provider_id) do
|
||||
case Conversation.create(name, ai_provider_id, category, allowed_tools) do
|
||||
:ok ->
|
||||
reply_with_started(name, state, fn new_state ->
|
||||
%{new_state | conversations: Map.put(new_state.conversations, name, [])}
|
||||
@@ -152,7 +153,7 @@ defmodule ElixirAi.ConversationManager do
|
||||
end
|
||||
|
||||
# Returns {pid} to callers that only need to know the process started (e.g. create).
|
||||
defp reply_with_started(name, state, update_state \\ fn s -> s end) do
|
||||
defp reply_with_started(name, state, update_state) do
|
||||
case start_and_subscribe(name, state) do
|
||||
{:ok, pid, new_subscriptions, new_runners} ->
|
||||
new_state =
|
||||
@@ -160,8 +161,12 @@ defmodule ElixirAi.ConversationManager do
|
||||
|
||||
{:reply, {:ok, pid}, new_state}
|
||||
|
||||
{:error, _reason} = error ->
|
||||
{:reply, error, state}
|
||||
{:error, reason} ->
|
||||
Logger.error(
|
||||
"ConversationManager: failed to start runner for #{name}: #{inspect(reason)}"
|
||||
)
|
||||
|
||||
{:reply, {:error, :failed_to_load}, state}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ defmodule ElixirAi.AiProvider do
|
||||
id: Zoi.optional(Zoi.string()),
|
||||
name: Zoi.string(),
|
||||
model_name: Zoi.string(),
|
||||
api_token: Zoi.string(),
|
||||
completions_url: Zoi.string()
|
||||
api_token: Zoi.nullish(Zoi.string()),
|
||||
completions_url: Zoi.nullish(Zoi.string())
|
||||
})
|
||||
end
|
||||
|
||||
|
||||
@@ -9,67 +9,69 @@ defmodule ElixirAi.Conversation do
|
||||
Zoi.object(%{
|
||||
name: Zoi.string(),
|
||||
model_name: Zoi.string(),
|
||||
api_token: Zoi.string(),
|
||||
completions_url: Zoi.string()
|
||||
api_token: Zoi.nullish(Zoi.string()),
|
||||
completions_url: Zoi.nullish(Zoi.string())
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
defmodule ConversationInfo do
|
||||
defstruct [:name, :provider]
|
||||
defstruct [:name, :category, :provider]
|
||||
|
||||
def schema do
|
||||
Zoi.object(%{
|
||||
name: Zoi.string(),
|
||||
category: Zoi.string(),
|
||||
provider:
|
||||
Zoi.object(%{
|
||||
name: Zoi.string(),
|
||||
model_name: Zoi.string(),
|
||||
api_token: Zoi.string(),
|
||||
completions_url: Zoi.string()
|
||||
api_token: Zoi.nullish(Zoi.string()),
|
||||
completions_url: Zoi.nullish(Zoi.string())
|
||||
})
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
def all_names do
|
||||
sql = """
|
||||
SELECT c.name,
|
||||
json_build_object(
|
||||
'name', p.name,
|
||||
'model_name', p.model_name,
|
||||
'api_token', p.api_token,
|
||||
'completions_url', p.completions_url
|
||||
) as provider
|
||||
FROM conversations c
|
||||
LEFT JOIN ai_providers p ON c.ai_provider_id = p.id
|
||||
"""
|
||||
|
||||
sql = "SELECT name, category FROM conversations"
|
||||
params = %{}
|
||||
|
||||
case DbHelpers.run_sql(sql, params, "conversations", ConversationInfo.schema()) do
|
||||
schema = Zoi.object(%{name: Zoi.string(), category: Zoi.string()})
|
||||
|
||||
case DbHelpers.run_sql(sql, params, "conversations", schema) do
|
||||
{:error, _} ->
|
||||
[]
|
||||
|
||||
rows ->
|
||||
Enum.map(rows, fn row ->
|
||||
struct(ConversationInfo, Map.put(row, :provider, struct(Provider, row.provider)))
|
||||
struct(ConversationInfo, row)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
def create(name, ai_provider_id) when is_binary(ai_provider_id) do
|
||||
def create(name, ai_provider_id, category \\ "user-web", allowed_tools \\ nil)
|
||||
|
||||
def create(name, ai_provider_id, category, nil),
|
||||
do: create(name, ai_provider_id, category, ElixirAi.AiTools.all_tool_names())
|
||||
|
||||
def create(name, ai_provider_id, category, allowed_tools)
|
||||
when is_binary(ai_provider_id) and is_binary(category) and is_list(allowed_tools) do
|
||||
case Ecto.UUID.dump(ai_provider_id) do
|
||||
{:ok, binary_id} ->
|
||||
sql = """
|
||||
INSERT INTO conversations (
|
||||
name,
|
||||
ai_provider_id,
|
||||
category,
|
||||
allowed_tools,
|
||||
inserted_at,
|
||||
updated_at)
|
||||
VALUES (
|
||||
$(name),
|
||||
$(ai_provider_id),
|
||||
$(category),
|
||||
$(allowed_tools)::jsonb,
|
||||
$(inserted_at),
|
||||
$(updated_at)
|
||||
)
|
||||
@@ -80,6 +82,8 @@ defmodule ElixirAi.Conversation do
|
||||
params = %{
|
||||
"name" => name,
|
||||
"ai_provider_id" => binary_id,
|
||||
"category" => category,
|
||||
"allowed_tools" => Jason.encode!(allowed_tools),
|
||||
"inserted_at" => timestamp,
|
||||
"updated_at" => timestamp
|
||||
}
|
||||
@@ -97,6 +101,78 @@ defmodule ElixirAi.Conversation do
|
||||
end
|
||||
end
|
||||
|
||||
def find_allowed_tools(name) do
|
||||
sql = "SELECT allowed_tools FROM conversations WHERE name = $(name) LIMIT 1"
|
||||
params = %{"name" => name}
|
||||
|
||||
case DbHelpers.run_sql(sql, params, "conversations") do
|
||||
{:error, :db_error} -> {:error, :db_error}
|
||||
[] -> {:error, :not_found}
|
||||
[row | _] -> {:ok, decode_json_list(row["allowed_tools"])}
|
||||
end
|
||||
end
|
||||
|
||||
defp decode_json_list(value) when is_list(value), do: value
|
||||
|
||||
defp decode_json_list(value) when is_binary(value) do
|
||||
case Jason.decode(value) do
|
||||
{:ok, list} when is_list(list) -> list
|
||||
_ -> []
|
||||
end
|
||||
end
|
||||
|
||||
defp decode_json_list(_), do: []
|
||||
|
||||
def update_allowed_tools(name, tool_names) when is_list(tool_names) do
|
||||
sql = """
|
||||
UPDATE conversations
|
||||
SET allowed_tools = $(allowed_tools)::jsonb, updated_at = $(updated_at)
|
||||
WHERE name = $(name)
|
||||
"""
|
||||
|
||||
params = %{
|
||||
"name" => name,
|
||||
"allowed_tools" => Jason.encode!(tool_names),
|
||||
"updated_at" => now()
|
||||
}
|
||||
|
||||
case DbHelpers.run_sql(sql, params, "conversations") do
|
||||
{:error, :db_error} -> {:error, :db_error}
|
||||
_ -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
def find_tool_choice(name) do
|
||||
sql = "SELECT tool_choice FROM conversations WHERE name = $(name) LIMIT 1"
|
||||
params = %{"name" => name}
|
||||
|
||||
case DbHelpers.run_sql(sql, params, "conversations") do
|
||||
{:error, :db_error} -> {:error, :db_error}
|
||||
[] -> {:error, :not_found}
|
||||
[row | _] -> {:ok, row["tool_choice"] || "auto"}
|
||||
end
|
||||
end
|
||||
|
||||
def update_tool_choice(name, tool_choice)
|
||||
when tool_choice in ["auto", "none", "required"] do
|
||||
sql = """
|
||||
UPDATE conversations
|
||||
SET tool_choice = $(tool_choice), updated_at = $(updated_at)
|
||||
WHERE name = $(name)
|
||||
"""
|
||||
|
||||
params = %{
|
||||
"name" => name,
|
||||
"tool_choice" => tool_choice,
|
||||
"updated_at" => now()
|
||||
}
|
||||
|
||||
case DbHelpers.run_sql(sql, params, "conversations") do
|
||||
{:error, :db_error} -> {:error, :db_error}
|
||||
_ -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
def find_id(name) do
|
||||
sql = "SELECT id FROM conversations WHERE name = $(name) LIMIT 1"
|
||||
params = %{"name" => name}
|
||||
|
||||
@@ -25,6 +25,7 @@ defmodule ElixirAi.Message do
|
||||
role: Zoi.string(),
|
||||
content: Zoi.nullish(Zoi.string()),
|
||||
reasoning_content: Zoi.nullish(Zoi.string()),
|
||||
tool_choice: Zoi.nullish(Zoi.string()),
|
||||
inserted_at: Zoi.any()
|
||||
})
|
||||
end
|
||||
@@ -125,6 +126,7 @@ defmodule ElixirAi.Message do
|
||||
tm.role,
|
||||
tm.content,
|
||||
tm.reasoning_content,
|
||||
tm.tool_choice,
|
||||
tm.inserted_at
|
||||
FROM text_messages tm
|
||||
WHERE tm.conversation_id = $(conversation_id)
|
||||
@@ -222,6 +224,7 @@ defmodule ElixirAi.Message do
|
||||
prev_message_table,
|
||||
role,
|
||||
content,
|
||||
tool_choice,
|
||||
inserted_at
|
||||
) VALUES (
|
||||
$(conversation_id),
|
||||
@@ -229,6 +232,7 @@ defmodule ElixirAi.Message do
|
||||
$(prev_message_table),
|
||||
$(role),
|
||||
$(content),
|
||||
$(tool_choice),
|
||||
$(inserted_at)
|
||||
)
|
||||
"""
|
||||
@@ -239,6 +243,7 @@ defmodule ElixirAi.Message do
|
||||
"prev_message_table" => prev_table,
|
||||
"role" => "user",
|
||||
"content" => message[:content],
|
||||
"tool_choice" => message[:tool_choice],
|
||||
"inserted_at" => timestamp
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user