Persist chat history
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
defmodule MusicLibrary.Chats do
|
||||
@moduledoc """
|
||||
Persistent storage for AI chat conversations about records and artists.
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
|
||||
alias MusicLibrary.Chats.{Chat, Message}
|
||||
alias MusicLibrary.Repo
|
||||
|
||||
@spec list_chats(atom(), String.t()) :: [Chat.t()]
|
||||
def list_chats(entity, musicbrainz_id) do
|
||||
message_count_query =
|
||||
from(m in Message,
|
||||
where: m.chat_id == parent_as(:chat).id,
|
||||
select: count(m.id)
|
||||
)
|
||||
|
||||
from(c in Chat,
|
||||
as: :chat,
|
||||
where: c.entity == ^entity and c.musicbrainz_id == ^musicbrainz_id,
|
||||
order_by: [desc: c.updated_at],
|
||||
select_merge: %{message_count: subquery(message_count_query)}
|
||||
)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@spec has_any_chats?(atom(), String.t()) :: boolean()
|
||||
def has_any_chats?(entity, musicbrainz_id) do
|
||||
from(c in Chat,
|
||||
where: c.entity == ^entity and c.musicbrainz_id == ^musicbrainz_id
|
||||
)
|
||||
|> Repo.exists?()
|
||||
end
|
||||
|
||||
@spec get_chat!(String.t()) :: Chat.t()
|
||||
def get_chat!(id) do
|
||||
Chat
|
||||
|> Repo.get!(id)
|
||||
|> Repo.preload(:messages)
|
||||
end
|
||||
|
||||
@spec create_chat_with_message(map(), map()) ::
|
||||
{:ok, Chat.t()} | {:error, Ecto.Changeset.t()}
|
||||
def create_chat_with_message(chat_attrs, message_attrs) do
|
||||
topic =
|
||||
message_attrs
|
||||
|> Map.get(:content, "")
|
||||
|> String.trim()
|
||||
|> String.slice(0, 80)
|
||||
|
||||
Repo.transaction(fn ->
|
||||
chat_attrs = Map.put(chat_attrs, :topic, topic)
|
||||
|
||||
with {:ok, chat} <- %Chat{} |> Chat.changeset(chat_attrs) |> Repo.insert(),
|
||||
message_attrs = Map.merge(message_attrs, %{position: 0}),
|
||||
{:ok, _message} <-
|
||||
%Message{chat_id: chat.id}
|
||||
|> Message.changeset(message_attrs)
|
||||
|> Repo.insert() do
|
||||
Repo.preload(chat, :messages)
|
||||
else
|
||||
{:error, changeset} -> Repo.rollback(changeset)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@spec add_message(Chat.t(), map()) :: {:ok, Message.t()} | {:error, Ecto.Changeset.t()}
|
||||
def add_message(%Chat{} = chat, attrs) do
|
||||
next_position =
|
||||
from(m in Message,
|
||||
where: m.chat_id == ^chat.id,
|
||||
select: coalesce(max(m.position), -1)
|
||||
)
|
||||
|> Repo.one!()
|
||||
|> Kernel.+(1)
|
||||
|
||||
attrs = Map.put(attrs, :position, next_position)
|
||||
|
||||
result =
|
||||
%Message{chat_id: chat.id}
|
||||
|> Message.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
|
||||
case result do
|
||||
{:ok, message} ->
|
||||
touch_chat(chat)
|
||||
{:ok, message}
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
@spec delete_chat(Chat.t()) :: {:ok, Chat.t()} | {:error, Ecto.Changeset.t()}
|
||||
def delete_chat(%Chat{} = chat) do
|
||||
Repo.delete(chat)
|
||||
end
|
||||
|
||||
defp touch_chat(chat) do
|
||||
now = DateTime.truncate(DateTime.utc_now(), :second)
|
||||
|
||||
from(c in Chat, where: c.id == ^chat.id)
|
||||
|> Repo.update_all(set: [updated_at: now])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,31 @@
|
||||
defmodule MusicLibrary.Chats.Chat do
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
alias MusicLibrary.Chats.Message
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "chats" do
|
||||
field :entity, Ecto.Enum, values: [:record, :artist]
|
||||
field :musicbrainz_id, Ecto.UUID
|
||||
field :topic, :string
|
||||
|
||||
field :message_count, :integer, virtual: true, default: 0
|
||||
|
||||
has_many :messages, Message, preload_order: [asc: :position]
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
@spec changeset(t() | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t()
|
||||
def changeset(chat, attrs) do
|
||||
chat
|
||||
|> cast(attrs, [:entity, :musicbrainz_id, :topic])
|
||||
|> validate_required([:entity, :musicbrainz_id])
|
||||
|> validate_length(:topic, max: 200)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,29 @@
|
||||
defmodule MusicLibrary.Chats.Message do
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
alias MusicLibrary.Chats.Chat
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "chat_messages" do
|
||||
field :role, :string
|
||||
field :content, :string
|
||||
field :position, :integer
|
||||
|
||||
belongs_to :chat, Chat
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
@spec changeset(t() | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t()
|
||||
def changeset(message, attrs) do
|
||||
message
|
||||
|> cast(attrs, [:role, :content, :position])
|
||||
|> validate_required([:role, :content, :position])
|
||||
|> validate_inclusion(:role, ["user", "assistant"])
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user