Table of Contents

Class RagCitations

Namespace
Qavren.Edge.Rag
Assembly
Qavren.Edge.Rag.dll

The two property keys the pipeline talks over, the pin key, and the marker resolver.

public static class RagCitations
Inheritance
RagCitations
Inherited Members

Fields

ContextMessagePropertyKey

Set to true on the AdditionalProperties of the retrieved-context ChatMessage this middleware injects, marking it pinned: a history reducer must never evict it.

It exists because the injected block breaks the shape a turn-based reducer assumes. The block is a second ChatRole.User message sitting immediately before the real user message, so the tail of the list is user-then-user rather than the user/assistant pairs EdgeChatTokenBudgetReducer evicts in - and evicting the grounding the whole recipe exists to supply, to make room for history, would be the worst possible trade.

The literal is duplicated in Qavren.Edge.Chat.Onnx on purpose. Qavren.Edge.Rag does not reference that package and must not, so the two cannot share a constant; the chat package's ChatHistoryOptions.PinnedMessageKeys defaults to this exact string and a tier-1 test asserts the two literals are equal. That is the same deliberate duplication, with the same asserted-equality guard, that sub-project 2 already uses for EdgeVectorData.QueryGeneratorServiceKey and EdgeEmbeddings.QueryServiceKey. A reducer that does not know the key simply sees an ordinary user message - it degrades to the old behaviour rather than breaking.

GroundedPropertyKey

Response-side only. Whether the answer was produced from retrieved sources.

SourcesPropertyKey

Carries IReadOnlyList<RagSource>, ranked, clamped and Ordinal-stamped.

It is used on both sides of the call, and that is the specified channel by which an inner client receives the structured sources. Before invoking the inner client, RagChatClient clones the caller's ChatOptions and sets this key on the clone's AdditionalProperties; after the turn it sets the same key on the ChatResponse. An inner client that wants the sources reads the request-side entry and never re-parses the rendered text block, which exists for the model to read. ExtractiveChatClient is the one in-box client that does so.

The clone matters: MEAI's IChatClient contract permits an implementation to mutate the ChatOptions it is handed, so consumers must not share one across concurrent calls - which means this client must not write into the caller's instance either. ChatOptions.Clone() is public and shallow-copies AdditionalProperties into a new dictionary, which is exactly the isolation needed.

A leaf that does not know this key ignores it: EdgeChatClient forwards AdditionalProperties to GeneratorParams.SetSearchOption only for bool and double values and skips everything else, so a RagSource list on the request is inert rather than a failure. A third-party leaf that inspected every property would see a documented, publicly-typed value.

Methods

AttachTo(ChatResponseUpdate, IReadOnlyList<CitationAnnotation>)

Attaches citations to update by appending one TextContent(string.Empty) whose Annotations hold them. The carrier is spelled out as a public helper because an AIAnnotation hangs off an AIContent and the final update has no text content of its own to hang them on - and because a consumer writing their own middleware should attach them the same way.

Build(string, IReadOnlyList<RagSource>)

Finds [n] markers in a completed answer and builds one CitationAnnotation { Title, Url, FileId, Snippet } per distinct marker, each carrying a TextSpanAnnotatedRegion(StartIndex, EndIndex) per occurrence. A marker with no matching source is left as plain text, counted, and never fabricated into a citation. A total function: it does not throw.