feat(rag): productionize the RAG pipeline (real LLM, real vector stores, async, quantization, eval, facade wiring)#1896
feat(rag): productionize the RAG pipeline (real LLM, real vector stores, async, quantization, eval, facade wiring)#1896ooples wants to merge 25 commits into
Conversation
…ing) RAG previously shipped no real LLM generator — only a local-LSTM NeuralGenerator and a StubGenerator — so every LLM-dependent component (HyDE, LLM/Cohere rerankers, LLM context compression, LLM query expansion, multi-query retrieval, GraphRAG summarization, LLM-judge eval) had nothing real to call. Add ChatClientGenerator<T> : GeneratorBase<T>, IStreamingGenerator<T> that bridges the RAG IGenerator<T> contract onto the existing production agentic IChatClient<T> connectors (OpenAI/Azure/Anthropic/Cohere/Gemini/Mistral/Ollama — real HTTP, retries, timeouts, streaming). Generate() returns the full answer; the new IStreamingGenerator<T>.GenerateStreamAsync yields token deltas. Reuses the battle-tested connectors instead of a new HTTP client. Unit-tested with a fake in-memory IChatClient (no network): role construction, option propagation, streaming concatenation, and prompt validation. Builds on net471/net8/net10 (IAsyncEnumerable streaming verified on net471). Unblocks #19, #25, #34. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
HyDE, LLM query expansion, multi-query retrieval, LLM reranking, and LLM context
compression previously ran lexical templates/heuristics while accepting unused
endpoint/apiKey args — they looked LLM-powered but never called a model.
Add a small non-generic ITextGenerator (string Generate(string)) that IGenerator<T>
now extends, so any real generator (e.g. ChatClientGenerator over a chat connector)
plugs into both the generic and non-generic RAG helpers. Each component gains an
optional generator:
- HyDEQueryExpansion: LLM writes hypothetical answer passages (true HyDE).
- LLMQueryExpansion: LLM produces alternative queries, parsed line-by-line.
- MultiQueryRetriever: reuses the de-stubbed expander (no more "{query} variation N").
- LLMBasedReranker: LLM scores each doc 0-10 for relevance.
- LLMContextCompressor: LLM extracts only query-relevant content.
When no generator is supplied each keeps a clearly-documented offline heuristic
fallback (non-breaking). Verified with a fake ITextGenerator (6 tests).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the in-memory brute-force simulations in QdrantDocumentStore<T>
and PineconeDocumentStore<T> with real HTTP clients that call the
Qdrant and Pinecone REST APIs. Both accept an optional HttpClient /
HttpMessageHandler for testability, support API-key auth + base URL,
translate metadata filters, and bridge the sync IDocumentStore methods
to async internally (matching ElasticsearchDocumentStore). The
IDocumentStore<T> contract is unchanged.
Adds mocked-HttpMessageHandler unit tests (no network) and gated
[Trait("Category","Integration")] tests driven by env vars.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ievers HybridRetriever summed raw dense (cosine ~0..1) and sparse (BM25, unbounded) scores, so the larger-scaled list dominated. Switch it to Reciprocal Rank Fusion (rank-based, scale-free — the default in ES/Weaviate/Qdrant and LangChain/LlamaIndex), keeping the weights and adding a configurable RRF k (default 60). Add three retrievers that were absent versus the common frameworks: - EnsembleRetriever<T>: RRF fusion over any number of weighted retrievers. - QueryRoutingRetriever<T>: routes a query to the best named data source via an LLM (ITextGenerator) with a token-overlap fallback. - SelfQueryRetriever<T>: uses the generator to split an NL query into a search string plus structured metadata filters over declared fields, then delegates (caller filters win). Malformed LLM JSON degrades to the raw query. Verified with fakes (6 tests): RRF ordering, ensemble, routing (LLM + fallback), and self-query filter extraction/precedence. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the in-memory simulations for WeaviateDocumentStore, MilvusDocumentStore and AzureSearchDocumentStore with real HTTP clients following the Qdrant/Pinecone pattern (HttpClient + Newtonsoft, sync *Core overrides bridging to a private async SendAsync, net471-safe). Adds mocked-HttpMessageHandler unit tests plus one gated integration SkippableFact per store. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
RAG chunking was character-based only, but LLM context/embedding limits are measured in tokens. Add TokenBasedChunkingStrategy: ChunkSize/ChunkOverlap are interpreted as tokens, with an injectable token-counter delegate so callers can plug an exact tokenizer (tiktoken/HF); the default counts whitespace words as an approximate proxy. Overlap and start/end positions preserved. (Remaining in #26: real embedding-based semantic chunking + LLM agentic chunking.) Verified: 4 tests (budget, overlap, position round-trip, custom counter). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add OnnxCrossEncoderReranker<T> that loads a cross-encoder ONNX model (e.g. BGE-reranker / ms-marco MiniLM) plus its tokenizer and scores each (query, document) pair by joint [CLS] q [SEP] d [SEP] inference, sorting descending and setting RelevanceScore/HasRelevanceScore. Batches pairs, disposes the session, and throws a clear FileNotFoundException on a missing model (no silent lexical fallback). Adds a convenience CrossEncoderReranker<T> ctor that accepts it as the scorer. Ranking logic is unit-tested via a virtual ScorePairs seam; a gated integration test loads a real model from an env var and skips when unset. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…R/MAP) Upgrade RAG evaluation beyond lexical overlap: - FaithfulnessMetric: optional ITextGenerator NLI-style judge (supported claims / total), lexical word-overlap fallback when none supplied. - AnswerSimilarityMetric: optional IEmbeddingModel cosine, Jaccard fallback. - AnswerCorrectnessMetric: optional ITextGenerator (0-10 judge) + IEmbeddingModel (semantic cosine), averaged; Jaccard fallback. Back-compat (endpoint, apiKey) ctor retained. - ContextRelevanceMetric: optional IEmbeddingModel query-vs-doc cosine, Jaccard fallback. - New AnswerRelevanceMetric (RAGAS): reverse-generate questions then mean cosine to query. - New ContextPrecisionMetric (RAGAS AP-style) and ContextRecallMetric (claim attribution), each judge/embedding/lexical. - New RetrievalMetrics static class: pure-math nDCG@k (binary + graded), MRR, MAP, Hit-Rate@k, Precision@k, Recall@k over ranked doc ids vs relevant set. - Shared helpers added to RAGMetricBase (sentence split, list parsing, yes/no verdict, cosine, Jaccard). net471-safe (log2 via change-of-base; guards, no ThrowIfNull/IsFinite). All metrics fall back to documented offline lexical behavior when no model is injected. Existing IRAGMetric metrics flow through RAGEvaluator unchanged. Tests (18, net10.0 green): IR metrics with exact numeric assertions; embedding metrics with a controlled fake IEmbeddingModel; LLM-judge metrics with a scripted fake ITextGenerator; all offline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SemanticChunkingStrategy was "semantic" in name only — it packed sentences to a size budget with no embeddings. It now, when given a sentence embedder, places chunk boundaries at semantic breakpoints (consecutive-sentence cosine distance above a percentile threshold — Kamradt/LlamaIndex SemanticSplitter), while still capping chunk size; without an embedder it falls back to the original size-based packing. Sentence spans are located in the source text so positions round-trip. Verified: 3 tests (topic-shift split, no-embedder fallback, position round-trip). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…index Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a decorator that caches embeddings by SHA-256 content hash so repeated texts are not re-embedded, matching LangChain CacheBackedEmbeddings and LlamaIndex ingestion cache. - ContentHash: stable, cross-process SHA-256 hex helper (reusable for dedup) - IEmbeddingCache<T> + thread-safe InMemoryEmbeddingCache<T> (ConcurrentDictionary, optional max-size / approximate-LRU eviction) - CacheBackedEmbeddings<T> : IEmbeddingModel<T> decorator; keys namespaced by inner model identity + dimension; batch paths dedup within/across calls, invoke inner only for distinct misses, and preserve row ordering - net471/net8.0-safe; 18 unit tests with a counting fake inner model Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…reports Add optional ITextGenerator (IGenerator<T>)-driven extraction to KGConstructor and LLM community reports to CommunitySummarizer, with the existing regex/ extractive behavior retained as a documented offline fallback. - KGConstructor: new ctor accepting IGenerator<T>; prompts the model for structured JSON (entities/relations, optional claims), parses with Newtonsoft.Json, and builds the graph. Malformed/empty JSON or generator errors transparently degrade to the regex path. Descriptions persisted on nodes/edges; claims attached to subject nodes. - CommunitySummarizer: new ctor accepting IGenerator<T>; generates natural- language community reports, falling back to the extractive template when no generator is present or the response is empty. - Options: UseLlmExtraction (default true), ExtractClaims (default false). - net471-safe (guards, Newtonsoft.Json, no ThrowIfNull/double.IsFinite). - Tests: 9 CI-runnable tests with a scripted fake generator covering LLM population, malformed-JSON fallback, markdown-fenced JSON, claims, and LLM-vs-extractive community reports. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the fake in-memory PostgresVectorDocumentStore/RedisVLDocumentStore in the core library with real IDocumentStore<T> implementations in the opt-in storage projects, mirroring ElasticsearchDocumentStore. - AiDotNet.Storage.Postgres: PostgresVectorDocumentStore<T> using Npgsql + pgvector (CREATE EXTENSION vector, table with vector(dim) column, ON CONFLICT upsert, KNN via <=>/<->/<+> ORDER BY LIMIT, jsonb metadata filters). - AiDotNet.Storage.Redis: RedisVLDocumentStore<T> using StackExchange.Redis RediSearch (FT.CREATE HNSW vector + TAG/NUMERIC fields, HSET, FT.SEARCH KNN with server-side filters, FT.DROPINDEX). Honors metadata filters and never mutates cached docs. - Delete the two fake in-memory stores from the core library (breaking change). - Add unit tests for the SQL/Redis query builders and gated integration tests (POSTGRES_VECTOR_CONN / REDIS_CONN). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…uilder facade Make the RAG pipeline wireable through the facade (your "100% wireable" goal): - ConfigureRetrievalAugmentedGeneration gains chunkingStrategy + contextCompressor params/fields, surfaced on AiModelResult and copied in BuildPipeline. - Fix the documentStore-dropped-without-a-KnowledgeGraph bug: the store is now kept in its own field, exposed as AiModelResult.DocumentStore, and a default vector retriever is built over it (honoring the configured embedding model + similarity metric) so vector-only RAG works. - New ConfigureVectorStore(IDocumentStore) and ConfigureVectorIndex(VectorIndexKind, metric) — one-call paths making all document-store backends and the in-memory indexes (Flat/HNSW/IVF/LSH) reachable. These live on both IAiModelBuilder and the concrete builder (every Configure* belongs on the interface). - ConfigureRAG(RAGConfiguration) materializes the previously-orphaned RAGConfigurationBuilder into the builder fields. Verified: 10 facade-wiring tests. YAML/declarative path is the follow-up (#31). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The generated YAML applier previously treated ConfigureRetrievalAugmentedGeneration
as a plain interface section: it kept only the retriever (the method's first
parameter) and silently dropped reranker, generator, query processors, document
store and graph store; ConfigureKnowledgeGraph was emitted as an Action<> TODO.
Rework the YamlConfigSourceGenerator so the RAG section is a COMPOSITE:
- Emit a new YamlRagSection POCO (retriever, reranker, generator,
queryProcessors[], documentStore, graphStore, embeddingModel,
similarityMetric, knowledgeGraph) and surface it as the
RetrievalAugmentedGeneration YAML property.
- Emit a dedicated applier that resolves each named sub-component from its own
type-registry section and passes them to the facade: ConfigureRetrievalAugmentedGeneration
(retriever/reranker/generator/queryProcessors/graphStore/documentStore),
ConfigureEmbeddingModel, ConfigureSimilarityMetric, and ConfigureKnowledgeGraph.
- Wire the standalone KnowledgeGraph section (Action<KnowledgeGraphOptions>) as an
options params-bag, removing the KG TODO; pipeline-style Action<> sections
(preprocessing/postprocessing) keep their existing steps shape.
- Mark IQueryProcessor [YamlConfigurable("QueryProcessor")] so the registry exposes
a QueryProcessor section for the composite applier.
- Add matching JSON-schema/docs handling for the composite and options-bag sections.
Adds RagYamlCompositeTests: a multi-component RAG YAML string loads, applies to a
builder, and every sub-component (reranker/generator/queryProcessors/graphStore/
documentStore/similarityMetric/embeddingModel) plus ConfigureKnowledgeGraph is
verified wired — nothing dropped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… concrete Pre-existing failure on this branch: the test asserted ConfigureAutoML(IAutoMLModel<...>) must NOT be on IAiModelBuilder, but every fluent Configure* method belongs on the interface (it's declared there at IAiModelBuilder.cs:1507). Flip the assertion to require the overload on BOTH the interface and the concrete builder, and rename the test to match (same fix applied on the #1789 branch). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add async, cancellation-aware methods to the RAG interfaces and make the I/O-bound implementations genuinely non-blocking. Interfaces (kept sync methods, added async overloads): - IRetriever<T>.RetrieveAsync(query, topK, filters, ct) - IDocumentStore<T>: Add/AddBatch/GetSimilar/GetSimilarWithFilters/GetById/ Remove/Clear/GetAll -> *Async(ct) - IReranker<T>.RerankAsync (all + topK overloads) - IContextCompressor<T>.CompressAsync - IGenerator<T>.GenerateAsync / GenerateGroundedAsync Base classes implement the async interface methods and expose a protected virtual *CoreAsync that defaults to Task.FromResult(syncCore), so CPU-bound subclasses need no changes; I/O-bound subclasses override the core for real async. All library async uses ConfigureAwait(false); net471-safe (Task-based). Truly-async (removed sync-over-async on the async path): the external vector stores Qdrant/Pinecone/Weaviate/Milvus/AzureSearch/ChromaDB and the Elasticsearch store (token threaded into the HTTP client), the LLM-backed ChatClientGenerator, and the VectorRetriever/DenseRetriever (await the embedding model + store). CPU-bound impls (in-memory stores, RRF/MMR/identity rerankers, extractive compressors, Stub/Neural generators) wrap sync. Updated all direct interface implementers (VectorIndexDocumentStore, Stub/NeuralGenerator) and 17 test mock classes. Added focused async + pre-cancelled-token tests (RagAsyncCancellationTests, 12 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… variations Seven pre-existing HybridRetrieverTests/MultiQueryRetrieverTests asserted the old behavior that the RRF-fusion (#27) and real-query-expansion (#19) changes correctly replaced: exact weighted-sum scores (0.7*d+0.3*s) and the "{query} variation N" placeholder. RRF is rank-based (scores are small, order is what matters) and MultiQuery now emits real reformulations. Reassert the meaningful properties: fused doc presence + ranking, and original-query-first with distinct variations. All 105 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The RAG subsystem had every component but no single class that composed them — callers wired chunk→embed→store and retrieve→rerank→compress→generate by hand. Add RagPipeline<T>: - IngestAsync: optional chunk → embed → upsert (returns chunk count). - QueryAsync: retrieve → optional rerank → optional compress → optional generate, returning a RagResult<T> (contexts + grounded answer). Fully async/cancellation-aware. Optional tenant/namespace: stamped on ingested documents' metadata and applied as a retrieval filter for cross-tenant isolation. Streaming is already provided by IStreamingGenerator/ChatClientGenerator. Verified: 3 tests (chunked ingest into a real InMemoryDocumentStore, query→generate, tenant stamp + filter). Remaining #33 items — rich boolean/$in/range metadata filters across every external store, and RAG-path GPU hooks — are larger enhancements tracked as follow-ups. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
c4797ad to
0ec5e8e
Compare
Commit messages auto-fixedOne or more commit messages did not follow Conventional Commits, so they were rewritten to comply (subject case, header length ≤ 100, valid type). Each commit and its diff were preserved — no squashing. The branch was force-pushed with the corrected messages. If you have local work on this branch, run |
|
Important Review skippedToo many files! This PR contains 156 files, which is 6 over the limit of 150. To get a review, narrow the scope: Upgrade to Pro+ to raise the limit. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (158)
You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
Full RAG-suite verification on the integrated branch (net10.0): 2100 passed, 0 failed, 8 skipped (the 8 skips are the gated Still-open gaps to actually exceed every competitor (deliberately deferred, not done): native FAISS, Neo4j/property-graph backend, rich boolean/ |
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
Add AiDotNet.Storage.Neo4j, an opt-in metapackage implementing IGraphStore<T> over the Neo4j C# driver so GraphRAG can run on a production property graph. Nodes map to labeled Neo4j nodes, edges to relationships; upserts use MERGE and traversal uses Cypher MATCH. All per-request data is passed as $parameters; the only literal identifiers (base node label, relationship type) are validated and backtick-escaped once at construction, so there is no injection surface. Pure Cypher/param/mapping logic is factored into Neo4jCypher for unit testing. net10.0-only (the driver's TFM), matching the Postgres/Redis storage projects; excluded from the core csproj glob and added to the solution + test project. Tests: 20 pure-logic unit tests plus gated [Trait(Category,Integration)] SkippableFact round-trip tests reading NEO4J_URI/NEO4J_USER/NEO4J_PASSWORD. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add GPU-accelerated brute-force vector search to the RAG vector-search path, matching FAISS-GPU / Milvus-GPU flat-search: all stored vectors are packed into a matrix and scored against the query in a single batched GPU Gemm, then top-k'd. - GpuVectorScorer: reuses the existing DirectGpuTensorEngine / IDirectGpuBackend abstraction (AiDotNetEngine.Current, GetBackend, AllocateBuffer, Gemm, DownloadBuffer) - no raw CUDA. Supports dot-product, cosine and Euclidean via closed forms over the GPU dot products + precomputed norms. - GpuFlatIndex<T> : IVectorIndex<T>: batched-GPU flat index with exact CPU fallback identical to FlatIndex ordering. - HNSWIndex.ScoreCandidatesGpu: batched GPU candidate re-scoring for a coarse-to-fine pipeline, with CPU fallback. - Falls back to CPU when no GPU/engine, below threshold, unsupported metric, or on any GPU error; gated off under AIDOTNET_DISABLE_GPU like the rest of the codebase. Compiles on net10.0/net8.0/net471 (GPU types ship on all TFMs). - Tests assert GPU-index top-k == CPU FlatIndex top-k across metrics and thresholds; deterministic and CI-runnable (GPU disabled -> CPU path). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Commit messages auto-fixedOne or more commit messages did not follow Conventional Commits, so they were rewritten to comply (subject case, header length ≤ 100, valid type). Each commit and its diff were preserved — no squashing. The branch was force-pushed with the corrected messages. If you have local work on this branch, run |
496b94b to
ba72d06
Compare
…ll stores
Adds a rich, composable metadata-filter model — Eq/Ne/Gt/Gte/Lt/Lte/In/Exists + And/Or/Not
with fluent factories — and an additive IDocumentStore.GetSimilarWithFilter[Async] that
every real store translates to its native filter language: Qdrant (must/should/must_not),
Pinecone ($and/$or/$in/$gte/…), Weaviate (where operators), Milvus (bool expr), Azure
(OData), pgvector (jsonb, parameterized), Elasticsearch (bool query), Redis (RediSearch);
InMemory/File use the base AST evaluator. Non-breaking: the old Dictionary filters remain,
and DocumentStoreBase provides an in-memory evaluator default so any store gets correct rich
filtering for free.
This matches/exceeds the Pinecone/Qdrant/Weaviate filter expressiveness (previously only
equality + one-sided >= range + any-of). Fixed two issues in the subagent's draft: a
string-vs-number comparison incorrectly ordinal-compared (Gt("name",5) "matched" "abc") — now
only two strings order against each other, otherwise not-comparable; and four sync filter
tests wrongly carried [Fact(Timeout)]. All 78 filter/translation/store tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Deployment failed with the following error: Learn More: https://vercel.com/franklins-projects-02a0b5a0?upgradeToPro=build-rate-limit |
Adds a real FAISS-backed IDocumentStore in a new opt-in storage project so
the core AiDotNet package takes no native dependency (mirrors the
AiDotNet.Storage.Elasticsearch/Sqlite extraction pattern).
- New net10.0-only project src/AiDotNet.Storage.Faiss referencing the FaissNet
managed wrapper (native Facebook AI Similarity Search). Wired into
Directory.Packages.props (central versioning), AiDotNet.sln, excluded from the
core csproj compile glob, and referenced net10-conditionally by the test project.
- FaissDocumentStore<T> supporting Flat / IVFFlat / HNSW / IVFPQ index types and
Cosine (normalized inner product) / InnerProduct / L2 metrics, selected via ctor.
- FaissSidecar maps FAISS int64 ids -> {docId, content, metadata, embedding};
drives id round-tripping, metadata filtering, index rebuild on delete for index
types without in-place removal (HNSW), and JSON persistence alongside the FAISS
index file (Save/Load).
- Metadata filtering via over-fetch (topK * oversample) + reuse of the base
DocumentStoreBase.MatchesFilters evaluator (FAISS has no native metadata filter).
- Kept the in-memory FAISSDocumentStore<T> (brute-force simulation) but documented
it clearly and pointed to the new native FaissDocumentStore<T>; no silent duplication.
- Tests: pure sidecar/id-mapping + over-fetch planner unit tests (no native); gated
[Trait Integration] tests that build a real Flat/HNSW index + search + persist,
self-skipping when the native lib can't load. IVF/IVFPQ training tests are opt-in
(AIDOTNET_FAISS_IVF=1) since FAISS k-means needs a complete Intel MKL runtime.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
"Exceed everyone" follow-ups now DONE (4 more commits)All four gaps from the earlier comment are implemented + pushed:
Final verification (net10.0): 2224 passed, 0 failed, 12 skipped (the 12 skips are the gated Remaining honest caveats: FAISS IVF/PQ needs a complete MKL runtime to exercise; live vector-DB/Neo4j clients are verified by mocked-HTTP shaping + gated integration tests, not continuous live CI runs. |
RAG productionization
Turns the RAG subsystem from a broad-but-partly-stubbed surface into a functional, production-oriented stack, and makes all of it reachable through the AiModelBuilder facade (code + YAML). Motivated by a competitive-parity audit (LangChain / LlamaIndex / Haystack / Semantic Kernel; Pinecone / Weaviate / Qdrant / Milvus / Chroma / pgvector / FAISS).
What changed (18 workstreams, one commit each)
Verification
NOT done — gaps vs. "exceed every competitor" (follow-ups, tracked not glossed)
🤖 Generated with Claude Code