Skip to content

Python: fix(python): Handle thread.message.completed event in Assistants API streaming#4333

Open
LEDazzio01 wants to merge 4 commits intomicrosoft:mainfrom
LEDazzio01:fix/4322-assistants-stream-message-completed
Open

Python: fix(python): Handle thread.message.completed event in Assistants API streaming#4333
LEDazzio01 wants to merge 4 commits intomicrosoft:mainfrom
LEDazzio01:fix/4322-assistants-stream-message-completed

Conversation

@LEDazzio01
Copy link
Contributor

Summary

Fixes #4322

Previously, thread.message.completed streaming events fell through to the catch-all else branch in _process_stream_events, yielding empty ChatResponseUpdate objects. This silently discarded fully-resolved annotation data — file citations with IDs, quotes, and character-offset regions.

Changes

_assistants_client.py

  • New imports: FileCitationAnnotation, FilePathAnnotation, Message as ThreadMessage from openai.types.beta.threads; Annotation, TextSpanRegion from _types
  • New elif branch for thread.message.completed in _process_stream_events that:
    • Walks the completed ThreadMessage.content array
    • Skips non-text blocks (e.g., image_file)
    • For text blocks, extracts fully-resolved annotations:
      • FileCitationAnnotationAnnotation(type="citation") with file_id and TextSpanRegion
      • FilePathAnnotation → same mapping pattern
    • Yields a ChatResponseUpdate with the complete text and annotations

test_assistants_message_completed.py (new)

7 test cases covering:

  • File citation annotation extraction
  • File path annotation extraction
  • Multiple annotations on a single text block
  • Text-only messages (no annotations)
  • Non-text blocks are skipped
  • Mixed content blocks (text + image)
  • Conversation ID propagation

Impact

Users of the Assistants API with file_search or code_interpreter will now receive resolved citation annotations in streaming responses, enabling proper citation rendering.

Previously, `thread.message.completed` events fell through to the
catch-all `else` branch and yielded empty `ChatResponseUpdate` objects,
silently discarding fully-resolved annotation data (file citations,
file paths, and their character-offset regions).

This commit adds a dedicated handler for `thread.message.completed`
that:
- Walks the completed ThreadMessage.content array
- Extracts text blocks with their fully-resolved annotations
- Maps FileCitationAnnotation and FilePathAnnotation to the
  framework's Annotation type with proper TextSpanRegion data
- Yields a ChatResponseUpdate containing the complete text and
  annotations

Fixes microsoft#4322
Tests cover:
- File citation annotation extraction
- File path annotation extraction
- Multiple annotations on a single text block
- Text-only messages (no annotations)
- Non-text blocks are skipped
- Mixed content blocks (text + image)
- Conversation ID propagation
Copilot AI review requested due to automatic review settings February 26, 2026 22:25
@github-actions github-actions bot changed the title fix(python): Handle thread.message.completed event in Assistants API streaming Python: fix(python): Handle thread.message.completed event in Assistants API streaming Feb 26, 2026
Copy link
Contributor

Copilot AI left a comment

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 a bug where thread.message.completed streaming events from the OpenAI/Azure Assistants API were falling through to a catch-all handler, yielding empty ChatResponseUpdate objects and discarding fully-resolved annotation metadata (file citations with IDs, quotes, and character offsets). The fix adds a dedicated event handler that extracts text content and annotations from completed messages, enabling proper citation rendering for users of the Assistants API with file_search or code_interpreter tools.

Changes:

  • Added handler for thread.message.completed event in _process_stream_events to extract fully-resolved annotation data
  • Added comprehensive test suite covering annotation extraction, edge cases, and conversation ID propagation
  • Added imports for FileCitationAnnotation, FilePathAnnotation, ThreadMessage, Annotation, and TextSpanRegion types

Reviewed changes

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

File Description
python/packages/core/agent_framework/openai/_assistants_client.py Added imports and new elif branch in _process_stream_events to handle thread.message.completed events, extracting text content and mapping FileCitationAnnotation and FilePathAnnotation to framework Annotation objects with TextSpanRegion data
python/packages/core/tests/openai/test_assistants_message_completed.py New test file with 7 test cases covering file citation extraction, file path extraction, multiple annotations, text-only messages, non-text block skipping, mixed content blocks, and conversation ID propagation

Comment on lines 582 to 619
if isinstance(annotation, FileCitationAnnotation):
ann: Annotation = Annotation(
type="citation",
additional_properties={
"text": annotation.text,
},
raw_representation=annotation,
)
if annotation.file_citation and annotation.file_citation.file_id:
ann["file_id"] = annotation.file_citation.file_id
if annotation.start_index is not None and annotation.end_index is not None:
ann["annotated_regions"] = [
TextSpanRegion(
type="text_span",
start_index=annotation.start_index,
end_index=annotation.end_index,
)
]
text_content.annotations.append(ann)
elif isinstance(annotation, FilePathAnnotation):
ann = Annotation(
type="citation",
additional_properties={
"text": annotation.text,
},
raw_representation=annotation,
)
if annotation.file_path and annotation.file_path.file_id:
ann["file_id"] = annotation.file_path.file_id
if annotation.start_index is not None and annotation.end_index is not None:
ann["annotated_regions"] = [
TextSpanRegion(
type="text_span",
start_index=annotation.start_index,
end_index=annotation.end_index,
)
]
text_content.annotations.append(ann)
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

Missing default case for unrecognized annotation types. The implementation should include an else clause to log unhandled annotation types at debug level, consistent with the pattern established in _responses_client.py lines 1176-1180. This ensures visibility into any future annotation types that may be added by the API without requiring code changes.

Copilot uses AI. Check for mistakes.
Comment on lines 582 to 600
if isinstance(annotation, FileCitationAnnotation):
ann: Annotation = Annotation(
type="citation",
additional_properties={
"text": annotation.text,
},
raw_representation=annotation,
)
if annotation.file_citation and annotation.file_citation.file_id:
ann["file_id"] = annotation.file_citation.file_id
if annotation.start_index is not None and annotation.end_index is not None:
ann["annotated_regions"] = [
TextSpanRegion(
type="text_span",
start_index=annotation.start_index,
end_index=annotation.end_index,
)
]
text_content.annotations.append(ann)
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The quote field from annotation.file_citation.quote should be included in additional_properties to preserve complete citation metadata. The issue description (#4322) explicitly mentions that completed message annotations contain "quote text" that needs to be captured. This field contains the exact text snippet from the source file that was cited, which is valuable for rendering proper citations.

Copilot uses AI. Check for mistakes.
…notations

- Include `quote` from `annotation.file_citation.quote` in
  `additional_properties` for FileCitationAnnotation, preserving the
  exact cited text snippet from the source file
- Add `else` clause to log unrecognized annotation types at debug level,
  consistent with the pattern in `_responses_client.py`
- Add `import logging` and module-level logger
- test_message_completed_with_file_citation_quote: verifies quote is
  included in additional_properties
- test_message_completed_with_file_citation_no_quote: verifies quote
  is omitted when None
- test_message_completed_unrecognized_annotation_logged: verifies
  unknown annotation types are logged at debug level and skipped
@LEDazzio01
Copy link
Contributor Author

Thanks @copilot for the review! Both suggestions were great catches. I've addressed them in the latest commits:

1. Quote field (dc060ee): Added annotation.file_citation.quote to additional_properties when present, preserving the exact cited text snippet for proper citation rendering.

2. Unrecognized annotation fallback (dc060ee): Added an else clause that logs unrecognized annotation types at DEBUG level, consistent with _responses_client.py lines 1176-1180.

3. Test coverage (2a6b80a): Added 3 new tests:

  • test_message_completed_with_file_citation_quote — verifies quote is captured
  • test_message_completed_with_file_citation_no_quote — verifies quote is omitted when None
  • test_message_completed_unrecognized_annotation_logged — verifies unknown types are logged at debug level and skipped (known annotations still processed)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: [Bug]: thread.message.completed Event Unhandled in Assistants API Streaming

3 participants