fix(captain-memory): ContradictionChecker exact-match parsing + rescue wrap + LLM failure test
This commit is contained in:
parent
9bc6429b91
commit
6723473fdc
@ -13,6 +13,8 @@ class Captain::ContactMemories::ContradictionCheckerService
|
||||
candidates.each do |candidate|
|
||||
candidate.supersede_by!(@memory) if contradicts?(candidate, @memory)
|
||||
end
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("[ContradictionChecker] call failed: #{e.class}: #{e.message} (memory_id=#{@memory&.id})")
|
||||
end
|
||||
|
||||
private
|
||||
@ -26,23 +28,31 @@ class Captain::ContactMemories::ContradictionCheckerService
|
||||
.where.not(embedding: nil)
|
||||
.nearest_neighbors(:embedding, @memory.embedding, distance: 'cosine')
|
||||
.first(MAX_CANDIDATES)
|
||||
.select { |c| cosine_distance(c) < DISTANCE_THRESHOLD }
|
||||
end
|
||||
|
||||
def cosine_distance(other)
|
||||
other.neighbor_distance
|
||||
.select { |c| c.neighbor_distance < DISTANCE_THRESHOLD }
|
||||
end
|
||||
|
||||
def contradicts?(fact_a, fact_b)
|
||||
response = RubyLLM.chat(model: CHECK_MODEL).with_temperature(0).ask(<<~PROMPT).content.to_s.downcase
|
||||
answer = query_llm_for_contradiction(fact_a, fact_b)
|
||||
answer == 'sim'
|
||||
rescue StandardError => e
|
||||
Rails.logger.warn("[ContradictionChecker] #{e.class}: #{e.message}")
|
||||
false
|
||||
end
|
||||
|
||||
def query_llm_for_contradiction(fact_a, fact_b)
|
||||
response = RubyLLM.chat(model: CHECK_MODEL).with_temperature(0).ask(contradiction_prompt(fact_a, fact_b)).content.to_s
|
||||
# Extract the first meaningful word. Expected "sim" or "nao" (or "não").
|
||||
first_word = response.strip.downcase.gsub(/[^a-zãáéíóúç]/, ' ').split.first.to_s
|
||||
# Normalize "não" → "nao" for ASCII comparison
|
||||
first_word.tr('ãáéíóúç', 'aaeiouc')
|
||||
end
|
||||
|
||||
def contradiction_prompt(fact_a, fact_b)
|
||||
<<~PROMPT
|
||||
Estes 2 fatos sobre o mesmo cliente se contradizem?
|
||||
Fato A: "#{fact_a.content}"
|
||||
Fato B: "#{fact_b.content}"
|
||||
Responda apenas "sim" ou "nao".
|
||||
PROMPT
|
||||
response.include?('sim')
|
||||
rescue StandardError => e
|
||||
Rails.logger.warn("[ContradictionChecker] #{e.class}: #{e.message}")
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
@ -57,5 +57,20 @@ RSpec.describe Captain::ContactMemories::ContradictionCheckerService do
|
||||
expect { described_class.new(memory: embeddingless).call }.not_to raise_error
|
||||
expect(old_fact.reload.superseded_by_id).to be_nil
|
||||
end
|
||||
|
||||
context 'when LLM raises during contradiction check' do
|
||||
it 'does not supersede when LLM raises' do
|
||||
old_fact
|
||||
chat_double = instance_double(RubyLLM::Chat)
|
||||
allow(chat_double).to receive(:with_temperature).and_return(chat_double)
|
||||
allow(chat_double).to receive(:ask).and_raise(StandardError, 'LLM down')
|
||||
allow(RubyLLM).to receive(:chat).and_return(chat_double)
|
||||
|
||||
# Exercise the real rescue path inside contradicts? (no stub here)
|
||||
real_service = described_class.new(memory: new_fact)
|
||||
expect { real_service.call }.not_to raise_error
|
||||
expect(old_fact.reload.superseded_by_id).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Loading…
Reference in New Issue
Block a user