iachat/enterprise/app/models/captain/contact_memory.rb
Rodribm10 cfffea9c16 feat(captain): semantic memory fixes + roleta + reclamações + analytics
Consolida o trabalho desta branch de abril/2026 em um bloco pronto pra
testar em staging antes do merge pra main.

## Correções de memória semântica
- ExtractionService: Princípio Zero + Regra de Ouro (ação consumada vs intenção).
- Cenário Daniela_Reservas: Passo 0 de classificação (consulta/intenção/fora).

## Roleta da Sorte (end-to-end)
- Schema Supabase + 7 RPCs atômicas (server-side, idempotentes).
- Services: Offer, Redeem, WeeklyReport.
- Jobs: OfferRouletteJob (hook em ConfirmationService após Pix pago),
  NotifyRevealed + Scheduler de fallback.
- Tool manual GenerateRoletaLinkTool + endpoint público /roleta/notify.
- Dashboard /captain/roleta com Resgate + Relatório + anomaly detection.

## Cenário Reclamacoes_Ouvidoria
- Triagem P1-P4, framework LAST, Three-level listening, Self-check.
- Sem compensação material, detecção de cliente frustrado eleva prioridade.

## Analytics
- Funil de conversão /captain/funnel: 5 etapas via regex, zero LLM.
- Detector de churn via ChurnOutreach* (cron dias úteis 10h-17h BRT).

## Trabalho pré-existente incluído
- Captain Executive Reports (ceo_digest, mattermost_delivery).
- get_reserva_preco_tool, Lifecycle ajustes, Reservations UI polimentos.

## Outros
- .gitignore: patterns pra credenciais.
- Migrations de scenarios idempotentes.
- i18n completa pt_BR+en pra roleta/funnel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 15:36:25 -03:00

95 lines
3.5 KiB
Ruby

# == Schema Information
#
# Table name: captain_contact_memories
#
# id :bigint not null, primary key
# confidence :float not null
# content :text not null
# deleted_at :datetime
# embedding :vector(1536)
# evidence :text not null
# expires_at :datetime
# last_verified_at :datetime not null
# memory_type :string not null
# metadata :jsonb not null
# scope :string default("global"), not null
# superseded_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# contact_id :bigint not null
# source_conversation_id :bigint
# source_inbox_id :bigint
# source_unit_id :bigint
# superseded_by_id :bigint
#
# Indexes
#
# idx_ccm_analytics (source_unit_id,memory_type,created_at)
# idx_ccm_embedding (embedding) USING ivfflat
# idx_ccm_hard_delete (deleted_at) WHERE (deleted_at IS NOT NULL)
# idx_ccm_recall (account_id,contact_id) WHERE ((deleted_at IS NULL) AND (superseded_at IS NULL))
# idx_ccm_source_conversation (source_conversation_id)
# idx_ccm_superseded (superseded_by_id) WHERE (superseded_at IS NOT NULL)
# index_captain_contact_memories_on_account_id (account_id)
# index_captain_contact_memories_on_contact_id (contact_id)
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id) ON DELETE => cascade
# fk_rails_... (contact_id => contacts.id) ON DELETE => cascade
#
class Captain::ContactMemory < ApplicationRecord
self.table_name = 'captain_contact_memories'
MEMORY_TYPES = %w[
preferencia data_comemorativa vinculo_social padrao_comportamental
reclamacao feedback_positivo restricao vinculo_comercial contexto_pessoal
].freeze
belongs_to :account
belongs_to :contact
belongs_to :source_conversation, class_name: 'Conversation', optional: true
belongs_to :source_unit, class_name: 'Captain::Unit', optional: true
belongs_to :source_inbox, class_name: 'Inbox', optional: true
belongs_to :superseded_by, class_name: 'Captain::ContactMemory', optional: true
has_neighbors :embedding, normalize: true
validates :memory_type, presence: true, inclusion: { in: MEMORY_TYPES }
validates :content, presence: true, length: { maximum: 1000 }
validates :evidence, presence: true
validates :confidence, presence: true, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 1 }
validates :scope, presence: true
scope :active, lambda {
where(deleted_at: nil, superseded_at: nil)
.where('expires_at IS NULL OR expires_at > ?', Time.current)
}
scope :for_contact, ->(id) { where(contact_id: id) }
scope :by_type, ->(type) { where(memory_type: type) }
scope :scope_compatible, lambda { |unit_id|
if unit_id.present?
where(scope: ['global', "unit:#{unit_id}"])
else
where(scope: 'global')
end
}
def soft_delete!
update!(deleted_at: Time.current)
end
def supersede_by!(other)
update!(superseded_at: Time.current, superseded_by_id: other.id)
end
def recall_weight
return 0.7 if expires_at.present? && expires_at < Time.current
1.0
end
end