symfonic.capabilities.knowledge¶
knowledge ¶
Knowledge, document, attachment, and citation bridges (T3.2.3).
Seven things used to decide independently how retrieved and ingested content reached a model: a knowledge provider formatted its own citation lines, a document store built its own block, extractors returned text nobody bounded, OCR output went wherever the caller put it, multimodal normalisation and provider wire formatting each had their own idea of what a media type was, and a streaming filter stripped citation tags in a different package again.
This capability makes each of them a contribution instead: a declaration that states what it supplies and how far it may be trusted, and never where it goes. Position, budget, cache region, and the untrusted wrapper are decided once, by T3.2.1's compiler, over the whole set. The bridges here decide only what is theirs to decide — what a store returns, what a parser is allowed to see, and what may go back out on the wire.
Three deliverables live here:
- Knowledge bridge — :mod:
.retrieval. - Document and attachment bridge — :mod:
.documents, :mod:.attachments, :mod:.multimodal, :mod:.outbound. - Citation and output bridge — :mod:
.citations, :mod:.grounding.
with :mod:.contracts holding the declaration all three emit, :mod:.labels
holding the "untrusted name is a label, never an identifier" rule, and
:mod:.assembly holding the one-function seam to the prompt compiler.
The T1.2.5 attack-surface contracts are preserved rather than restated:
AS-ING-1 becomes a constructor that refuses an authored tier, AS-ING-2 becomes
:func:~.labels.safe_label plus a document-id charset, AS-ING-5/6 become the
ceilings and the exception wrapper around every parser call, and AS-NET-1/2/3
become :func:~.outbound.check_outbound_url and wire formatters whose only
parameter is the content.
AttachmentRef
dataclass
¶
One attachment as it arrived: an untrusted name and untrusted bytes.
AttachmentSource
dataclass
¶
AttachmentSource(refs: Sequence[AttachmentRef], extractor: TextExtractor, limits: ExtractionLimits = DEFAULT_LIMITS, on_rejection: Literal['omit', 'raise'] = 'omit', scope_aware: bool = False, offline_safe: bool = True)
A :class:~.contracts.ContextSource over a fixed set of attachments.
select ¶
Extract the attachments, and say why each omitted one was omitted.
on_rejection='raise' makes a refusal loud by ending the read;
'omit' — the default, and the right one when four of five files
should still reach the model — used to make it silent. It is neither
now: the omission and its reason are carried out on the selection, so a
composition root can surface "report.pdf: parser failed" the way the
prompting capability surfaces a PromptDiagnostic, rather than
leaving an operator to guess why a file they attached is absent.
Source code in src/symfonic/capabilities/knowledge/attachments.py
BridgeContractError ¶
Bases: KnowledgeBridgeError
A declaration is impossible: wrong tier, wrong layer, unusable source.
Raised at declaration time, never at render time. A deployment that declares retrieved content at an authored tier is misconfigured, and the only useful moment to say so is before the first turn.
Citation
dataclass
¶
One numbered, display-safe reference to a retrieved source.
CitationIndex
dataclass
¶
The grounded set: every source a retrieval actually returned.
from_fragments
classmethod
¶
Number the distinct sources of fragments in ranked order.
Ranked, not received: the numbering has to be a function of the content so two runs over the same retrieval produce the same reference list.
Source code in src/symfonic/capabilities/knowledge/grounding.py
render ¶
sources ¶
CitationTagFilter ¶
Streaming filter that strips internal layer tags across chunk boundaries.
Feed chunks in order and concatenate what comes back; call :meth:flush
once at stream end. A partial prefix that never completes is dropped at
flush rather than emitted: it was only held back because it matched a known
layer name, so emitting it would leak a fragment of an internal annotation.
Source code in src/symfonic/capabilities/knowledge/citations.py
feed ¶
Append text and return the prefix that is safe to emit.
ContentPart
dataclass
¶
One provider-neutral piece of a multimodal message.
ContextContribution
dataclass
¶
ContextContribution(contribution_id: str, source: ContextSource, capability: str = 'knowledge', layer: ContextLayer = ContextLayer.L2, tier: ContextTier = ContextTier.SESSION, scope: ContextScope = ContextScope.DEPLOYMENT, order: int = 0, inherit: bool = True, pinned: bool = False, requires_hydration: bool = True)
One bridge's declaration of ingested context it contributes.
validate ¶
Refuse every declaration this capability is not allowed to make.
Source code in src/symfonic/capabilities/knowledge/contracts.py
ContextContributor ¶
Bases: Protocol
What a knowledge-side capability registers with the composition root.
ContextLayer ¶
Bases: StrEnum
The stratigraphic layer a contribution renders on.
ContextRead
dataclass
¶
What an ingested-context source answered.
untrusted defaults to True — the inverse of the prompt contract's
default, and the whole point of a separate value type. In the general
contract a source must remember to declare its payload untrusted; here it
would have to remember to declare it trusted, which nothing in this
capability ever does.
ContextRequest
dataclass
¶
What a source is asked for: one contribution, one scope, one turn.
ContextScope ¶
Bases: StrEnum
How widely one contribution's content is shared.
ContextSource ¶
Bases: Protocol
Reads the current ingested content for one contribution and scope.
ContextTier ¶
Bases: StrEnum
Authority tiers, mirroring the prompt contract's vocabulary.
DocumentPolicy
dataclass
¶
The ceilings applied to a rendered document block.
DocumentSource
dataclass
¶
DocumentSource(store: DocumentStore, document_ids: Sequence[str], policy: DocumentPolicy = DocumentPolicy(), scope_aware: bool = False, offline_safe: bool = False)
A :class:~.contracts.ContextSource backed by a document store.
select ¶
Read the documents, and say why each omitted one was omitted.
Four different things make a declared document not appear — the store
has no such id, the document is blank, it is over the per-document
ceiling, or it would cross the aggregate one — and the rendered text
looks identical in all four. The reason channel is what lets a
composition root tell an operator which happened, the way the knowledge
bridge's :class:~.retrieval.FragmentSelection already does.
Source code in src/symfonic/capabilities/knowledge/documents.py
DocumentStore ¶
Bases: Protocol
The port a document store adapter satisfies.
None means "not here", not "empty". A store that answered with an empty
document for a missing id would make a deleted document indistinguishable
from a blank one, and the bridge would render a heading over nothing.
ExtractedAttachment
dataclass
¶
One attachment after extraction, carrying an opaque label.
ExtractionFailed ¶
Bases: KnowledgeBridgeError
A parser could not read one input.
__cause__ carries the underlying library exception. Format parsers are
assumed compromise-prone (AS-ING-5); their failure is a typed rejection of
that input and never propagates as a raw third-party exception.
ExtractionLimits
dataclass
¶
ExtractionLimits(max_bytes: int = 8 * 1024 * 1024, max_uncompressed_bytes: int = 8 * 1024 * 1024, max_expansion_ratio: float = 100.0, max_members: int = 1024, max_nesting_depth: int = 1, max_pages: int = 20, max_text_chars: int = 50000, max_total_chars: int = 120000)
The ceilings applied around a parser call.
FragmentSelection
dataclass
¶
FragmentSelection(admitted: tuple[RetrievedFragment, ...] = (), dropped: tuple[tuple[str, str], ...] = ())
What survived selection, and why the rest did not.
IngestSelection
dataclass
¶
What a source emitted, plus a reason for everything it left out.
The knowledge bridge already answers this way — FragmentSelection.dropped
carries a (source, reason) pair per refused fragment — and the document
and attachment bridges refuse content for the same kinds of reason. Without
the channel, an operator asking "why is the handbook not in the prompt?"
cannot tell a store that returned None from a document over the
per-document ceiling from one evicted by the aggregate cap: the read's text
shows the same absence in all three cases and its revision lists only
what survived.
:meth:as_read is what the :class:ContextSource protocol consumes; the
selection itself is what a composition root reads when it wants to surface
the omissions the way the prompting capability surfaces PromptDiagnostic.
as_read ¶
Project the selection into the value :meth:ContextSource.read returns.
IngestionRejected ¶
Bases: KnowledgeBridgeError
One input hit an ingestion guard: size, count, shape, or identifier.
The guard fired before the parser where the contract allows it (AS-ING-6), so this is cheap and says nothing about the payload's contents.
KnowledgeBridgeError ¶
Bases: Exception
Base for everything this capability raises.
KnowledgeRetriever ¶
Bases: Protocol
The port a vector store adapter satisfies.
Synchronous on purpose. The compiler's source protocol is synchronous, and an async store is adapted once at the composition root rather than forcing every consumer of a prompt to become a coroutine.
KnowledgeSource
dataclass
¶
KnowledgeSource(retriever: KnowledgeRetriever, query: str, policy: RetrievalPolicy = RetrievalPolicy(), scope_aware: bool = False, offline_safe: bool = False, scope_in_query: bool = False)
A :class:~.contracts.ContextSource backed by a retrieval store.
OutboundRejected ¶
Bases: KnowledgeBridgeError
A request or wire payload was refused before it left the framework.
Covers the AS-NET guards (scheme allowlist, host controls) and the media-type allowlist that keeps an attacker-chosen MIME string out of a provider's own parser dispatch.
RetrievalPolicy
dataclass
¶
RetrievalPolicy(limit: int = 3, min_score: float = 0.0, max_fragment_chars: int = 2000, max_total_chars: int = 8000)
The ceilings this side of the boundary applies to a retrieval.
RetrievedFragment
dataclass
¶
One scored chunk of retrieved knowledge with its source attribution.
citation_line ¶
The rendered form: safe source label, single-line content.
Content is flattened to one line, not just labelled. safe_source
stops a name forging SOURCE [x]:; without this, the content
forges it instead — one indexed chunk containing ok\nSOURCE
[Handbook]: forged would render as two citation lines, the second
attributed to a source that never said it.
Source code in src/symfonic/capabilities/knowledge/retrieval.py
StaticContextSource
dataclass
¶
StaticContextSource(text: str, revision: str = 'static', scope_aware: bool = False, offline_safe: bool = True)
A source whose content is fixed at declaration time. Test and seed use.
StoredDocument
dataclass
¶
StoredDocument(document_id: str, title: str, text: str, revision: str = '', media_type: str = 'text/plain')
One document as a store returns it.
TextExtractor ¶
Bases: Protocol
The port a format extractor (PDF, office, image OCR) satisfies.
One method, and it receives bytes rather than a path. That is deliberate: an extractor that took a path would need the framework to write untrusted bytes to a name derived from an untrusted label, which is the AS-ING-2 failure with extra steps.
attachment_contribution ¶
attachment_contribution(contribution_id: str, source: AttachmentSource, *, order: int = 0, scope: str | None = None) -> ContextContribution
Declare this turn's attachments as volatile (L2), session-tier context.
Source code in src/symfonic/capabilities/knowledge/attachments.py
check_outbound_url ¶
Return url if it may be fetched, else raise :class:OutboundRejected.
Literal-address checks only. DNS resolution happens in whatever HTTP client performs the fetch, and the resolution-time and per-redirect checks AS-NET-2 also requires belong there, next to the socket — a guard that validates a name here and lets the client resolve it later is a time-of-check gap, not a control.
"Literal address" is read the way a C resolver reads it, not the way
:func:ipaddress.ip_address does. 2130706433, 127.1 and
0177.0.0.1 all reach 127.0.0.1 without any DNS involvement, so a
guard that only understands dotted-quad hands the loopback interface to
anyone who writes the address a different way.
And the host is normalised through IDNA before any of that, because the
same argument applies one level down: 127.0.0.1 is the loopback
address written in fullwidth digits, and every check here is a comparison
against the ASCII spelling. See :func:_normalise_hostname.
Source code in src/symfonic/capabilities/knowledge/outbound.py
contribution_spec ¶
Project a validated declaration into compiler-ready keyword values.
Validation runs first and unconditionally. A spec is the last moment this capability controls, so emitting one for a declaration it would have refused would move the refusal into the compiler, where the error message can no longer explain which bridge got it wrong.
Enum members are emitted as their string values. The prompt contract's
layers, tiers, and scopes are StrEnums over the same strings, so the
composition root's conversion is total by construction.
Source code in src/symfonic/capabilities/knowledge/assembly.py
document_contribution ¶
document_contribution(contribution_id: str, source: DocumentSource, *, order: int = 0, scope: str | None = None) -> ContextContribution
Declare a document set as standing (L1), session-tier context.
L1 rather than L2: the documents attached to a conversation are
stable for its life, and putting them on the volatile layer would push the
per-turn boundary above them and forfeit the cached prefix for content that
never changes.
Source code in src/symfonic/capabilities/knowledge/documents.py
drop_ungrounded_citations ¶
Remove [[source:X]] references whose X was never retrieved.
Returns the rewritten text and the ungrounded names, each reported once in first-appearance order. When nothing was ungrounded the text is returned byte-identical — a filter that reflowed whitespace on every clean response would make its own no-op observable.
Source code in src/symfonic/capabilities/knowledge/grounding.py
extract_attachment ¶
extract_attachment(ref: AttachmentRef, extractor: TextExtractor, limits: ExtractionLimits = DEFAULT_LIMITS) -> ExtractedAttachment
Extract one attachment's text under the ingestion ceilings.
Raises :class:~.errors.IngestionRejected when a guard fires and
:class:~.errors.ExtractionFailed when the parser itself loses. Nothing a
third-party parser raises escapes this function untyped.
Source code in src/symfonic/capabilities/knowledge/attachments.py
flatten_content ¶
knowledge_contribution ¶
knowledge_contribution(contribution_id: str, source: KnowledgeSource, *, order: int = 0, scope: str | None = None) -> ContextContribution
Declare a retrieval as per-turn, session-tier ingested context.
Source code in src/symfonic/capabilities/knowledge/retrieval.py
knowledge_sources ¶
knowledge_sources(*, retriever: Any = None, query: str = '', store: Any = None, document_ids: Sequence[str] = (), attachments: Sequence[Any] = (), extractor: Any = None, context: str = '', **policies: Any) -> tuple[Any, ...]
The sources this deployment's knowledge reaches the prompt through.
Every argument is optional and each one adds a source only when it can
actually produce something -- a retriever with no query retrieves nothing,
and a store with no ids reads nothing. Returning a source that cannot
contribute would cost the compiler a budget slot and a delimiter for an
empty block, which is the same rule compose follows in governance:
leave it out rather than in-but-inert.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
retriever
|
Any
|
a |
None
|
query
|
str
|
what to retrieve. Required for |
''
|
store
|
Any
|
a |
None
|
document_ids
|
Sequence[str]
|
which documents to pin into the prompt. |
()
|
attachments
|
Sequence[Any]
|
refs the caller sent with the turn. |
()
|
extractor
|
Any
|
turns an attachment into text. Required for |
None
|
context
|
str
|
static text this deployment always wants present. |
''
|
**policies
|
Any
|
|
{}
|
Source code in src/symfonic/capabilities/knowledge/factory.py
layer_index ¶
media_part ¶
Normalise bytes into a base64 part, under the media-type allowlist.
Source code in src/symfonic/capabilities/knowledge/multimodal.py
normalise_extracted ¶
Unix line endings, control characters stripped, trailing space removed.
Source code in src/symfonic/capabilities/knowledge/attachments.py
ordering_key ¶
Total order: layer, then declared order, then id.
Totality is the point — two contributions that tie on layer and order still have a defined relative position, so what the bridge hands the compiler is byte-stable across runs and across upstream dict iteration orders.
Source code in src/symfonic/capabilities/knowledge/contracts.py
render_fragments ¶
Render admitted fragments in the legacy SOURCE [x]: y line format.
resolve_scope ¶
Turn a caller's scope string into a member, or refuse it in-hierarchy.
ContextScope('tenant-a') raises a bare :class:ValueError, which is the
one refusal in this capability a caller catching
:class:~.errors.BridgeContractError around declaration building would
miss. The bridge factories go through here so that every way of getting a
declaration wrong reports the same way.
Source code in src/symfonic/capabilities/knowledge/contracts.py
safe_label ¶
Reduce a filename to an opaque display label.
Path structure is discarded rather than escaped: the basename is taken
after splitting on both separators, so a POSIX host still refuses a
Windows-shaped traversal. Anything that reduces to "", . or ..
becomes :data:FALLBACK_LABEL, because a caller that received an empty
label would be tempted to substitute the original.
Source code in src/symfonic/capabilities/knowledge/labels.py
safe_source ¶
Reduce a citation source name to something that cannot forge a delimiter.
Spaces survive — a source name is read by a human — but every bracket-ish
character is removed. SOURCE [x]: is a delimiter, and a name allowed to
contain ] decides where that delimiter ends.
Source code in src/symfonic/capabilities/knowledge/labels.py
select_fragments ¶
select_fragments(fragments: Iterable[RetrievedFragment], policy: RetrievalPolicy) -> FragmentSelection
Rank, filter, and cap what the store returned. Never mutates the input.
Source code in src/symfonic/capabilities/knowledge/retrieval.py
selection_revision ¶
A content-derived revision, so a changed retrieval changes the cache key.
Source code in src/symfonic/capabilities/knowledge/retrieval.py
text_part ¶
Normalise a text part. Whitespace-only text is refused, not sent.
Source code in src/symfonic/capabilities/knowledge/multimodal.py
to_anthropic_blocks ¶
Render parts as Anthropic content blocks.
Source code in src/symfonic/capabilities/knowledge/multimodal.py
to_openai_blocks ¶
Render parts as OpenAI content blocks.
A document part is refused rather than approximated. OpenAI's chat content
schema has no document block, and inventing one — a data: image URL
carrying a PDF, say — produces a request the provider rejects at best and
silently misreads at worst.
Source code in src/symfonic/capabilities/knowledge/multimodal.py
validate_document_id ¶
Refuse any id that could be read as a path or a delimiter.