You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Implement core/pipeline.py with the IngestionPipeline class that orchestrates the full ingest and re-ingest flow from adapter through storage, coordinating all previously implemented components.
Requirements
Implement IngestionPipeline class in core/pipeline.py
Iterate adapter.fetch() to get NormalizedContent items
For each NormalizedContent item:
a. Look up latest version: self.document_store.get_latest_version(content.source_id)
b. Chunk via domain_chunker.chunk(content) → list[Chunk]
c. Compute curr_chunk_hashes = {chunk.chunk_hash for chunk in chunks}
d. Run differ: self.differ.diff(prev_markdown, curr_markdown, prev_hashes, curr_hashes)
e. If diff_result.changed is False: update last_fetched_at only, skip all writes, increment unchanged counter
f. If diff_result.changed is True:
Register source if new: self.document_store.register_source(...)
Determine new version number (prev_version + 1, or 1 if first ingest)
Filter to only added chunks: added_chunks = [c for c in chunks if c.chunk_hash in diff_result.added_hashes]
Embed added chunks: vectors = self.embedder.embed([c.content for c in added_chunks])
Build LineageRecord for each added chunk using source_version_id, adapter.adapter_id, adapter.domain, content.normalizer_version, self.embedder.model_id
Return summary dict: {"sources_processed": N, "chunks_added": N, "chunks_removed": N, "chunks_unchanged": N}
On first ingest of a source, all chunks are written and embedded
On re-ingest of unchanged source, no new records are created
On re-ingest with changes, only added/modified chunks are re-embedded; unchanged chunks are referenced by hash in the new version record without re-embedding
Design Guidance
core/pipeline.py already exists as a stub — implement IngestionPipeline there
Imports: from context_library.storage.document_store import DocumentStore, from context_library.storage.vector_store import VectorStore (or use lancedb directly), from context_library.storage.models import LineageRecord, SourceVersion, from context_library.core.embedder import Embedder, from context_library.core.differ import Differ, from context_library.adapters.base import BaseAdapter, from context_library.domains.base import BaseDomain
LanceDB write pattern (from storage/vector_store.py): open table, create ChunkVector instances with chunk_hash, content, source_id, embedding (the vector), then table.add(chunk_vectors)
LanceDB delete pattern: table.delete(f"chunk_hash IN {list(removed_hashes)}") — use LanceDB's SQL-like delete API
Write ordering invariant: ALWAYS write SQLite first, then LanceDB. If LanceDB write fails, data is safe in SQLite.
prev_markdown for the differ is prev_version.markdown if get_latest_version() returns a result, else None
prev_chunk_hashes for the differ is set(prev_version.chunk_hashes) if a previous version exists, else None
fetch_timestamp for create_source_version(): datetime.now(timezone.utc).isoformat()
The pipeline is the only place that writes to both SQLite and LanceDB in the same operation — this is intentional
Integration test approach: use DocumentStore(":memory:") and a temp LanceDB directory; use a real (or mock) embedder
Acceptance Criteria
First ingest of a directory: all .md files are chunked, embedded, and stored in both SQLite and LanceDB
lancedb_sync_log has one entry per chunk vector written after first ingest
Re-ingest of unchanged directory: no new source version records, no new chunk records, return dict shows chunks_added=0
Re-ingest after modifying one chunk: new source version created, only the modified chunk is re-embedded, unchanged chunks are not re-embedded, removed chunks are retired in SQLite and deleted from LanceDB
Re-ingest after adding a new file: new source record + source version created for the new file, all other files unchanged
Re-ingest after deleting a chunk: chunk retired in SQLite, removed from LanceDB, sync log updated
Implement
core/pipeline.pywith theIngestionPipelineclass that orchestrates the full ingest and re-ingest flow from adapter through storage, coordinating all previously implemented components.Requirements
IngestionPipelineclass incore/pipeline.py__init__(self, document_store: DocumentStore, embedder: Embedder, differ: Differ, vector_store_path: str | Path)ingest(adapter: BaseAdapter, domain_chunker: BaseDomain) -> dictmethod:adapter.register(self.document_store)(idempotent)adapter.fetch()to getNormalizedContentitemsNormalizedContentitem:a. Look up latest version:
self.document_store.get_latest_version(content.source_id)b. Chunk via
domain_chunker.chunk(content)→list[Chunk]c. Compute
curr_chunk_hashes = {chunk.chunk_hash for chunk in chunks}d. Run differ:
self.differ.diff(prev_markdown, curr_markdown, prev_hashes, curr_hashes)e. If
diff_result.changedis False: updatelast_fetched_atonly, skip all writes, increment unchanged counterf. If
diff_result.changedis True:self.document_store.register_source(...)self.document_store.create_source_version(...)→source_version_idadded_chunks = [c for c in chunks if c.chunk_hash in diff_result.added_hashes]vectors = self.embedder.embed([c.content for c in added_chunks])LineageRecordfor each added chunk usingsource_version_id,adapter.adapter_id,adapter.domain,content.normalizer_version,self.embedder.model_idself.document_store.write_chunks(added_chunks, lineage_records)self.document_store.retire_chunks(diff_result.removed_hashes); remove from LanceDBself.document_store.write_sync_log(added_hashes, "insert"); delete sync log for removed:self.document_store.delete_sync_log(removed_hashes){"sources_processed": N, "chunks_added": N, "chunks_removed": N, "chunks_unchanged": N}Design Guidance
core/pipeline.pyalready exists as a stub — implementIngestionPipelinetherefrom context_library.storage.document_store import DocumentStore,from context_library.storage.vector_store import VectorStore(or use lancedb directly),from context_library.storage.models import LineageRecord, SourceVersion,from context_library.core.embedder import Embedder,from context_library.core.differ import Differ,from context_library.adapters.base import BaseAdapter,from context_library.domains.base import BaseDomainstorage/vector_store.py): open table, createChunkVectorinstances withchunk_hash,content,source_id,embedding(the vector), thentable.add(chunk_vectors)table.delete(f"chunk_hash IN {list(removed_hashes)}")— use LanceDB's SQL-like delete APIprev_markdownfor the differ isprev_version.markdownifget_latest_version()returns a result, elseNoneprev_chunk_hashesfor the differ isset(prev_version.chunk_hashes)if a previous version exists, elseNonefetch_timestampforcreate_source_version():datetime.now(timezone.utc).isoformat()DocumentStore(":memory:")and a temp LanceDB directory; use a real (or mock) embedderAcceptance Criteria
.mdfiles are chunked, embedded, and stored in both SQLite and LanceDBlancedb_sync_loghas one entry per chunk vector written after first ingestchunks_added=0sources_processed,chunks_added,chunks_removed,chunks_unchangedtests/core/test_pipeline.pypasses: first ingest → re-ingest unchanged → re-ingest with changeDependencies
Phase 2: Document Store (SQLite), Phase 3: Adapter Contract and Filesystem Adapter, Phase 4: Notes Domain Chunker, Phase 5: Differ, Phase 6: Embedder
Parent Issue
Part of #40
Discussion
This work is detailed in discussion 41