feat(captain-memory): add Captain::ContactMemory model with scopes and lifecycle methods
This commit is contained in:
parent
575af02aff
commit
e1273f142b
78
enterprise/app/models/captain/contact_memory.rb
Normal file
78
enterprise/app/models/captain/contact_memory.rb
Normal file
@ -0,0 +1,78 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: captain_contact_memories
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# account_id :bigint not null
|
||||
# contact_id :bigint not null
|
||||
# memory_type :string not null
|
||||
# content :text not null
|
||||
# evidence :text not null
|
||||
# confidence :float not null
|
||||
# scope :string not null, default: 'global'
|
||||
# embedding :vector(1536)
|
||||
# source_conversation_id :bigint
|
||||
# source_unit_id :bigint
|
||||
# source_inbox_id :bigint
|
||||
# expires_at :datetime
|
||||
# last_verified_at :datetime not null
|
||||
# superseded_at :datetime
|
||||
# superseded_by_id :bigint
|
||||
# deleted_at :datetime
|
||||
# metadata :jsonb not null, default: {}
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
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
|
||||
101
spec/enterprise/models/captain/contact_memory_spec.rb
Normal file
101
spec/enterprise/models/captain/contact_memory_spec.rb
Normal file
@ -0,0 +1,101 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Captain::ContactMemory, type: :model do
|
||||
let(:account) { create(:account) }
|
||||
let(:contact) { create(:contact, account: account) }
|
||||
|
||||
describe 'validations' do
|
||||
it 'requires memory_type' do
|
||||
memory = build(:captain_contact_memory, account: account, contact: contact, memory_type: nil)
|
||||
expect(memory).not_to be_valid
|
||||
end
|
||||
|
||||
it 'requires content' do
|
||||
memory = build(:captain_contact_memory, account: account, contact: contact, content: '')
|
||||
expect(memory).not_to be_valid
|
||||
end
|
||||
|
||||
it 'requires evidence' do
|
||||
memory = build(:captain_contact_memory, account: account, contact: contact, evidence: '')
|
||||
expect(memory).not_to be_valid
|
||||
end
|
||||
|
||||
it 'requires confidence between 0 and 1' do
|
||||
memory = build(:captain_contact_memory, account: account, contact: contact, confidence: 1.5)
|
||||
expect(memory).not_to be_valid
|
||||
end
|
||||
|
||||
it 'rejects invalid memory_type' do
|
||||
memory = build(:captain_contact_memory, account: account, contact: contact, memory_type: 'invalid_type')
|
||||
expect(memory).not_to be_valid
|
||||
end
|
||||
|
||||
it 'limits content to 1000 chars' do
|
||||
memory = build(:captain_contact_memory, account: account, contact: contact, content: 'a' * 1001)
|
||||
expect(memory).not_to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
describe '.active' do
|
||||
# rubocop:disable RSpec/LetSetup
|
||||
let!(:active) { create(:captain_contact_memory, account: account, contact: contact) }
|
||||
let!(:deleted) { create(:captain_contact_memory, account: account, contact: contact, deleted_at: Time.current) }
|
||||
let!(:superseded) { create(:captain_contact_memory, account: account, contact: contact, superseded_at: Time.current) }
|
||||
let!(:expired) { create(:captain_contact_memory, account: account, contact: contact, expires_at: 1.day.ago) }
|
||||
# rubocop:enable RSpec/LetSetup
|
||||
|
||||
it 'excludes deleted, superseded, and expired' do
|
||||
expect(described_class.active).to contain_exactly(active)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.scope_compatible' do
|
||||
# rubocop:disable RSpec/LetSetup
|
||||
let!(:global_fact) { create(:captain_contact_memory, account: account, contact: contact, scope: 'global') }
|
||||
let!(:unit_fact) { create(:captain_contact_memory, account: account, contact: contact, scope: 'unit:5') }
|
||||
let!(:other_unit_fact) { create(:captain_contact_memory, account: account, contact: contact, scope: 'unit:7') }
|
||||
# rubocop:enable RSpec/LetSetup
|
||||
|
||||
it 'returns global + facts for requested unit' do
|
||||
expect(described_class.scope_compatible(5)).to contain_exactly(global_fact, unit_fact)
|
||||
end
|
||||
|
||||
it 'when unit_id is nil, returns only global' do
|
||||
expect(described_class.scope_compatible(nil)).to contain_exactly(global_fact)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#soft_delete!' do
|
||||
let(:memory) { create(:captain_contact_memory, account: account, contact: contact) }
|
||||
|
||||
it 'sets deleted_at' do
|
||||
freeze_time do
|
||||
memory.soft_delete!
|
||||
expect(memory.reload.deleted_at).to eq(Time.current)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#supersede_by!' do
|
||||
let(:old) { create(:captain_contact_memory, account: account, contact: contact) }
|
||||
let(:new_one) { create(:captain_contact_memory, account: account, contact: contact) }
|
||||
|
||||
it 'links the old memory as superseded by the new one' do
|
||||
old.supersede_by!(new_one)
|
||||
expect(old.reload.superseded_by_id).to eq(new_one.id)
|
||||
expect(old.superseded_at).not_to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '#recall_weight' do
|
||||
it 'returns 1.0 for non-expired' do
|
||||
memory = build(:captain_contact_memory, account: account, contact: contact, expires_at: 1.day.from_now)
|
||||
expect(memory.recall_weight).to eq(1.0)
|
||||
end
|
||||
|
||||
it 'returns 0.7 for expired' do
|
||||
memory = build(:captain_contact_memory, account: account, contact: contact, expires_at: 1.day.ago)
|
||||
expect(memory.recall_weight).to eq(0.7)
|
||||
end
|
||||
end
|
||||
end
|
||||
12
spec/factories/captain_contact_memories.rb
Normal file
12
spec/factories/captain_contact_memories.rb
Normal file
@ -0,0 +1,12 @@
|
||||
FactoryBot.define do
|
||||
factory :captain_contact_memory, class: 'Captain::ContactMemory' do
|
||||
account
|
||||
contact
|
||||
memory_type { 'preferencia' }
|
||||
content { 'Prefere Stilo com hidromassagem' }
|
||||
evidence { "cliente disse 'quero a Stilo com hidro de novo'" }
|
||||
confidence { 0.9 }
|
||||
scope { 'global' }
|
||||
last_verified_at { Time.current }
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue
Block a user