From dc366433bb139367c8a037fe2f33611383285181 Mon Sep 17 00:00:00 2001 From: Rodribm10 Date: Sun, 19 Apr 2026 00:35:06 -0300 Subject: [PATCH] feat(captain-memory): add UpdateEmbeddingJob --- .../contradiction_checker_job.rb | 14 +++++++ .../contact_memories/update_embedding_job.rb | 17 +++++++++ .../update_embedding_job_spec.rb | 38 +++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 enterprise/app/jobs/captain/contact_memories/contradiction_checker_job.rb create mode 100644 enterprise/app/jobs/captain/contact_memories/update_embedding_job.rb create mode 100644 spec/enterprise/jobs/captain/contact_memories/update_embedding_job_spec.rb diff --git a/enterprise/app/jobs/captain/contact_memories/contradiction_checker_job.rb b/enterprise/app/jobs/captain/contact_memories/contradiction_checker_job.rb new file mode 100644 index 000000000..02f1c89e7 --- /dev/null +++ b/enterprise/app/jobs/captain/contact_memories/contradiction_checker_job.rb @@ -0,0 +1,14 @@ +class Captain::ContactMemories::ContradictionCheckerJob < ApplicationJob + queue_as :low + + # TODO(phase3-task3.2): implement full contradiction detection logic. + # This skeleton exists so Captain::ContactMemories::UpdateEmbeddingJob can + # enqueue it when run_contradiction_check is true. Task 3.2 will replace + # the body with the real implementation. + def perform(memory_id) + memory = Captain::ContactMemory.find_by(id: memory_id) + return if memory.blank? + + # no-op until task 3.2 + end +end diff --git a/enterprise/app/jobs/captain/contact_memories/update_embedding_job.rb b/enterprise/app/jobs/captain/contact_memories/update_embedding_job.rb new file mode 100644 index 000000000..49d74f8ac --- /dev/null +++ b/enterprise/app/jobs/captain/contact_memories/update_embedding_job.rb @@ -0,0 +1,17 @@ +class Captain::ContactMemories::UpdateEmbeddingJob < ApplicationJob + queue_as :low + + retry_on Captain::Llm::EmbeddingService::EmbeddingsError, wait: :polynomially_longer, attempts: 3 + + def perform(memory_id, run_contradiction_check: false) + memory = Captain::ContactMemory.find_by(id: memory_id) + return if memory.blank? + + embedding = Captain::Llm::EmbeddingService.new(account_id: memory.account_id).get_embedding(memory.content) + return if embedding.blank? + + memory.update!(embedding: embedding) + + Captain::ContactMemories::ContradictionCheckerJob.perform_later(memory.id) if run_contradiction_check + end +end diff --git a/spec/enterprise/jobs/captain/contact_memories/update_embedding_job_spec.rb b/spec/enterprise/jobs/captain/contact_memories/update_embedding_job_spec.rb new file mode 100644 index 000000000..fddfd4064 --- /dev/null +++ b/spec/enterprise/jobs/captain/contact_memories/update_embedding_job_spec.rb @@ -0,0 +1,38 @@ +require 'rails_helper' + +RSpec.describe Captain::ContactMemories::UpdateEmbeddingJob do + let(:memory) { create(:captain_contact_memory) } + let(:embedding_service) { instance_double(Captain::Llm::EmbeddingService) } + + before do + allow(Captain::Llm::EmbeddingService).to receive(:new).and_return(embedding_service) + allow(embedding_service).to receive(:get_embedding).and_return(Array.new(1536, 0.5)) + end + + it 'sets embedding on the memory' do + described_class.perform_now(memory.id) + stored = memory.reload.embedding + # Model uses `has_neighbors :embedding, normalize: true`, so pgvector stores + # the L2-normalized vector rather than the raw stub. We assert the embedding + # is persisted with the correct dimensionality and reflects the stubbed + # uniform vector (all components equal after normalization). + expect(stored).to be_present + expect(stored.size).to eq(1536) + expect(stored.uniq.size).to eq(1) + expect(stored.first).to be_within(1e-6).of(1.0 / Math.sqrt(1536)) + end + + it 'does not enqueue ContradictionCheckerJob inside itself' do + expect { described_class.perform_now(memory.id) } + .not_to have_enqueued_job(Captain::ContactMemories::ContradictionCheckerJob) + end + + it 'enqueues ContradictionCheckerJob after setting embedding when configured' do + expect { described_class.perform_now(memory.id, run_contradiction_check: true) } + .to have_enqueued_job(Captain::ContactMemories::ContradictionCheckerJob).with(memory.id) + end + + it 'tolerates a missing memory silently' do + expect { described_class.perform_now(99_999_999) }.not_to raise_error + end +end