From 844833c46d516c82669b6528de56465584f15489 Mon Sep 17 00:00:00 2001 From: Himansh Raj Date: Fri, 30 Oct 2026 00:57:15 +0530 Subject: [PATCH] Python: keep empty tool results through the prompt round trip FunctionResultContent.to_element renders an empty result as a self-closing element, and from_element read its text back as None, which ChatMessageContent then sent to the model as the string "None". Map missing text to "". Fixes #14685 --- .../contents/function_result_content.py | 4 +++- .../contents/test_function_result_content.py | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/python/semantic_kernel/contents/function_result_content.py b/python/semantic_kernel/contents/function_result_content.py index b881371a398f..654c4ef565be 100644 --- a/python/semantic_kernel/contents/function_result_content.py +++ b/python/semantic_kernel/contents/function_result_content.py @@ -116,7 +116,9 @@ def from_element(cls: type[_T], element: Element) -> _T: """Create an instance from an Element.""" if element.tag != cls.tag: raise ContentInitializationError(f"Element tag is not {cls.tag}") # pragma: no cover - return cls(id=element.get("id", ""), result=element.text, name=element.get("name", None)) + # An empty result renders as a self-closing element, whose text parses back as None. + result = element.text if element.text is not None else "" + return cls(id=element.get("id", ""), result=result, name=element.get("name", None)) @classmethod def from_function_call_content_and_result( diff --git a/python/tests/unit/contents/test_function_result_content.py b/python/tests/unit/contents/test_function_result_content.py index 2e83dc9737a9..7745a2d2a89a 100644 --- a/python/tests/unit/contents/test_function_result_content.py +++ b/python/tests/unit/contents/test_function_result_content.py @@ -2,10 +2,12 @@ from unittest.mock import Mock +from xml.etree.ElementTree import XML, tostring # nosec import pytest from pydantic import BaseModel, ConfigDict, Field +from semantic_kernel.contents.chat_history import ChatHistory from semantic_kernel.contents.chat_message_content import ChatMessageContent from semantic_kernel.contents.function_call_content import FunctionCallContent from semantic_kernel.contents.function_result_content import FunctionResultContent @@ -123,6 +125,21 @@ def test_to_cmc(): assert cmc.items[0].result == "test-result" +@pytest.mark.parametrize("result", ["", "test-result"], ids=["empty", "non_empty"]) +def test_element_round_trip_keeps_result(result: str): + frc = FunctionResultContent(id="call_1", name="my-plugin-do", result=result) + restored = FunctionResultContent.from_element(XML(tostring(frc.to_element(), encoding="unicode"))) + assert restored.result == result + assert restored.to_dict() == {"tool_call_id": "call_1", "content": result} + + +def test_chat_history_prompt_round_trip_keeps_empty_tool_result(): + history = ChatHistory() + history.add_tool_message("", tool_call_id="call_1", function_name="my-plugin-do") + parsed = ChatHistory.from_rendered_prompt(history.to_prompt()) + assert parsed.messages[0].to_dict() == {"role": "tool", "content": "", "tool_call_id": "call_1"} + + def test_serialize(): class CustomResultClass: def __init__(self, result):