Skip to content

feat: DedupVectorStore + LlmExtractor + FactExtractor (#40, #41) - #42

Merged
0xvasanth merged 3 commits into
mainfrom
playful-persimmon
May 21, 2026
Merged

feat: DedupVectorStore + LlmExtractor + FactExtractor (#40, #41)#42
0xvasanth merged 3 commits into
mainfrom
playful-persimmon

Conversation

@0xvasanth

@0xvasanth 0xvasanth commented May 21, 2026

Copy link
Copy Markdown
Owner

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 any VectorStore. 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) — generic Runnable<String, O> for any O: DeserializeOwned + JsonSchema. Derives format instructions from the JSON Schema automatically. The reusable building block underneath FactExtractor.

  • FactExtractor (cognis) — ready-to-use specialisation of LlmExtractor that maps FactExtractionInput → 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

File What
crates/cognis-rag/src/vectorstore/dedup.rs DedupVectorStore<S,F>, normalized_fingerprint — 15 tests
crates/cognis/src/agent/fact_extractor.rs LlmExtractor<O>, FactExtractor, Fact, FactKind, FactExtractionInput — 10 tests
examples/memory/fact_extraction.rs End-to-end demo (Ollama-verified)

All types re-exported from cognis::agent and cognis top-level.

Design decisions

Generic fingerprint functionDedupVectorStore<S, F> uses a type parameter for the fingerprint function rather than Arc<dyn Fn> so there is no heap allocation or vtable call on the hot add path. DedupVectorStore::new default-types F to fn(&str) -> String with normalized_fingerprint as the value, keeping the simple case ergonomic.

Restart persistence — the seen-set is in-memory only. DedupVectorStore::with_seen and the public normalized_fingerprint function 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. LlmExtractor propagates errors normally; only FactExtractor swallows them.

LlmExtractor as public primitive — any extraction task (sentiment, entity, classification, custom schema) uses it directly without going through FactExtractor. This was the generic-at-library-level requirement from the issue.

Test plan

  • cargo test -p cognis-rag --lib vectorstore::dedup — 15/15
  • cargo test -p cognis --lib agent::fact_extractor — 10/10
  • cargo build --workspace — clean
  • COGNIS_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.

  • New Features
    • cognis-rag: DedupVectorStore<S, F> — drops duplicate documents by a content fingerprint; default normalized_fingerprint (lowercase + whitespace collapse + FNV-1a); supports custom keys; with_seen/seen_fingerprints() for restart persistence; preserves output length by returning dedup: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: FactExtractorRunnable<FactExtractionInput, Vec<Fact>> producing Fact { content, kind, importance }; swallows parse errors (returns []) to keep memory writes non-blocking.
    • Re-exports from cognis::agent and crate root; adds example memory_fact_extraction showing FactExtractor + DedupVectorStore.

Written for commit 9ade2ab. Summary will update on new commits. Review in cubic

0xvasanth added 3 commits May 21, 2026 23:11
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

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 5 files (changes from recent commits).

Re-trigger cubic

@0xvasanth
0xvasanth merged commit 9799300 into main May 21, 2026
7 checks passed
@0xvasanth
0xvasanth deleted the playful-persimmon branch May 21, 2026 17:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant