Skip to main content

lib/cairnloop/retrieval/gap_event.ex

defmodule Cairnloop.Retrieval.GapEvent do
  use Ecto.Schema
  import Ecto.Changeset

  alias Cairnloop.Retrieval.GapEventSnapshot

  @surface_values [:draft_generation, :search_modal, :api, :unspecified]
  @tenant_scope_values [:host_user_scoped, :public_only, :system_unscoped]
  @ui_surface_values [:conversation, :inbox, :settings, :unspecified]
  @outcome_values [:empty_recall, :retrieval_error, :weak_grounding, :policy_limit]
  @reason_values [
    :canonical_results,
    :mixed_results,
    :assistive_only_results,
    :no_canonical_results,
    :canonical_insufficient_detail,
    :clarification_limit_reached,
    :provider_timeout,
    :index_unavailable,
    :unexpected_error
  ]
  @max_excerpt_length 160
  @max_snapshots 5

  schema "cairnloop_retrieval_gap_events" do
    field(:occurred_at, :utc_datetime_usec)
    field(:surface, Ecto.Enum, values: @surface_values)
    field(:outcome_class, Ecto.Enum, values: @outcome_values)
    field(:reason, Ecto.Enum, values: @reason_values)
    field(:host_user_id, :string)
    field(:tenant_scope, Ecto.Enum, values: @tenant_scope_values)
    field(:ui_surface, Ecto.Enum, values: @ui_surface_values, default: :unspecified)
    field(:query_fingerprint, :string)
    field(:sanitized_query_excerpt, :string)
    field(:canonical_hit_count, :integer, default: 0)
    field(:assistive_hit_count, :integer, default: 0)
    field(:clarification_attempts, :integer, default: 0)

    embeds_many(:attempted_evidence_snapshots, GapEventSnapshot, on_replace: :delete)

    timestamps(updated_at: false)
  end

  def changeset(gap_event, attrs) do
    gap_event
    |> cast(attrs, [
      :occurred_at,
      :surface,
      :outcome_class,
      :reason,
      :host_user_id,
      :tenant_scope,
      :ui_surface,
      :query_fingerprint,
      :sanitized_query_excerpt,
      :canonical_hit_count,
      :assistive_hit_count,
      :clarification_attempts
    ])
    |> cast_embed(:attempted_evidence_snapshots)
    |> validate_required([
      :occurred_at,
      :surface,
      :outcome_class,
      :reason,
      :tenant_scope,
      :ui_surface,
      :query_fingerprint,
      :sanitized_query_excerpt
    ])
    |> validate_length(:query_fingerprint, is: 64)
    |> validate_length(:sanitized_query_excerpt, max: @max_excerpt_length)
    |> validate_number(:canonical_hit_count, greater_than_or_equal_to: 0)
    |> validate_number(:assistive_hit_count, greater_than_or_equal_to: 0)
    |> validate_number(:clarification_attempts, greater_than_or_equal_to: 0)
    |> validate_change(:attempted_evidence_snapshots, fn :attempted_evidence_snapshots,
                                                         snapshots ->
      if length(snapshots || []) <= @max_snapshots do
        []
      else
        [attempted_evidence_snapshots: "must contain at most #{@max_snapshots} snapshots"]
      end
    end)
  end
end