lib/openagents/conversations/message.ex

main at 58e6347eeb72 · 2 KB

defmodule OpenAgents.Conversations.Message do
  @moduledoc false

  use Ecto.Schema
  import Ecto.Changeset

  @roles ~w(user assistant system)
  @statuses ~w(streaming complete failed cancelled)
  @maximum_content_bytes 1_048_576

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id
  @timestamps_opts [type: :utc_datetime_usec]

  schema "messages" do
    belongs_to :conversation, OpenAgents.Conversations.Conversation
    field :role, :string
    field :content, :string, default: ""
    field :status, :string, default: "complete"
    field :provider_response_id, :string
    field :modality, :string, default: "text"
    belongs_to :voice_session, OpenAgents.Voice.Session
    field :provider_item_id, :string
    field :transcript_kind, :string
    field :interrupted, :boolean, default: false
    belongs_to :work_job, OpenAgents.Work.Job
    field :search_vector, :string, load_in_query: false
    timestamps()
  end

  def changeset(message, attributes) do
    message
    |> cast(attributes, [
      :conversation_id,
      :role,
      :content,
      :status,
      :provider_response_id,
      :modality,
      :voice_session_id,
      :provider_item_id,
      :transcript_kind,
      :interrupted,
      :work_job_id
    ])
    |> validate_required([:conversation_id, :role, :status, :modality, :interrupted])
    |> validate_inclusion(:role, @roles)
    |> validate_inclusion(:status, @statuses)
    |> validate_inclusion(:modality, ~w(text voice))
    |> validate_content_bound()
    |> validate_voice_provenance()
    |> foreign_key_constraint(:conversation_id)
    |> foreign_key_constraint(:voice_session_id)
    |> unique_constraint([:voice_session_id, :provider_item_id, :role],
      name: :messages_voice_item_role_index
    )
  end

  defp validate_content_bound(changeset) do
    case get_field(changeset, :content) do
      content when is_binary(content) and byte_size(content) <= @maximum_content_bytes ->
        changeset

      content when is_binary(content) ->
        add_error(changeset, :content, "exceeds #{@maximum_content_bytes} bytes")

      _invalid ->
        changeset
    end
  end

  defp validate_voice_provenance(changeset) do
    modality = get_field(changeset, :modality)

    case modality do
      "text" ->
        changeset

      "voice" ->
        changeset
        |> validate_required([:voice_session_id, :provider_item_id, :transcript_kind])
        |> validate_inclusion(:transcript_kind, [
          "provider_input_transcription",
          "provider_output_transcript"
        ])

      _invalid ->
        changeset
    end
  end
end