feat: DedupVectorStore + LlmExtractor + FactExtractor (#40, #41) - #42
Merged
Conversation
Closes #41 and #40. ## cognis-rag: DedupVectorStore<S, F> Content-fingerprinting decorator over any VectorStore. Silently skips documents whose normalised text (lowercase + collapsed whitespace) has already been seen, so repeated agent observations never bloat the index. Generic over both the inner store (S: VectorStore) and the fingerprint function (F: Fn(&str) -> String), so callers can substitute any key derivation (document ID, composite hash, …) at zero runtime cost via monomorphisation. Key constructors: DedupVectorStore::new(inner) — default fingerprint DedupVectorStore::with_seen(inner, hashes) — restore across restarts DedupVectorStore::with_fingerprint(inner, f) — custom key function Public fn normalized_fingerprint(text) for callers that need to pre-compute hashes (e.g. seeding from a DB on startup). 15 unit tests covering dedup, case/whitespace normalisation, batch add with mixed duplicates, custom fingerprint, add_vectors, and persistence helpers. ## cognis: LlmExtractor<O> + FactExtractor LlmExtractor<O> — generic Runnable<String, O> for any O: DeserializeOwned + JsonSchema. Derives format instructions from the schema automatically and appends them to the system prompt; handles prose-wrapped JSON via StructuredOutputParser. Fully composable with .pipe() and other Runnable combinators. FactExtractor — specialisation of LlmExtractor that maps FactExtractionInput → Vec<Fact>. Parse failures are logged at WARN and swallowed (returns Ok(vec![])) so the memory write path never stalls on a badly-formatted model response. Configurable via builder for domain-specific prompts. New types: FactKind (Rule/Preference/Context/Decision/Observation), Fact { content, kind, importance }, FactExtractionInput with builder chain (.with_hint(), .with_max_facts()). 10 unit tests covering both extractors: structured parse, prose tolerance, parse error handling, max_facts truncation, render helpers, and builder chain. All types re-exported from cognis::agent and cognis top-level.
…tore demo
End-to-end example showing the two new primitives working together
against a real LLM (Ollama / qwen2.5:3b verified, any provider works
via env vars).
Four sections:
1. Session 1 — extract facts from a planning summary, store in
DedupVectorStore.
2. In-session dedup — re-add the same facts verbatim, all skipped.
3. Session 2 — extract facts from a follow-up session; new phrasing
creates new fingerprints (correct — text-based, not semantic).
4. Generic LlmExtractor<TechStack> — custom output struct extracted
from the same text, showing the generic primitive directly.
5. Persistence — fingerprints serialised and restored in a fresh
store; all facts correctly skipped on re-add.
Registered as memory_fact_extraction in crates/examples/Cargo.toml.
Run with:
COGNIS_PROVIDER=ollama COGNIS_OLLAMA_MODEL=qwen2.5:3b \
cargo run -p cognis-examples --example memory_fact_extraction
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements issues #40 and #41 as a single integrated feature pair — the two primitives that every persistent-memory system needs at write time.
DedupVectorStore<S, F>(cognis-rag) — content-fingerprinting decorator over anyVectorStore. Silently drops documents whose normalised text has already been seen. Generic over both the inner store and the fingerprint function so callers can supply a custom key at zero runtime cost.LlmExtractor<O>(cognis) — genericRunnable<String, O>for anyO: DeserializeOwned + JsonSchema. Derives format instructions from the JSON Schema automatically. The reusable building block underneathFactExtractor.FactExtractor(cognis) — ready-to-use specialisation ofLlmExtractorthat mapsFactExtractionInput → Vec<Fact>. Parse failures are swallowed (Ok(vec![])+ WARN log) so the memory write path never stalls on a badly-formatted model response.What's new
crates/cognis-rag/src/vectorstore/dedup.rsDedupVectorStore<S,F>,normalized_fingerprint— 15 testscrates/cognis/src/agent/fact_extractor.rsLlmExtractor<O>,FactExtractor,Fact,FactKind,FactExtractionInput— 10 testsexamples/memory/fact_extraction.rsAll types re-exported from
cognis::agentandcognistop-level.Design decisions
Generic fingerprint function —
DedupVectorStore<S, F>uses a type parameter for the fingerprint function rather thanArc<dyn Fn>so there is no heap allocation or vtable call on the hot add path.DedupVectorStore::newdefault-typesFtofn(&str) -> Stringwithnormalized_fingerprintas the value, keeping the simple case ergonomic.Restart persistence — the seen-set is in-memory only.
DedupVectorStore::with_seenand the publicnormalized_fingerprintfunction let callers pre-seed hashes from storage on startup;seen_fingerprints()exposes the set for serialisation on shutdown.Error-swallowing on
FactExtractor— intentional. The write path to a memory store must not block because of a chatty model response.LlmExtractorpropagates errors normally; onlyFactExtractorswallows them.LlmExtractoras public primitive — any extraction task (sentiment, entity, classification, custom schema) uses it directly without going throughFactExtractor. This was the generic-at-library-level requirement from the issue.Test plan
cargo test -p cognis-rag --lib vectorstore::dedup— 15/15cargo test -p cognis --lib agent::fact_extractor— 10/10cargo build --workspace— cleanCOGNIS_PROVIDER=ollama COGNIS_OLLAMA_MODEL=qwen2.5:3b cargo run -p cognis-examples --example memory_fact_extraction— verified end-to-end🤖 Generated with Claude Code
Summary by cubic
Adds write-time memory deduplication and a generic LLM-based extractor to prevent duplicate index entries and enable schema-driven extraction. Implements #40 and #41.
cognis-rag:DedupVectorStore<S, F>— drops duplicate documents by a content fingerprint; defaultnormalized_fingerprint(lowercase + whitespace collapse + FNV-1a); supports custom keys;with_seen/seen_fingerprints()for restart persistence; preserves output length by returningdedup:skipped:{fingerprint}for skipped items.cognis:LlmExtractor<O>—Runnable<String, O>that derives JSON Schema format instructions and parses prose-wrapped JSON; builder for prompt and parser config.cognis:FactExtractor—Runnable<FactExtractionInput, Vec<Fact>>producingFact { content, kind, importance }; swallows parse errors (returns[]) to keep memory writes non-blocking.cognis::agentand crate root; adds examplememory_fact_extractionshowingFactExtractor+DedupVectorStore.Written for commit 9ade2ab. Summary will update on new commits. Review in cubic