Skip to content

Python: Preserve messages when replacing chat history from itself - #14465

Open
黄耀鑫(Huang Yaoxin) (YaoxinHuang) wants to merge 2 commits into
microsoft:mainfrom
YaoxinHuang:fix/chat-history-replace-iterables
Open

黄耀鑫(Huang Yaoxin) (YaoxinHuang) wants to merge 2 commits into
microsoft:mainfrom
YaoxinHuang:fix/chat-history-replace-iterables

Conversation

@YaoxinHuang

Copy link
Copy Markdown

Motivation and Context

ChatHistory.replace clears its messages before consuming the replacement iterable. Passing the history itself, its messages list, or a generator filtering the history therefore silently empties it. For example, removing assistant messages with history.replace(m for m in history if m.role != AuthorRole.ASSISTANT) also loses the system and user messages.

Description

Materialize the iterable before clearing the history, while preserving the existing messages list. Add regression coverage for the history, its backing list, an iterator, and a filtered generator.

Validated the contents unit suite, repository pre-commit hooks for both changed files, scoped mypy, and a wheel build. The full optional-provider test matrix was not run.

Contribution Checklist

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.

Copilot review overview

🟢 Approval recommended

Reviewed changes address the reported replacement behavior with regression coverage.

Review effort: Lite
Findings: None

What changed in this PR

Updates ChatHistory.replace to preserve messages when replacing history from itself or related iterables.

Changes:

  • Materializes replacement messages before clearing history.
  • Adds regression tests for histories, lists, iterators, and filtered generators.
File Description
python/​tests/​unit/​contents/​test_chat_history.py Adds regression coverage for self-referential replacements.
python/​semantic_kernel/​contents/​chat_history.py Safely materializes replacement messages while preserving list identity.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@YaoxinHuang

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

@VANDRANKI PRABHU KIRAN VANDRANKI (VANDRANKI) 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.

Community review, not a maintainer review, so this does not clear the merge gate. I read the full diff and ran ChatHistory.replace on this branch and on origin/main (Python 3.13, python/ of a local worktree).

The bug is real and the fix is correct for what I ran. I built a history of three messages (system s, user q, assistant a) and called replace with different sources:

  • replace(history), replace(history.messages) and replace(iter(history)): main leaves the history empty ([]), because clear() empties the very list the iterable reads from. This branch leaves ['s', 'q', 'a'].
  • replace(m for m in history if m.role != assistant): main gives [], this branch gives ['s', 'q'].
  • replace(list(history.messages)) (a copy): ['s', 'q', 'a'] on both, so existing callers that already pass a copy see no change.

Tests: tests/unit/contents/test_chat_history.py gives 59 passed on this branch. With main's chat_history.py and the PR's test file, 4 fail (the three test_replace_from_same_history ids and test_replace_from_filtered_history), so the new tests do catch the bug.

Reading notes, nothing blocking:

  • The new messages is messages assertion in the test checks that the list object is kept and cleared in place. That matches clear() calling self.messages.clear(), so anyone holding a reference to the old list still sees the new content.
  • list(messages) copies the input once. That is the whole cost of the change.
  • In python/semantic_kernel my grep (for def replace, .replace(messages, .replace(self and .replace(chat) found only the ChatHistory.replace definition and no caller that passes a history to itself. That grep is narrow, so it does not rule out other call shapes. I did not search the samples or other packages.

The change is small and does what it says.

This branch had an error being deployed

1 failed deployment
github-app-auth — 570ffe21 Deployed Sep 20, 2026 by YaoxinHuang via review #527
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.

3 participants