persisting in postgres
This commit is contained in:
@@ -72,7 +72,7 @@ Hooks.MarkdownStream = {
|
||||
|
||||
Hooks.ScrollBottom = {
|
||||
mounted() {
|
||||
this.scrollToBottom()
|
||||
requestAnimationFrame(() => this.scrollToBottom())
|
||||
this.observer = new MutationObserver(() => {
|
||||
if (this.isNearBottom()) this.scrollToBottom()
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import Config
|
||||
|
||||
config :elixir_ai,
|
||||
ecto_repos: [ElixirAi.Repo],
|
||||
generators: [timestamp_type: :utc_datetime]
|
||||
|
||||
# Configures the endpoint
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import Config
|
||||
|
||||
config :elixir_ai, ElixirAi.Repo,
|
||||
username: "elixir_ai",
|
||||
password: "elixir_ai",
|
||||
hostname: "localhost",
|
||||
database: "elixir_ai_dev",
|
||||
stacktrace: true,
|
||||
show_sensitive_data_on_connection_error: true,
|
||||
pool_size: 10
|
||||
|
||||
# For development, we disable any cache and enable
|
||||
# debugging and code reloading.
|
||||
#
|
||||
|
||||
11
docker-compose.yml
Normal file
11
docker-compose.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
services:
|
||||
db:
|
||||
image: postgres:17
|
||||
environment:
|
||||
POSTGRES_USER: elixir_ai
|
||||
POSTGRES_PASSWORD: elixir_ai
|
||||
POSTGRES_DB: elixir_ai_dev
|
||||
ports:
|
||||
- 5432:5432
|
||||
volumes:
|
||||
- ./schema.sql:/docker-entrypoint-initdb.d/schema.sql
|
||||
@@ -6,6 +6,7 @@ defmodule ElixirAi.Application do
|
||||
def start(_type, _args) do
|
||||
children = [
|
||||
ElixirAiWeb.Telemetry,
|
||||
ElixirAi.Repo,
|
||||
{DNSCluster, query: Application.get_env(:elixir_ai, :dns_cluster_query) || :ignore},
|
||||
{Phoenix.PubSub, name: ElixirAi.PubSub},
|
||||
ElixirAi.ToolTesting,
|
||||
|
||||
@@ -2,9 +2,11 @@ defmodule ElixirAi.ChatRunner do
|
||||
require Logger
|
||||
use GenServer
|
||||
import ElixirAi.ChatUtils
|
||||
alias ElixirAi.{Conversation, Message}
|
||||
|
||||
defp via(name), do: {:via, Registry, {ElixirAi.ChatRegistry, name}}
|
||||
defp topic(name), do: "ai_chat:#{name}"
|
||||
defp message_topic(name), do: "conversation_messages:#{name}"
|
||||
|
||||
def new_user_message(name, text_content) do
|
||||
GenServer.cast(via(name), {:user_message, text_content})
|
||||
@@ -24,13 +26,20 @@ defmodule ElixirAi.ChatRunner do
|
||||
end
|
||||
|
||||
def init(name) do
|
||||
{:ok, %{
|
||||
name: name,
|
||||
messages: [],
|
||||
streaming_response: nil,
|
||||
pending_tool_calls: [],
|
||||
tools: tools(self(), name)
|
||||
}}
|
||||
messages =
|
||||
case Conversation.find_id(name) do
|
||||
{:ok, conv_id} -> Message.load_for_conversation(conv_id)
|
||||
_ -> []
|
||||
end
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
name: name,
|
||||
messages: messages,
|
||||
streaming_response: nil,
|
||||
pending_tool_calls: [],
|
||||
tools: tools(self(), name)
|
||||
}}
|
||||
end
|
||||
|
||||
def tools(server, name) do
|
||||
@@ -54,7 +63,11 @@ defmodule ElixirAi.ChatRunner do
|
||||
description:
|
||||
"set the background color of the chat interface, accepts specified tailwind colors",
|
||||
function: fn %{"color" => color} ->
|
||||
Phoenix.PubSub.broadcast(ElixirAi.PubSub, "ai_chat:#{name}", {:set_background_color, color})
|
||||
Phoenix.PubSub.broadcast(
|
||||
ElixirAi.PubSub,
|
||||
"ai_chat:#{name}",
|
||||
{:set_background_color, color}
|
||||
)
|
||||
end,
|
||||
parameters: %{
|
||||
"type" => "object",
|
||||
@@ -73,7 +86,8 @@ defmodule ElixirAi.ChatRunner do
|
||||
|
||||
def handle_cast({:user_message, text_content}, state) do
|
||||
new_message = %{role: :user, content: text_content}
|
||||
broadcast(state.name, {:user_chat_message, new_message})
|
||||
broadcast_ui(state.name, {:user_chat_message, new_message})
|
||||
store_message(state.name, new_message)
|
||||
new_state = %{state | messages: state.messages ++ [new_message]}
|
||||
|
||||
request_ai_response(self(), new_state.messages, state.tools)
|
||||
@@ -82,7 +96,7 @@ defmodule ElixirAi.ChatRunner do
|
||||
|
||||
def handle_info({:start_new_ai_response, id}, state) do
|
||||
starting_response = %{id: id, reasoning_content: "", content: "", tool_calls: []}
|
||||
broadcast(state.name, {:start_ai_response_stream, starting_response})
|
||||
broadcast_ui(state.name, {:start_ai_response_stream, starting_response})
|
||||
|
||||
{:noreply, %{state | streaming_response: starting_response}}
|
||||
end
|
||||
@@ -100,7 +114,7 @@ defmodule ElixirAi.ChatRunner do
|
||||
end
|
||||
|
||||
def handle_info({:ai_reasoning_chunk, _id, reasoning_content}, state) do
|
||||
broadcast(state.name, {:reasoning_chunk_content, reasoning_content})
|
||||
broadcast_ui(state.name, {:reasoning_chunk_content, reasoning_content})
|
||||
|
||||
{:noreply,
|
||||
%{
|
||||
@@ -113,7 +127,7 @@ defmodule ElixirAi.ChatRunner do
|
||||
end
|
||||
|
||||
def handle_info({:ai_text_chunk, _id, text_content}, state) do
|
||||
broadcast(state.name, {:text_chunk_content, text_content})
|
||||
broadcast_ui(state.name, {:text_chunk_content, text_content})
|
||||
|
||||
{:noreply,
|
||||
%{
|
||||
@@ -137,7 +151,8 @@ defmodule ElixirAi.ChatRunner do
|
||||
tool_calls: state.streaming_response.tool_calls
|
||||
}
|
||||
|
||||
broadcast(state.name, {:end_ai_response, final_message})
|
||||
broadcast_ui(state.name, {:end_ai_response, final_message})
|
||||
store_message(state.name, final_message)
|
||||
|
||||
{:noreply,
|
||||
%{
|
||||
@@ -202,13 +217,14 @@ defmodule ElixirAi.ChatRunner do
|
||||
tool_calls: state.streaming_response.tool_calls
|
||||
}
|
||||
|
||||
broadcast(state.name, {:tool_request_message, tool_request_message})
|
||||
|
||||
broadcast_ui(state.name, {:tool_request_message, tool_request_message})
|
||||
|
||||
{failed_call_messages, pending_call_ids} =
|
||||
Enum.reduce(state.streaming_response.tool_calls, {[], []}, fn tool_call, {failed, pending} ->
|
||||
Enum.reduce(state.streaming_response.tool_calls, {[], []}, fn tool_call,
|
||||
{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
|
||||
tool when not is_nil(tool) <-
|
||||
Enum.find(state.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
|
||||
@@ -224,6 +240,8 @@ defmodule ElixirAi.ChatRunner do
|
||||
end
|
||||
end)
|
||||
|
||||
store_message(state.name, [tool_request_message] ++ failed_call_messages)
|
||||
|
||||
{:noreply,
|
||||
%{
|
||||
state
|
||||
@@ -235,7 +253,8 @@ defmodule ElixirAi.ChatRunner do
|
||||
def handle_info({:tool_response, _id, tool_call_id, result}, state) do
|
||||
new_message = %{role: :tool, content: inspect(result), tool_call_id: tool_call_id}
|
||||
|
||||
broadcast(state.name, {:one_tool_finished, new_message})
|
||||
broadcast_ui(state.name, {:one_tool_finished, new_message})
|
||||
store_message(state.name, new_message)
|
||||
|
||||
new_pending_tool_calls =
|
||||
Enum.filter(state.pending_tool_calls, fn id -> id != tool_call_id end)
|
||||
@@ -250,7 +269,7 @@ defmodule ElixirAi.ChatRunner do
|
||||
end
|
||||
|
||||
if new_pending_tool_calls == [] do
|
||||
broadcast(state.name, :tool_calls_finished)
|
||||
broadcast_ui(state.name, :tool_calls_finished)
|
||||
request_ai_response(self(), state.messages ++ [new_message], state.tools)
|
||||
end
|
||||
|
||||
@@ -271,5 +290,20 @@ defmodule ElixirAi.ChatRunner do
|
||||
{:reply, state.streaming_response, state}
|
||||
end
|
||||
|
||||
defp broadcast(name, msg), do: Phoenix.PubSub.broadcast(ElixirAi.PubSub, topic(name), msg)
|
||||
defp broadcast_ui(name, msg), do: Phoenix.PubSub.broadcast(ElixirAi.PubSub, topic(name), msg)
|
||||
|
||||
defp store_message(name, messages) when is_list(messages) do
|
||||
Enum.each(messages, &store_message(name, &1))
|
||||
messages
|
||||
end
|
||||
|
||||
defp store_message(name, message) do
|
||||
Phoenix.PubSub.broadcast(
|
||||
ElixirAi.PubSub,
|
||||
message_topic(name),
|
||||
{:store_message, name, message}
|
||||
)
|
||||
|
||||
message
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
defmodule ElixirAi.ConversationManager do
|
||||
use GenServer
|
||||
alias ElixirAi.{Conversation, Message}
|
||||
|
||||
def start_link(_opts), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
|
||||
def init(names), do: {:ok, names}
|
||||
def start_link(_opts), do: GenServer.start_link(__MODULE__, nil, name: __MODULE__)
|
||||
|
||||
def init(_) do
|
||||
names = Conversation.all_names()
|
||||
conversations = Map.new(names, fn name -> {name, []} end)
|
||||
{:ok, conversations}
|
||||
end
|
||||
|
||||
def create_conversation(name) do
|
||||
GenServer.call(__MODULE__, {:create, name})
|
||||
@@ -16,34 +22,76 @@ defmodule ElixirAi.ConversationManager do
|
||||
GenServer.call(__MODULE__, :list)
|
||||
end
|
||||
|
||||
def handle_call({:create, name}, _from, names) do
|
||||
if name in names do
|
||||
{:reply, {:error, :already_exists}, names}
|
||||
def get_messages(name) do
|
||||
GenServer.call(__MODULE__, {:get_messages, name})
|
||||
end
|
||||
|
||||
def handle_call({:create, name}, _from, conversations) do
|
||||
if Map.has_key?(conversations, name) do
|
||||
{:reply, {:error, :already_exists}, conversations}
|
||||
else
|
||||
{:reply, start_runner(name), [name | names]}
|
||||
case Conversation.create(name) do
|
||||
:ok ->
|
||||
case start_and_subscribe(name) do
|
||||
{:ok, _pid} = ok -> {:reply, ok, Map.put(conversations, name, [])}
|
||||
error -> {:reply, error, conversations}
|
||||
end
|
||||
|
||||
{:error, _} = error ->
|
||||
{:reply, error, conversations}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def handle_call({:open, name}, _from, names) do
|
||||
if name in names do
|
||||
{:reply, start_runner(name), names}
|
||||
def handle_call({:open, name}, _from, conversations) do
|
||||
if Map.has_key?(conversations, name) do
|
||||
case start_and_subscribe(name) do
|
||||
{:ok, _pid} = ok -> {:reply, ok, conversations}
|
||||
error -> {:reply, error, conversations}
|
||||
end
|
||||
else
|
||||
{:reply, {:error, :not_found}, names}
|
||||
end
|
||||
end
|
||||
def handle_call(:list, _from, names) do
|
||||
{:reply, names, names}
|
||||
end
|
||||
|
||||
defp start_runner(name) do
|
||||
case DynamicSupervisor.start_child(
|
||||
ElixirAi.ChatRunnerSupervisor,
|
||||
{ElixirAi.ChatRunner, name: name}
|
||||
) do
|
||||
{:ok, pid} -> {:ok, pid}
|
||||
{:error, {:already_started, pid}} -> {:ok, pid}
|
||||
error -> error
|
||||
{:reply, {:error, :not_found}, conversations}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_call(:list, _from, conversations) do
|
||||
{:reply, Map.keys(conversations), conversations}
|
||||
end
|
||||
|
||||
def handle_call({:get_messages, name}, _from, conversations) do
|
||||
{:reply, Map.get(conversations, name, []), conversations}
|
||||
end
|
||||
|
||||
def handle_info({:store_message, name, message}, conversations) do
|
||||
messages = Map.get(conversations, name, [])
|
||||
position = length(messages)
|
||||
|
||||
case Conversation.find_id(name) do
|
||||
{:ok, conv_id} -> Message.insert(conv_id, message, position)
|
||||
_ -> :ok
|
||||
end
|
||||
|
||||
{:noreply, Map.update(conversations, name, [message], &(&1 ++ [message]))}
|
||||
end
|
||||
|
||||
defp start_and_subscribe(name) do
|
||||
result =
|
||||
case DynamicSupervisor.start_child(
|
||||
ElixirAi.ChatRunnerSupervisor,
|
||||
{ElixirAi.ChatRunner, name: name}
|
||||
) do
|
||||
{:ok, pid} -> {:ok, pid}
|
||||
{:error, {:already_started, pid}} -> {:ok, pid}
|
||||
error -> error
|
||||
end
|
||||
|
||||
case result do
|
||||
{:ok, _pid} ->
|
||||
Phoenix.PubSub.subscribe(ElixirAi.PubSub, "conversation_messages:#{name}")
|
||||
result
|
||||
|
||||
_ ->
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
26
lib/elixir_ai/data/conversation.ex
Normal file
26
lib/elixir_ai/data/conversation.ex
Normal file
@@ -0,0 +1,26 @@
|
||||
defmodule ElixirAi.Conversation do
|
||||
import Ecto.Query
|
||||
alias ElixirAi.Repo
|
||||
|
||||
def all_names do
|
||||
Repo.all(from c in "conversations", select: c.name)
|
||||
end
|
||||
|
||||
def create(name) do
|
||||
case Repo.insert_all("conversations", [[id: Ecto.UUID.generate(), name: name, inserted_at: now(), updated_at: now()]]) do
|
||||
{1, _} -> :ok
|
||||
_ -> {:error, :db_error}
|
||||
end
|
||||
rescue
|
||||
e in Ecto.ConstraintError -> if e.constraint == "conversations_name_index", do: {:error, :already_exists}, else: {:error, :db_error}
|
||||
end
|
||||
|
||||
def find_id(name) do
|
||||
case Repo.one(from c in "conversations", where: c.name == ^name, select: c.id) do
|
||||
nil -> {:error, :not_found}
|
||||
id -> {:ok, id}
|
||||
end
|
||||
end
|
||||
|
||||
defp now, do: DateTime.truncate(DateTime.utc_now(), :second)
|
||||
end
|
||||
49
lib/elixir_ai/data/message.ex
Normal file
49
lib/elixir_ai/data/message.ex
Normal file
@@ -0,0 +1,49 @@
|
||||
defmodule ElixirAi.Message do
|
||||
import Ecto.Query
|
||||
alias ElixirAi.Repo
|
||||
|
||||
def load_for_conversation(conversation_id) do
|
||||
Repo.all(
|
||||
from m in "messages",
|
||||
where: m.conversation_id == ^conversation_id,
|
||||
order_by: m.position,
|
||||
select: %{
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
reasoning_content: m.reasoning_content,
|
||||
tool_calls: m.tool_calls,
|
||||
tool_call_id: m.tool_call_id
|
||||
}
|
||||
)
|
||||
|> Enum.map(&decode_message/1)
|
||||
end
|
||||
|
||||
def insert(conversation_id, message, position) do
|
||||
Repo.insert_all("messages", [
|
||||
[
|
||||
id: Ecto.UUID.generate(),
|
||||
conversation_id: conversation_id,
|
||||
role: to_string(message.role),
|
||||
content: message[:content],
|
||||
reasoning_content: message[:reasoning_content],
|
||||
tool_calls: encode_tool_calls(message[:tool_calls]),
|
||||
tool_call_id: message[:tool_call_id],
|
||||
position: position,
|
||||
inserted_at: DateTime.truncate(DateTime.utc_now(), :second)
|
||||
]
|
||||
])
|
||||
end
|
||||
|
||||
defp encode_tool_calls(nil), do: nil
|
||||
defp encode_tool_calls(calls), do: Jason.encode!(calls)
|
||||
|
||||
defp decode_message(row) do
|
||||
row
|
||||
|> Map.update!(:role, &String.to_existing_atom/1)
|
||||
|> drop_nil_fields()
|
||||
end
|
||||
|
||||
defp drop_nil_fields(map) do
|
||||
Map.reject(map, fn {_k, v} -> is_nil(v) end)
|
||||
end
|
||||
end
|
||||
5
lib/elixir_ai/data/repo.ex
Normal file
5
lib/elixir_ai/data/repo.ex
Normal file
@@ -0,0 +1,5 @@
|
||||
defmodule ElixirAi.Repo do
|
||||
use Ecto.Repo,
|
||||
otp_app: :elixir_ai,
|
||||
adapter: Ecto.Adapters.Postgres
|
||||
end
|
||||
4
mix.exs
4
mix.exs
@@ -54,7 +54,9 @@ defmodule ElixirAi.MixProject do
|
||||
{:telemetry_poller, "~> 1.0"},
|
||||
{:jason, "~> 1.2"},
|
||||
{:dns_cluster, "~> 0.1.1"},
|
||||
{:bandit, "~> 1.5"}
|
||||
{:bandit, "~> 1.5"},
|
||||
{:ecto_sql, "~> 3.11"},
|
||||
{:postgrex, ">= 0.0.0"}
|
||||
]
|
||||
end
|
||||
|
||||
|
||||
5
mix.lock
5
mix.lock
@@ -4,9 +4,13 @@
|
||||
"castore": {:hex, :castore, "1.0.17", "4f9770d2d45fbd91dcf6bd404cf64e7e58fed04fadda0923dc32acca0badffa2", [:mix], [], "hexpm", "12d24b9d80b910dd3953e165636d68f147a31db945d2dcb9365e441f8b5351e5"},
|
||||
"chatterbox": {:hex, :ts_chatterbox, "0.15.1", "5cac4d15dd7ad61fc3c4415ce4826fc563d4643dee897a558ec4ea0b1c835c9c", [:rebar3], [{:hpack, "~> 0.3.0", [hex: :hpack_erl, repo: "hexpm", optional: false]}], "hexpm", "4f75b91451338bc0da5f52f3480fa6ef6e3a2aeecfc33686d6b3d0a0948f31aa"},
|
||||
"ctx": {:hex, :ctx, "0.6.0", "8ff88b70e6400c4df90142e7f130625b82086077a45364a78d208ed3ed53c7fe", [:rebar3], [], "hexpm", "a14ed2d1b67723dbebbe423b28d7615eb0bdcba6ff28f2d1f1b0a7e1d4aa5fc2"},
|
||||
"db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"},
|
||||
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
|
||||
"dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"},
|
||||
"dotenvy": {:hex, :dotenvy, "1.1.1", "00e318f3c51de9fafc4b48598447e386f19204dc18ca69886905bb8f8b08b667", [:mix], [], "hexpm", "c8269471b5701e9e56dc86509c1199ded2b33dce088c3471afcfef7839766d8e"},
|
||||
"earmark": {:hex, :earmark, "1.4.48", "5f41e579d85ef812351211842b6e005f6e0cef111216dea7d4b9d58af4608434", [:mix], [], "hexpm", "a461a0ddfdc5432381c876af1c86c411fd78a25790c75023c7a4c035fdc858f9"},
|
||||
"ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.13.5", "2f8282b2ad97bf0f0d3217ea0a6fff320ead9e2f8770f810141189d182dc304e", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aa36751f4e6a2b56ae79efb0e088042e010ff4935fc8684e74c23b1f49e25fdc"},
|
||||
"esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"},
|
||||
"expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"},
|
||||
"file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
|
||||
@@ -42,6 +46,7 @@
|
||||
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
|
||||
"plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
|
||||
"postgrex": {:hex, :postgrex, "0.22.0", "fb027b58b6eab1f6de5396a2abcdaaeb168f9ed4eccbb594e6ac393b02078cbd", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a68c4261e299597909e03e6f8ff5a13876f5caadaddd0d23af0d0a61afcc5d84"},
|
||||
"req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
|
||||
"swoosh": {:hex, :swoosh, "1.23.0", "a1b7f41705357ffb06457d177e734bf378022901ce53889a68bcc59d10a23c27", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "97aaf04481ce8a351e2d15a3907778bdf3b1ea071cfff3eb8728b65943c77f6d"},
|
||||
|
||||
18
schema.sql
Normal file
18
schema.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
CREATE TABLE conversations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
inserted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE messages (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
||||
role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'tool')),
|
||||
content TEXT,
|
||||
reasoning_content TEXT,
|
||||
tool_calls JSONB,
|
||||
tool_call_id TEXT,
|
||||
position INTEGER NOT NULL,
|
||||
inserted_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
Reference in New Issue
Block a user