ir.synopsis¶
Synopsis surfaces — LLM-derived summaries as an indexed surface (report 12).
The document-summary-index / collapsed-tree fuel (ADR #43): run a summarizer
over each artifact at build time to produce a short synopsis, index it as a
"synopsis" surface, and let the collapsed-tree policy (ir.traverse())
route a synopsis match down to that artifact’s chunks. Build-time cost, ≈free at
query time — and incremental, so only new / changed artifacts are re-synthesized.
with_synopsis() wraps any IndexingStrategy and adds
one synopsis surface per artifact:
strat = ir.with_synopsis(ir.Chunked(), synthesize=my_summarizer)
corpus = ir.build(ir.CorpusSource.from_mapping(docs, name="d", strategy=strat))
hits = ir.traverse(q, corpus, policy=ir.collapsed_tree_policy()) # routes via synopsis
The synopsis is prepended (plan position 0) so it is the first summary
surface — hence the collapsed-tree router (on with_synopsis(Package()) the
synopsis, not the terse description, routes). An empty synopsis is dropped, so
a synth that returns "" simply leaves the artifact with its other surfaces.
synthesize: Callable[[Artifact], str] is injectable (a test double, or
your own summarizer); omitted, it is built lazily on aix (the
multi-provider LLM facade) via make_llm_synthesizer() (the make_llm_*
idiom — import ir stays offline, aix is imported only on the first
synthesis).
Staleness. The wrapper exposes its identity as scalar attributes
(synthesizer_id, synopsis_kind) and holds the inner strategy, so
ir.index._strategy_id() (which recurses into nested strategies) folds both
the inner strategy’s parameters and the synthesizer identity into the corpus’s
strategy_id. A prompt / model change — or an inner-strategy change — therefore
re-synthesizes exactly the affected artifacts, the same way an embedder_id
change does; no silent staleness. (An injected unnamed synthesizer — a lambda
or a local closure — has no stable identity to fold in, so with_synopsis
warns and disables its staleness tracking unless you pass synthesizer_id=.)
Routing needs no edges. collapsed-tree descends synopsis→chunks within an
artifact via ir.retrieve.records_for_artifact() (shared artifact_id), so
synopsis routing works with no entries in the ir.graph links view —
that view models cross-artifact edges (REF / PARENT between artifacts), a
different grain than surface→surface within one artifact.
Conditional synopsis. when= (default "always") restricts synthesis to the
artifacts a predicate selects, so LLM cost is paid only where a synopsis helps —
e.g. with_synopsis(ir.Package(), when=thin_description_predicate) synthesizes
only for packages whose [project].description is empty or short (their
README is the only topical signal), and skips the rest *without calling the
synthesizer*. The predicate’s identity folds into strategy_id (when_id)
like the synthesizer’s, so changing it re-processes the corpus (strategy_id is
corpus-wide) and re-synthesizes every artifact the new gate selects.
**Caveat — edge_extractor.** Eager edge ingest (build(edge_extractor=...))
calls decompose for every artifact each build, and synthesis lives in
decompose; so combining with_synopsis() with an edge_extractor
re-runs synthesis on every rebuild. The common path — synopsis routing with no
edge_extractor — stays fully incremental.
Module Attributes
an |
|
an |
|
|
|
Descriptions shorter than this many characters count as thin by default. |
|
The surface kind a synopsis is indexed under — a member of |
|
Default prompt for |
|
A topical / classification-oriented alternative to |
|
|
Ready-made gate: synthesize only for packages with an empty / short description. |
Functions
|
An LLM-backed |
|
A |
|
Ready-made gate: synthesize only for packages with an empty / short description. |
|
Wrap strategy to add one LLM-derived |
- ir.synopsis.ALWAYS = 'always'¶
whenvalue meaning “synthesize for every artifact” (the default).
- ir.synopsis.DFLT_THIN_DESCRIPTION_CHARS = 40¶
Descriptions shorter than this many characters count as thin by default.
- ir.synopsis.SYNOPSIS_KIND = 'synopsis'¶
The surface kind a synopsis is indexed under — a member of
ir.traverse.DFLT_SUMMARY_KINDS, so collapsed-tree routes from it.
- ir.synopsis.SYNOPSIS_PROMPT = 'Write a concise synopsis (2-4 sentences) of the document below: what it is about and what questions it answers, so that a search over synopses can route to it. Output only the synopsis, no preamble.\n\nDocument:\n{text}'¶
Default prompt for
make_llm_synthesizer()(a routing-oriented summary).
- ir.synopsis.Synthesizer¶
an
Artifact→ its synopsis text (""to skip — no synopsis surface is added for that artifact).- Type:
A synthesizer
- ir.synopsis.TOPICAL_SYNOPSIS_PROMPT = 'Write a concise synopsis (2-3 sentences) of the software package described below: which domain or problem class it belongs to (e.g. audio, graphs, http clients, data validation) and what it is used for, so that a search for that domain finds it. Output only the synopsis, no preamble.\n\nPackage:\n{text}'¶
A topical / classification-oriented alternative to
SYNOPSIS_PROMPT, suited to thin-description packages (opt-in:make_llm_synthesizer(prompt=...)).
- ir.synopsis.make_llm_synthesizer(*, summarize=None, prompt='Write a concise synopsis (2-4 sentences) of the document below: what it is about and what questions it answers, so that a search over synopses can route to it. Output only the synopsis, no preamble.\\n\\nDocument:\\n{text}', model=None, synthesizer_id=None, text_key=None, text_fields=None, **prompt_function_kwargs)[source]¶
An LLM-backed
Synthesizer(Artifact→ synopsis).summarizeis an injectabletext -> strcallable (a test double, or your own summarizer); when omitted it is built lazily onaix(aix.prompt_func) on the first synthesis and reused — so importing this module, and even constructing the synthesizer, stays offline. The artifact’s text is extracted withir.strategy.text_of()usingtext_key— whichwith_synopsis()threads from the inner strategy, so the synopsis summarizes the same field the strategy indexes.text_fields(mutually exclusive withtext_key) names the fields of a mapping artifact whose text is sent to the LLM, and only those: without it, a mapping with notext/text_keyhas every string field joined and sent (seeir.strategy.text_of()) — including, for aPackagerecord, its localpath.with_synopsisthreads the inner strategy’ssynopsis_fields(Packagedeclares("name", "description", "readme")), so the Package default never leaks the path. The fields fold into the defaultsynthesizer_id, so changing them re-synthesizes. An empty text, or any synthesis error, yields""(the surface is then skipped, never a fabricated summary).The returned callable carries a
synthesizer_idattribute (default"aix:{model}:{sha(prompt)[:12]}") thatwith_synopsis()reads into the corpus’sstrategy_idfor staleness — a prompt or model change re-synthesizes.
- ir.synopsis.make_thin_description_predicate(*, min_chars=40, field='description')[source]¶
A
Predicatetrue when the artifact’sfieldis empty or thin.Reads
artifact.raw[field](aPackagerecord’sdescription); a raw that is not a mapping is a thin source when its text is shorter thanmin_chars. Whitespace is ignored. The returned callable carries awhen_idstamp so the threshold folds intostrategy_id.>>> thin = make_thin_description_predicate(min_chars=10) >>> thin(Artifact(id="a", raw={"name": "a", "description": ""})) True >>> thin(Artifact(id="b", raw={"name": "b", "description": "Tools to parse dates"})) False
- ir.synopsis.thin_description_predicate(artifact)¶
Ready-made gate: synthesize only for packages with an empty / short description.
- Return type:
- ir.synopsis.with_synopsis(strategy, *, synthesize=None, synthesizer_id=None, synopsis_kind='synopsis', when='always', when_id=None)[source]¶
Wrap strategy to add one LLM-derived
synopsissurface per artifact.- Parameters:
strategy (
IndexingStrategy) – the innerIndexingStrategy(Chunked,Package, …). Its surfaces are kept; the synopsis is prepended.synthesize (
Optional[Callable[[Artifact],str]]) – an injectableArtifact -> str(test double / custom summarizer). Omitted →make_llm_synthesizer()(lazyaix).synthesizer_id (
str|None) – explicit identity stamp for staleness (recommended when injecting an unnamed callable / lambda). Omitted → the synthesizer’s ownsynthesizer_id/__qualname__.synopsis_kind (
str) – the surface kind (default"synopsis", a summary kind).when (
Union[str,Callable[[Artifact],bool]]) –"always"(default: every artifact) or a predicateArtifact -> bool; the synthesizer is called, and a synopsis surface added, only where it returns true (e.g.thin_description_predicate). A predicate that raises propagates.when_id (
str|None) – explicit identity stamp for an unnamed predicate (staleness); omitted → the predicate’swhen_id/__qualname__.
- Return type:
- Returns:
an
IndexingStrategyusable anywhere a strategy is —ir.CorpusSource.from_mapping(docs, name=..., strategy=with_synopsis(...)).
>>> strat = with_synopsis(Chunked(), synthesize=lambda a: "a summary")