A Synthesizer turns raw chunks into understanding. Where the Retriever fetches the source material, the Synthesizer reads it and writes a structured briefing — by default a query-independent list of atomic, source-grounded claims — using your LLM. It also notes which chunks it actually used; that record is the lineage that drives surgical freshness.
The contract
def synthesize(self, query: str, chunks: list[Chunk]) -> Synthesis:
...
Synthesis carries the understanding, the indices of the chunks it used, and an ok flag:
from coalent import Synthesis
Synthesis(
understanding={"summary": "...", "claims": [...], "facts": {...}},
used=[0, 2], # only these chunks become provenance -> precise invalidation
ok=True, # False = degrade, don't cache fabricated content
)
LLMSynthesizer (recommended)
The built-in choice for real understanding. It owns the envelope: it presents the candidate sources, requires strict JSON, and reads the model's own used citations to build precise provenance.
from coalent import SemanticCache, LLMSynthesizer, OpenAIProvider
cache = SemanticCache(retriever, LLMSynthesizer(OpenAIProvider(), model="gpt-4o-mini"))
What you get:
- Extractive understanding New in v0.4 — by default, a query-independent list of source-grounded
claims(alongsidesummary,entities,facts) that keeps every number. See below. - Precise provenance — only sources the model cited can invalidate the unit (a change to a retrieved-but-uncited source touches nothing).
- No garbage — a parse failure degrades (
ok=False): the unit keeps its raw evidence and is flagged, never serving fabricated text.
Extractive understanding — one unit, many questions
New in v0.4 By default LLMSynthesizer runs in extractive mode (extract=True). Instead of a question-shaped prose summary, it produces a query-independent list of atomic, source-grounded claims — one claim per fact, every number kept with what it measures, its unit, and any condition attached to it.
Two things follow:
- One unit answers many questions. Because extraction ignores the seed query, the same source yields the same unit no matter which query first built it — so one cached unit serves diverse later questions instead of only the one it was born for.
- It keeps every number. In our tests the prose default dropped roughly 40% of the numbers in a source; extraction copies values verbatim.
understanding["claims"]is the substance;understanding["summary"]may be terse or empty.
extract=True is the default, so there's nothing to pass:
from coalent import SemanticCache, LLMSynthesizer, OpenAIProvider
cache = SemanticCache(retriever, LLMSynthesizer(OpenAIProvider())) # extract=True by default
Extractive mode is strongest on structured / reuse-heavy sources — policies, pricing, specs, FAQs — where one document is asked many different questions. It's the foundation the rest of the v0.4 read path stands on: per-claim coverage, cross-unit recall, and atom selection all operate over these claims.
Escape hatch — v0.3 prose
Want the old query-grounded decision summary back? Pass extract=False:
LLMSynthesizer(OpenAIProvider(), extract=False) # v0.3 prose summary, keyed to the seed query
Upgrading from v0.3? Newly built units now cache claims, not prose — existing cached units are untouched until they rebuild. Downstream code that read understanding["summary"] for the substance should read understanding["claims"].
Tune the extraction
The extractive prompt is exported as EXTRACTIVE_INSTRUCTION, so you can start from it and adapt for your domain:
from coalent import LLMSynthesizer, OpenAIProvider, EXTRACTIVE_INSTRUCTION
synth = LLMSynthesizer(
OpenAIProvider(),
instruction=EXTRACTIVE_INSTRUCTION + " Prefer SI units and note the effective date of each rate.",
)
Passing your own instruction overrides extract — a custom instruction always wins. Build on EXTRACTIVE_INSTRUCTION when you want to keep the query-independent behavior while adding your own rules.
Your prompt, our envelope
You decide what understanding to produce; Coalent always wraps it with the sources, a strict-JSON contract, and the citation list. Pass your own instruction (a string, or a query -> str function) and the fields you want back:
synth = LLMSynthesizer(
OpenAIProvider(),
instruction=(
"Summarize the incident: likely root cause, blast radius, and the next "
"action to take. Be specific and cite the runbook steps you used."
),
fields=["summary", "root_cause", "blast_radius", "next_action"],
)
Coalent still injects the candidate sources and requires the model to cite the sources it relied on — so provenance is captured no matter what you ask for. The built-in default now extracts query-independent claims (see above) into summary / claims / entities / facts; a custom instruction like this one overrides the extractor to fit your domain.
Depth — completeness vs cost
New in v0.3 depth (0.0–1.0) dials how much the understanding captures, trading synthesis cost against how often a later query has to escalate to raw:
LLMSynthesizer(OpenAIProvider(), depth=0.8) # 0.0 terse · 0.5 balanced (default) · 1.0 exhaustive
Watch cache.stats()["escalation_rate"] to tune it — a high rate means the understanding is too thin for your queries, so raise depth.
Providers
A provider is a thin generate(*, model, system, user, max_tokens, temperature) -> str:
| Provider | Use | Notes |
|---|---|---|
StubProvider | dev & tests | deterministic, no network |
OpenAIProvider | production | coalent[openai], reads OPENAI_API_KEY |
AnthropicProvider | production | coalent[anthropic], reads ANTHROPIC_API_KEY |
from coalent import LLMSynthesizer, AnthropicProvider
synth = LLMSynthesizer(AnthropicProvider(), model="claude-haiku-4-5")
JSONPassthroughSynthesizer — structured data, no LLM
When your source is already structured — a REST/MCP tool returning JSON — there's nothing to summarize. JSONPassthroughSynthesizer treats the JSON as the understanding: no model call, no latency, no key. Objects become facts, other values become claims, and every chunk is cited so provenance and freshness still work exactly as with an LLM.
from coalent import SemanticCache, JSONPassthroughSynthesizer
cache = SemanticCache(tool_retriever, JSONPassthroughSynthesizer())
# a result like {"employee": "A", "annual_leave": 12} is cached as-is —
# and invalidated like a document when the tool result changes.
Reach for it when the data is the answer; use LLMSynthesizer when raw text needs to be understood. See the MCP & tool results example.
StubSynthesizer (no key)
For dev and tests, StubSynthesizer() returns the same structured shape deterministically — so the whole loop runs without an API key (the detail lives in the retained raw).
Build-side controls (v0.5)
New in v0.5 Two cache-level knobs shape what the Synthesizer sees at build time:
split_by_artifact(default off) — when retrieval mixes chunks from several artifacts, build one unit per source instead of one blended unit, so provenance and freshness stay per-source.source_fetcher(callable) — bring your own source-chunk fetcher for widened builds: whenwiden_chunks=Nis set and a miss-triggered build wants more of the dominant source than retrieval returned, Coalent calls it (or a duck-typedretriever.widen(artifact_id, limit=)) to read up to N chunks of that source. Widening never fires at ingest — see The gate ladder.
Custom synthesizers
Any object with synthesize(query, chunks) -> Synthesis works — wrap a local model, or return used=[i for i, _ in enumerate(chunks)] to cite everything. Set ok=False on failure to make Coalent degrade instead of caching bad output.
Next
- Context intelligence — how the structured understanding is projected to minimum context.
- Provenance & freshness — what the citations drive.