defmodule OpenAgents.Box.ConversationBox do
@moduledoc "One box a conversation provisioned, with its last observed lifecycle state."
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
@states ~w(init provisioning provisioned cloning ready idle running archiving archived error)
@setup_statuses ~w(pending running done failed)
schema "conversation_boxes" do
belongs_to :conversation, OpenAgents.Conversations.Conversation
field :box_id, :string
field :label, :string
field :state, :string, default: "provisioning"
field :setup_status, :string, default: "pending"
field :stopped_at, :utc_datetime_usec
field :stop_reason, :string
field :stop_requested_at, :utc_datetime_usec
field :last_reconciled_at, :utc_datetime_usec
field :reconciliation_failures, :integer, default: 0
field :reconciliation_error, :string
field :next_reconciliation_at, :utc_datetime_usec
field :lifetime_seconds, :integer
field :settled_cost_microusd, :integer
field :usage_settled_at, :utc_datetime_usec
timestamps(type: :utc_datetime_usec)
end
def changeset(conversation_box, attrs) do
conversation_box
|> cast(attrs, [
:state,
:setup_status,
:stopped_at,
:label,
:stop_reason,
:stop_requested_at,
:last_reconciled_at,
:reconciliation_failures,
:reconciliation_error,
:next_reconciliation_at,
:lifetime_seconds,
:settled_cost_microusd,
:usage_settled_at
])
|> put_programmatic_change(attrs, :conversation_id)
|> put_programmatic_change(attrs, :box_id)
|> put_default_label()
|> validate_required([:conversation_id, :box_id, :label, :state, :setup_status])
|> validate_inclusion(:state, @states)
|> validate_inclusion(:setup_status, @setup_statuses)
|> unique_constraint(:box_id)
|> unique_constraint(:label, name: :conversation_boxes_active_label_index)
|> foreign_key_constraint(:conversation_id)
end
defp put_programmatic_change(changeset, attrs, field) do
case Map.fetch(attrs, field) do
{:ok, value} -> put_change(changeset, field, value)
:error -> changeset
end
end
defp put_default_label(changeset) do
case get_field(changeset, :label) do
label when is_binary(label) and label != "" ->
changeset
_missing ->
case get_field(changeset, :box_id) do
box_id when is_binary(box_id) ->
put_change(changeset, :label, "box-" <> String.slice(box_id, 0, 64))
_missing_box_id ->
changeset
end
end
end
@spec states() :: [String.t()]
def states, do: @states
end