LangChain

langchain-coalent — your existing LangChain vector store, embeddings, and chat model become the substrate of a provenance-invalidated semantic cache. Four surfaces, BYO-first.

langchain-coalent New in v0.6.1 is Coalent as a LangChain-native freshness/reuse layer — BYO-first by construction: your existing LangChain vector store (or retriever), embeddings, and chat model become the substrate of a provenance-invalidated SemanticCache. Nothing about how you built them changes.

pip install langchain-coalent

It depends only on coalent>=0.6 and langchain-core>=0.3 — no langchain-community, no langgraph.

1. create_coalent_cache — one call over your stack

from langchain_coalent import create_coalent_cache

cache = create_coalent_cache(
    my_vectorstore,            # any VectorStore or BaseRetriever — unchanged
    llm=my_chat_model,         # any BaseChatModel, used as YOU configured it
    embeddings=my_embeddings,  # any Embeddings — keys the cache semantically
    # ...every Coalent knob passes through:
    # hit_threshold=..., serve_budget=..., key_floor=..., preset="multi_hop", ...
)

Recommended defaults are applied only where you left the knob unset:

  • read_path="pool" — the v0.6 measured operating point — whenever you supplied a semantic embedder (embeddings=, or a non-hashing Coalent embedder=). Without one the factory stays on the unit read path rather than guess.
  • pool_header — per-source attribution headers on the pool path (pass your own callable for [title | source | date] richness — the measured best recipe).
  • Behavioral knobs (residual_spans, query_keys, ...) stay opt-in, exactly as in Coalent itself.

Knob passthrough is total. **knobs forwards every SemanticCache constructor argument unchanged — the integration adds no allowlist between you and the library, so new library knobs work in create_coalent_cache the day they ship.

Your chat model is invoked as you configured it — the synthesizer's model / max_tokens / temperature are not forwarded (there is no portable kwarg contract across LangChain chat integrations). Temperature 0 on your model is recommended for the strict-JSON synthesis contract.

2. CoalentRetriever — the cache as a LangChain retriever

A drop-in BaseRetriever for any chain that takes a retriever:

from langchain_coalent import CoalentRetriever

retriever = CoalentRetriever(cache=cache)
docs = retriever.invoke("what is our leave policy?")

docs[0].page_content              # the served, attributed context payload
docs[0].metadata["read_id"]       # -> cache.report_refusal() / report_success()
docs[0].metadata["sources"]       # artifact ids behind the read (provenance)
docs[0].metadata["cache_hit"]     # True == served with zero LLM spend

include_evidence=True additionally returns the retained raw evidence chunks as separate Documents.

3. CoalentVectorStoreRetriever — your index as Coalent's substrate

Used internally by the factory; also available directly:

from langchain_coalent import CoalentVectorStoreRetriever
retriever = CoalentVectorStoreRetriever(my_vectorstore, k=6)   # a Coalent Retriever

Document → Chunk mapping: page_contentChunk.text; the artifact id (what cache.source_changed(...) keys on) resolves as metadata["artifact_id"]metadata["source"]Document.idmetadata["id"] → a deterministic chunk:<sha1(text)[:12]> fallback. Give your documents a source so invalidation has a stable identity. metadata["version"]Chunk.version; other metadata is not carried (Coalent's Chunk has no metadata dict).

4. The refusal loop — LangGraph-shaped

When your answerer refuses over a served payload, that refusal is evidence — hand the read_id back and Coalent serves the verbatim source excerpts the extraction missed, then confirms the recovery as a durable alternate key:

retrieve ──> synthesize ──(refused?)──> report_refusal ──> re-synthesize ──> report_success
    ^                └─(answered)──> done                        └─(still refused)──> done

As nodes and one conditional edge:

def synthesize(state):
    doc = state["docs"][0]
    answer = my_llm.invoke(prompt(doc.page_content, state["question"]))
    return {**state, "answer": answer, "read_id": doc.metadata["read_id"]}

def on_refusal(state):
    retry = cache.report_refusal(state["read_id"])       # attributed spans, or None
    if retry is None:
        return state
    answer = my_llm.invoke(prompt(state["context"] + "\n\n" + retry, state["question"]))
    if not is_refusal(answer):
        cache.report_success(state["read_id"])           # -> durable query key
    return {**state, "answer": answer}

examples/refusal_loop.py in the package is the runnable, fully offline demonstration — a plain conditional loop implementing the identical LangGraph pattern; its docstring shows the 1:1 StateGraph mapping (langgraph is deliberately not a dependency).

Freshness — the reason this exists

# Your ingestion pipeline noticed a document changed:
cache.source_changed("policy.md", text=new_text)   # surgical, provenance-keyed
# The very next retrieval that touches it rebuilds; untouched knowledge stays warm.

Compatibility

Tested against langchain-core 1.x; written against the stable core contracts (BaseRetriever._get_relevant_documents, VectorStore.similarity_search, Embeddings.embed_query/embed_documents, BaseChatModel.invoke), which are unchanged from 0.3 — so langchain-core>=0.3 is supported.

Next