Skip to content

.NET: fix(hosting): emit url_citation annotation events from streamed AI Search responses#6649

Open
anneheartrecord wants to merge 1 commit into
microsoft:mainfrom
anneheartrecord:fix/6641-url-citation-annotations
Open

.NET: fix(hosting): emit url_citation annotation events from streamed AI Search responses#6649
anneheartrecord wants to merge 1 commit into
microsoft:mainfrom
anneheartrecord:fix/6641-url-citation-annotations

Conversation

@anneheartrecord

Copy link
Copy Markdown

Problem

When using FoundryAITool.CreateAzureAISearchTool() with a hosted agent via MapFoundryResponses, the SSE stream produced citation markers in text (e.g. 【5:0†source】) but:

  • response.output_text.annotation.added events were never emitted
  • annotations arrays in response.content_part.done and response.output_item.done were always empty

Root Cause

OutputConverter.ConvertUpdatesToEventsAsync processes TextContent from AgentResponseUpdate.Contents but only extracts .Text — it ignores TextContent.Annotations, which is where the agent framework surfaces CitationAnnotation (url_citation) metadata from Azure AI Search grounding.

Fix

  • Accumulate CitationAnnotation instances from each TextContent update across the full message (parallel to how text is accumulated in StringBuilder)
  • Convert them to UrlCitationBody SDK objects via a new ConvertToSdkAnnotations helper, matching the approach already used by the OpenAI ChatCompletions path in AgentResponseExtensions.ToChoiceMessageAnnotations
  • Emit them via TextContentBuilder.EmitAnnotationAdded in CloseCurrentMessage, after EmitTextDone and before EmitDone (required by the SDK builder lifecycle)
  • Non-CitationAnnotation types and citations without explicit TextSpanAnnotatedRegion start/end indices are silently skipped

Tests

7 new unit tests (N-01–N-07) in OutputConverterTests:

Test Covers
N-01 Basic url_citation emission: URL, title, start/end indices
N-02 Ordering: annotation after last text delta, before output_item.done
N-03 Multiple annotations on one TextContent all emitted in order
N-04 Annotations accumulated across multiple streaming updates for the same message
N-05 CitationAnnotation with null URL is skipped
N-06 CitationAnnotation with no TextSpanAnnotatedRegion is skipped
N-07 Non-CitationAnnotation AIAnnotation is skipped

Fixes #6641

…arch responses

OutputConverter.ConvertUpdatesToEventsAsync accumulated text content deltas but
silently dropped CitationAnnotation metadata from TextContent.Annotations. As a
result, hosted agents that use CreateAzureAISearchTool emitted citation markers in
text (e.g. 【5:0†source】) but produced empty annotations arrays and no
response.output_text.annotation.added SSE events.

The fix accumulates UrlCitationBody SDK annotations across all TextContent updates
for a message and emits them via TextContentBuilder.EmitAnnotationAdded after
EmitTextDone (as required by the SDK lifecycle) and before EmitDone. Non-citation
and region-less annotations are silently skipped, matching the existing OpenAI
ChatCompletions path in AgentResponseExtensions.

Adds 7 unit tests (N-01–N-07) covering: basic emission, ordering constraints,
multiple annotations, multi-update accumulation, and skip conditions.

Fixes microsoft#6641
Copilot AI review requested due to automatic review settings June 20, 2026 13:17
@moonbox3 moonbox3 added the .NET Issues related to the .NET codebase label Jun 20, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes the .NET Foundry Hosting SSE conversion path so that url_citation metadata (from Azure AI Search grounding) is not dropped during streaming: annotations from TextContent.Annotations are accumulated across deltas and emitted as response.output_text.annotation.added events via the Responses SDK builder lifecycle.

Changes:

  • Accumulates MEAI CitationAnnotation instances from streamed TextContent updates and converts them into Responses SDK UrlCitationBody annotations.
  • Emits annotation-added events during message finalization (after EmitTextDone and before EmitDone) so clients receive response.output_text.annotation.added.
  • Adds a new N-series unit test suite covering basic emission, ordering, multi-annotation, multi-update accumulation, and skip rules.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/OutputConverter.cs Accumulates and emits citation annotations from streamed text updates as Responses SDK annotation events.
dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/OutputConverterTests.cs Adds N-series unit tests validating url_citation annotation event emission, ordering, accumulation, and skip conditions.

Comment on lines +1398 to +1405
var annotationEvent = Assert.Single(events.OfType<ResponseOutputTextAnnotationAddedEvent>());
var urlCitation = Assert.IsType<UrlCitationBody>(annotationEvent.Annotation);
Assert.Equal(new Uri("https://example.com/doc"), urlCitation.Url);
Assert.Equal("Example Document", urlCitation.Title);
Assert.Equal(0L, urlCitation.StartIndex);
Assert.Equal(5L, urlCitation.EndIndex);
Assert.IsType<ResponseCompletedEvent>(events[^1]);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

.NET Issues related to the .NET codebase

Projects

None yet

Development

Successfully merging this pull request may close these issues.

.NET: AgentFrameworkResponseHandler drops AI Search annotations (url_citation) from streamed responses

3 participants