feat: add scoped collection and document management APIs - #2378
feat: add scoped collection and document management APIs#2378KyleZheng1284 wants to merge 44 commits into
Conversation
Greptile SummaryThis PR introduces scope-isolated collection and document lifecycle management to NeMo Retriever, adding a logical API boundary where clients work with collection names and stable document IDs while physical LanceDB tables and storage locations remain service-owned. The changes address several issues flagged in earlier review rounds —
|
| Filename | Overview |
|---|---|
| nemo_retriever/src/nemo_retriever/common/vdb/lancedb_collections.py | New 1211-line LanceDB-backed catalog for collection/document lifecycle. Key concern: _collection_write_lock is a global lock that serializes all collection writes regardless of target collection. Reader/writer lease mechanism is sound; reconciliation scan pattern is correct. |
| nemo_retriever/src/nemo_retriever/service/routers/collections.py | New gateway proxy router forwarding collection/document management requests to VectorDB. Path parameters typed as str instead of the validated CollectionName alias, missing early validation at the gateway boundary. |
| nemo_retriever/src/nemo_retriever/service/vectordb_app.py | Major refactor: VDB operations moved behind a contract-based VDB interface; collection CRUD endpoints and metrics added. Health endpoint now returns 503 on transient backend exceptions, a behavioral change from the always-200 old implementation. |
| nemo_retriever/src/nemo_retriever/service/auth.py | Added ScopeAuthorizer with multi-token/scope file support; fixed the previously reported 401/404 oracle. Internal token path added for worker/VectorDB authentication. |
| nemo_retriever/src/nemo_retriever/service/services/job_tracker.py | Added scope, idempotency map, document manifest, and register_document_idempotent; idempotency-key ownership check in _drop_job_locked addresses the previously reported memory leak. |
| nemo_retriever/src/nemo_retriever/service/client.py | Added collection/document lifecycle methods and _scope header support. Most async methods now have explicit typed signatures, but aupdate_collection still uses **changes: Any. |
| nemo_retriever/src/nemo_retriever/common/schemas/collections.py | New shared wire models. CollectionName and DocumentId use a strict pattern addressing the previously reported SQL-injection concern. Models are well-structured. |
| nemo_retriever/src/nemo_retriever/common/schemas/requests.py | Added manifest, idempotency, and collection fields to JobCreateRequest. _validate_job_contract enforces operation-specific constraints correctly. collection_name now uses the validated CollectionName type. |
| nemo_retriever/src/nemo_retriever/service/config.py | Added scope/token/internal-VDB-token configuration; allow_unscoped_dev default is now False (fail-closed). Secrets loaded from environment variables and secret files correctly. |
| nemo_retriever/src/nemo_retriever/service/routers/ingest.py | Extended with collection-aware document handling; _resolve_stable_document_id correctly uses attempt_id for legacy jobs. _prepare_job_work_item consolidates shared upload logic cleanly. |
Reviews (27): Last reviewed commit: "fix: reconcile agentic queries with VDB ..." | Re-trigger Greptile
d2a7419 to
3833c68
Compare
jperez999
left a comment
There was a problem hiding this comment.
Lets do this in multiple passes
Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
The gateway-forwarded upload path has been unreachable since work moved to pull scheduling, so its X-Gateway-* headers, spec recovery and worker-side fallbacks are removed rather than left as untested branches. With a single admission path remaining, the five loose collection fields on WorkItem collapse into one DocumentWriteContext that crosses the broker intact. This replaces the getattr defaults that were silently absorbing a missing context across the process boundary, and lets the executor take one argument where it previously reconstructed the same values from six. IngestOperation becomes a str enum so backends can compare by identity instead of by string. CollectionWriteContext coerces in __post_init__ because it is a frozen dataclass, not a Pydantic model, and would otherwise hold a raw wire string that silently failed every identity comparison. The wire contract is unchanged: the enum still parses and serialises as "append"/"replace", and the job idempotency fingerprint is byte-identical. Also drops the /page broker payload, whose source_document_id and page_number were written but never read and were discarded on reconstruction because RichModel ignores unknown keys. Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Every collection, document and job operation carried a near-identical synchronous and asynchronous copy. Keeping two copies in step is how query() and aquery() drifted to different top_k defaults without any test noticing, and nine of the eleven async methods had no coverage at all. Each operation now has a single async implementation, with the synchronous method as a thin facade over it. The facade hands the coroutine to a worker thread when a loop is already running, so synchronous callers inside async code keep working exactly as they did under the blocking httpx.Client. The top_k asymmetry is deliberately preserved and pinned by a signature test: query() requires it, aquery() defaults to 10, and both are released API. Parity tests now cover all nine previously untested async methods. _arequest keeps building its client inside the coroutine, since the facade may run it on another thread's loop; a comment records that constraint so a future connection-pooling change does not break the bridge. Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
JobTracker.get_idempotent_job and ScopeAuthorizer.credentials_required had no production callers. Idempotent replay and conflict rejection already run inside register_job, so the tracker test now drives them through the path the routers actually use. Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
The delete path and the expiry sweeper each open the deletion phase with a byte-identical row update; both now call _mark_collection_deleting_locked so the durable phase transition is defined once. _table_capabilities and _resolve_effective_retrieval_mode also re-checked table existence that their only caller already establishes under the write lock, so each query repeated two list_tables round trips. Dropping the redundant probes lets _table_capabilities return a non-optional result. Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
jperez999
left a comment
There was a problem hiding this comment.
This looks good to me, aside from the greptile comments that have not been resolved. I had a few questions about the abstract_methods decorator not being used for the new function in adt_vdb. We should probably require those, so everything is on the new setup.
e6e6914 to
e2e3009
Compare
Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Collection and document operations were optional capabilities that raised UnsupportedVDBOperation from default implementations, which let a backend claim the collection API while silently supporting none of it. They are now abstract, so an incomplete backend fails at construction instead of at the first request. UnsupportedVDBOperation itself remains for genuinely unsupported retrieval modes; reconcile_collections() and health() stay optional with safe defaults. Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Scope enforcement now defaults to on, so fixtures that build a ServiceConfig directly must request allow_unscoped_dev explicitly rather than inheriting a permissive default. Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
create_job() validated the target collection and the replacement document with two byte-identical httpx blocks, each followed by its own copy of the upstream response relay, and built the idempotency fingerprint inline. Three private helpers now cover those concerns: _vectordb_get() for a scoped read, _proxied_response() for the verbatim relay, and _job_idempotency_fingerprint() for the explicit field allowlist. The relay helper also replaces the identical block in the answer-generation route. Behavior is unchanged: same scope and internal-auth headers, same 30s timeout, same 502 details and exception chaining, and non-200 upstream responses are still relayed verbatim rather than reshaped into a gateway error envelope. Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
e2e3009 to
f3a0b41
Compare
Description
This PR adds collection and document lifecycle management to the NeMo Retriever service. It provides a logical, scope-isolated API for creating collections, ingesting and replacing documents, tracking asynchronous jobs, querying collection content, and cleaning up resources without exposing physical LanceDB table names or storage locations to clients.
What changed
Why
Applications that want to integrate NeMo Retriever (like aiq) need a stable service boundary for managing durable knowledge collections. Previously, consumers had to depend on ingestion-specific behavior or physical VectorDB details.
This change establishes a logical API boundary where:
Validation
Focused regression testing was run from commit
edfed55da:The focused coverage includes:
The collection workflow was also exercised end to end with collection creation, asynchronous ingestion, job polling, document listing, retrieval, deletion, cleanup, and persistence across service recreation.
Out of scope
The separate TXT/HTML tokenizer fix is intentionally not included in this PR. The integrated validation environment contained that independent patch, but this PR does not modify the tokenizer implementation.
Checklist