From 2f582d4558ad234811f597973a2176d340ff12a6 Mon Sep 17 00:00:00 2001 From: Kumaresan Date: Sun, 13 Sep 2026 12:28:03 +0530 Subject: [PATCH] Python: Read and write chat history files as UTF-8 ChatHistory.store_chat_history_to_file and load_chat_history_from_file opened the file in text mode without an encoding, so they used the locale encoding. On Windows that is cp1252, and storing a history with non-ASCII content raised UnicodeEncodeError; a history written as UTF-8 elsewhere also failed to load. Both now pass encoding="utf-8", matching how the rest of the package reads text files (KernelPlugin.from_directory and KernelFunctionFromPrompt.from_directory default to utf-8). Adds a regression test that round-trips a history with Chinese, accented and emoji content, which fails on the Windows CI legs before this change. --- .../semantic_kernel/contents/chat_history.py | 7 +++++-- .../tests/unit/contents/test_chat_history.py | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/python/semantic_kernel/contents/chat_history.py b/python/semantic_kernel/contents/chat_history.py index a773c232c7bc..1f3d34c84f8a 100644 --- a/python/semantic_kernel/contents/chat_history.py +++ b/python/semantic_kernel/contents/chat_history.py @@ -414,12 +414,14 @@ def store_chat_history_to_file(self, file_path: str) -> None: """Stores the serialized ChatHistory to a file. Uses mode "w" which means the file is created if it does not exist and gets truncated if it does. + The file is always written as UTF-8, so that histories with non-ASCII content can be stored + and reloaded on platforms whose locale encoding is not UTF-8. Args: file_path: The path to the file where the serialized data will be stored. """ json_str = self.serialize() - with open(file_path, "w") as local_file: + with open(file_path, "w", encoding="utf-8") as local_file: local_file.write(json_str) @classmethod @@ -427,6 +429,7 @@ def load_chat_history_from_file(cls: type[_T], file_path: str) -> _T: """Loads the ChatHistory from a file. Uses mode "r" which means it can only be read if it exists. + The file is always read as UTF-8, matching what store_chat_history_to_file writes. Args: file_path: The path to the file from which to load the ChatHistory. @@ -434,6 +437,6 @@ def load_chat_history_from_file(cls: type[_T], file_path: str) -> _T: Returns: ChatHistory: The deserialized ChatHistory instance. """ - with open(file_path) as file: + with open(file_path, encoding="utf-8") as file: json_str = file.read() return cls.restore_chat_history(json_str) diff --git a/python/tests/unit/contents/test_chat_history.py b/python/tests/unit/contents/test_chat_history.py index 3c1b92945296..e0a8c5fed33a 100644 --- a/python/tests/unit/contents/test_chat_history.py +++ b/python/tests/unit/contents/test_chat_history.py @@ -612,6 +612,24 @@ def test_to_from_file(chat_history: ChatHistory, tmp_path): assert chat_history_2.messages[4] == chat_history.messages[4] +def test_to_from_file_non_ascii(chat_history: ChatHistory, tmp_path): + """Chat histories with non-ASCII content should round-trip on any platform. + + The file is always written and read as UTF-8, so the round-trip does not depend on the + locale encoding of the machine (which is not UTF-8 on Windows). + """ + chat_history.add_user_message("你好,今天天气怎么样?") + chat_history.add_assistant_message("À Paris, il fait beau ☀️") + + file_path = tmp_path / "chat_history.json" + chat_history.store_chat_history_to_file(file_path) + + assert "你好" in file_path.read_text(encoding="utf-8") + + chat_history_2 = ChatHistory.load_chat_history_from_file(file_path) + assert chat_history_2.messages == chat_history.messages + + def test_from_rendered_prompt_preserves_html_p_tag(): """HTML

tags in prompts should be preserved as text, not treated as template tags.