feat(captain): add chunk builder job and orchestration
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Captain::Documents::ChunkBuilderJob, type: :job do
|
||||
let(:document) { create(:captain_document, status: :available) }
|
||||
let(:builder_service) { instance_double(Captain::Documents::ChunkBuilderService) }
|
||||
|
||||
describe '#perform' do
|
||||
it 'runs the chunk builder service for the document' do
|
||||
allow(Captain::Documents::ChunkBuilderService).to receive(:new).with(document).and_return(builder_service)
|
||||
allow(builder_service).to receive(:process)
|
||||
|
||||
described_class.new.perform(document)
|
||||
|
||||
expect(Captain::Documents::ChunkBuilderService).to have_received(:new).with(document)
|
||||
expect(builder_service).to have_received(:process)
|
||||
end
|
||||
|
||||
it 'bubbles service errors for retry handling' do
|
||||
allow(Captain::Documents::ChunkBuilderService).to receive(:new).with(document).and_return(builder_service)
|
||||
allow(builder_service).to receive(:process).and_raise(StandardError, 'transient failure')
|
||||
|
||||
expect { described_class.new.perform(document) }.to raise_error(StandardError, 'transient failure')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,80 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Captain::Documents::ChunkBuilderService do
|
||||
let(:document) { create(:captain_document, status: :available, content: 'Pricing and limits content') }
|
||||
let(:chunking_service) { instance_double(Captain::Documents::ChunkingService) }
|
||||
let(:embedding_service) { instance_double(Captain::Documents::ChunkEmbeddingService) }
|
||||
let(:context_service) { instance_double(Captain::Documents::ContextGenerationService) }
|
||||
let(:chunks) do
|
||||
[
|
||||
{ position: 0, content: 'Chunk 1', token_count: 10 },
|
||||
{ position: 1, content: 'Chunk 2', token_count: 12 }
|
||||
]
|
||||
end
|
||||
let(:embedding_vector) { Array.new(1536, 0.1) }
|
||||
|
||||
before do
|
||||
allow(Captain::Documents::ChunkingService).to receive(:new).and_return(chunking_service)
|
||||
allow(chunking_service).to receive(:chunk).and_return(chunks)
|
||||
|
||||
allow(Captain::Documents::ChunkEmbeddingService).to receive(:new).and_return(embedding_service)
|
||||
allow(embedding_service).to receive(:build_record_attributes) do |args|
|
||||
chunk = args.fetch(:chunk)
|
||||
{
|
||||
document_id: document.id,
|
||||
assistant_id: document.assistant_id,
|
||||
account_id: document.account_id,
|
||||
position: chunk.fetch(:position),
|
||||
content: chunk.fetch(:content),
|
||||
token_count: chunk[:token_count],
|
||||
context: args[:context],
|
||||
embedding: embedding_vector,
|
||||
created_at: Time.current,
|
||||
updated_at: Time.current
|
||||
}
|
||||
end
|
||||
|
||||
allow(Captain::Documents::ContextGenerationService).to receive(:new).and_return(context_service)
|
||||
allow(context_service).to receive(:generate).and_return('Chunk context')
|
||||
end
|
||||
|
||||
describe '#process' do
|
||||
it 'builds chunks and marks the document ready with progress counters' do
|
||||
described_class.new(document).process
|
||||
|
||||
document.reload
|
||||
expect(document.chunking_status).to eq('ready')
|
||||
expect(document.expected_chunk_count).to eq(2)
|
||||
expect(document.indexed_chunk_count).to eq(2)
|
||||
expect(document.chunks_generated_at).to be_present
|
||||
expect(document.last_chunk_error).to be_nil
|
||||
expect(document.chunks.count).to eq(2)
|
||||
end
|
||||
|
||||
it 'replaces existing chunks when reprocessed (idempotent rebuild)' do
|
||||
create(
|
||||
:captain_document_chunk,
|
||||
document: document,
|
||||
assistant: document.assistant,
|
||||
account: document.account,
|
||||
position: 99,
|
||||
embedding: nil
|
||||
)
|
||||
|
||||
described_class.new(document).process
|
||||
|
||||
positions = document.chunks.reload.order(:position).pluck(:position)
|
||||
expect(positions).to eq([0, 1])
|
||||
end
|
||||
|
||||
it 'marks the document failed and stores the error when processing fails' do
|
||||
allow(chunking_service).to receive(:chunk).and_raise(StandardError, 'chunking failed')
|
||||
|
||||
expect { described_class.new(document).process }.to raise_error(StandardError, 'chunking failed')
|
||||
|
||||
document.reload
|
||||
expect(document.chunking_status).to eq('failed')
|
||||
expect(document.last_chunk_error).to eq('chunking failed')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
FactoryBot.define do
|
||||
factory :captain_document_chunk, class: 'Captain::DocumentChunk' do
|
||||
association :document, factory: :captain_document
|
||||
assistant { document.assistant }
|
||||
account { document.account }
|
||||
content { 'Chunk content' }
|
||||
add_attribute(:context) { 'Chunk context' }
|
||||
embedding { Array.new(1536, 0.1) }
|
||||
position { 0 }
|
||||
token_count { 12 }
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user