Skip to content

Python: Fix extract_range reordering messages when preserving function call/result pairs#14165

Open
ErenAta16 wants to merge 1 commit into
microsoft:mainfrom
ErenAta16:fix/extract-range-preserve-pairs-ordering
Open

Python: Fix extract_range reordering messages when preserving function call/result pairs#14165
ErenAta16 wants to merge 1 commit into
microsoft:mainfrom
ErenAta16:fix/extract-range-preserve-pairs-ordering

Conversation

@ErenAta16

Copy link
Copy Markdown

Motivation and Context

extract_range() in chat_history_reducer_utils.py is what ChatHistorySummarizationReducer uses (when include_function_content_in_summary=True) to pull the slice of chat history that gets sent to the LLM for summarization, while making sure a function call is never separated from its result.

When a function-call/result pair had other messages interleaved between them, extract_range silently moved the result message to sit right after the call, corrupting the chronological order of the messages handed to the summarizer. For example:

history = [user, assistant(call), user, tool(result), assistant]
extract_range(history, 0, 5, preserve_pairs=True)
# returned: [user, assistant(call), tool(result), user, assistant]
# instead of the original order.

The old implementation walked a mutable sliced index list with a manual cursor. When it hit the first half of a pair, it appended both messages to the output immediately and removed the paired index from sliced so it wouldn't be reprocessed later. That is what actually moved the second message out of its original position.

Description

  • Rewrote extract_range to do a single forward pass over range(start, end) and decide, per index, whether to keep or skip the message without ever moving it out of its natural position.
  • For call/result pairs fully contained in the requested slice, keep/skip is still decided together (unchanged behavior), but each message is only appended when its own index comes up in the iteration, so order is preserved.
  • Pairs that are only partially in range keep the existing "never orphan" behavior of always keeping the in-range half.
  • Added test_extract_range_preserve_pairs_keeps_chronological_order to tests/unit/contents/test_chat_history_reducer_utils.py, reusing the existing chat_messages_with_pairs fixture (its call2/result2 pair already has two unrelated messages between them) and asserting the extracted list equals the original slice, not just the same set of messages.

Verified with a standalone repro against the unmodified code (via git stash) that the bug reproduces, and that it's fixed after applying the patch. Ran tests/unit/contents/test_chat_history_reducer_utils.py, test_chat_history_summarization_reducer.py, test_chat_history_truncation_reducer.py, and the full tests/unit/contents/ suite — all passing. Also ran ruff check and ruff format --check on both changed files.

Contribution Checklist

…n call/result pairs

Problem:
extract_range() in chat_history_reducer_utils.py is used by
ChatHistorySummarizationReducer (when include_function_content_in_summary=True)
to pull the slice of history that gets sent to the LLM for summarization,
while making sure a function call and its result are never split apart.

When a function-call/result pair had other messages interleaved between them,
extract_range silently moved the result message to sit right after the call,
corrupting the chronological order of the messages handed to the summarizer.

Example: history = [user, assistant(call), user, tool(result), assistant]
extract_range(..., preserve_pairs=True) returned
[user, assistant(call), tool(result), user, assistant]
instead of the original order. The "unrelated" user message got pushed past
the tool result.

Root cause:
The old implementation walked a mutable `sliced` index list with a manual
cursor `i`. When it hit the first half of a pair, it appended both messages
to the output immediately and called `sliced.remove(paired_idx)` to avoid
re-processing the second half later. That is what actually moved the second
message: it got appended out of turn instead of being left in its original
position.

Fix:
Rewrote extract_range to make a single forward pass over range(start, end)
and decide, for each index, whether to keep or skip it without ever moving
a message out of its natural position. For pairs fully contained in the
slice, keep/skip is still decided together (unchanged behavior), but each
message is only appended when its own index comes up in the iteration, so
order is preserved. Pairs that are only partially in range keep the old
"never orphan" behavior of always keeping the in-range half.

Testing:
- Wrote a standalone repro script that builds a 5-message history with a
  call/result pair separated by an unrelated user message, called
  extract_range(..., preserve_pairs=True), and compared the output order
  to the input order. Confirmed it fails on the original code (the result
  moves ahead of the interleaved user message) and passes after the fix.
- Confirmed via git stash / stash pop that the bug reproduces on the
  unmodified code and is resolved after applying the fix.
- Added test_extract_range_preserve_pairs_keeps_chronological_order to
  tests/unit/contents/test_chat_history_reducer_utils.py, reusing the
  existing chat_messages_with_pairs fixture (its call2/result2 pair already
  has two unrelated messages between them), asserting extracted equals the
  original slice rather than just set equality like the existing tests do.
- Ran tests/unit/contents/test_chat_history_reducer_utils.py (11 passed),
  test_chat_history_summarization_reducer.py and
  test_chat_history_truncation_reducer.py (32 passed), and the full
  tests/unit/contents/ suite (385 passed, no new warnings).
- Ran ruff check and ruff format --check on both changed files.
Copilot AI review requested due to automatic review settings July 18, 2026 00:36
@ErenAta16
ErenAta16 requested a review from a team as a code owner July 18, 2026 00:36

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 extract_range() (used by Python chat history reducers) so that enabling preserve_pairs=True no longer reorders messages when a function call/result pair has unrelated messages interleaved between them, ensuring the summarization input remains chronologically correct.

Changes:

  • Rewrote extract_range() to iterate forward over the requested slice and decide keep/skip per index without relocating paired messages.
  • Added a regression test asserting that preserve_pairs=True preserves the original message order for an interleaved call/result pair.

Reviewed changes

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

File Description
python/semantic_kernel/contents/history_reducer/chat_history_reducer_utils.py Refactors extract_range() to preserve chronological order while still coordinating keep/skip decisions for fully in-range call/result pairs.
python/tests/unit/contents/test_chat_history_reducer_utils.py Adds a regression test to ensure extract_range(..., preserve_pairs=True) does not reorder messages.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +197 to +200
# The paired message falls outside [start, end): keep this one unconditionally
# so a function call is never separated from its result (or vice versa).
extracted.append(msg)
i += 1
continue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants