From 289ac3f1a4366c7bba0a82239d4450ec547bdbeb Mon Sep 17 00:00:00 2001 From: VANDRANKI Date: Wed, 30 Sep 2026 12:39:40 -0400 Subject: [PATCH 1/2] Python: keep a streamed '{}' delta when combining function call arguments --- .../contents/function_call_content.py | 18 ++++++++++++- .../contents/test_function_call_content.py | 26 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/python/semantic_kernel/contents/function_call_content.py b/python/semantic_kernel/contents/function_call_content.py index e4ca2338111b..7a1d8cbec535 100644 --- a/python/semantic_kernel/contents/function_call_content.py +++ b/python/semantic_kernel/contents/function_call_content.py @@ -28,7 +28,7 @@ _T = TypeVar("_T", bound="FunctionCallContent") -EMPTY_VALUES: Final[list[str | None]] = ["", "{}", None] +EMPTY_VALUES: Final[list[str | None]] = ["", None] class FunctionCallContent(KernelContent): @@ -146,8 +146,24 @@ def combine_arguments( return arg2 or "{}" if arg2 in EMPTY_VALUES: return arg1 or "{}" + # A literal "{}" is only a placeholder when it stands next to a complete value. As the accumulated + # first side it is a finished empty object, so anything that follows replaces it. As the second side + # it is dropped only when the first side already parses as a complete object, otherwise it is a real + # delta (for example the value of a nested key) and must be kept. + if arg1 == "{}": + return arg2 or "{}" + if arg2 == "{}" and self._is_complete_object(arg1): + return arg1 or "{}" return (arg1 or "") + (arg2 or "") + @staticmethod + def _is_complete_object(value: str | None) -> bool: + """Check if the value is a complete JSON object.""" + try: + return isinstance(json.loads(value or ""), Mapping) + except json.JSONDecodeError: + return False + def parse_arguments(self) -> Mapping[str, Any] | None: """Parse the arguments into a dictionary.""" if not self.arguments: diff --git a/python/tests/unit/contents/test_function_call_content.py b/python/tests/unit/contents/test_function_call_content.py index 6cdfc4557e99..5278096a3375 100644 --- a/python/tests/unit/contents/test_function_call_content.py +++ b/python/tests/unit/contents/test_function_call_content.py @@ -1,5 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. +from functools import reduce + import pytest from semantic_kernel.contents.function_call_content import FunctionCallContent @@ -74,6 +76,30 @@ def test_add_empty(): assert fc3.arguments == """{"input2": "world2"}""" +@pytest.mark.parametrize( + "deltas", + [ + ['{"query":"x","filters":', "{}", "}"], + ['{"a":[', "{}", ",", "{}", "]}"], + ['{"a": {"b":', "{}", "}}"], + ], +) +def test_add_streamed_empty_object_delta_is_kept(deltas: list[str]): + fcs = [ + FunctionCallContent(id="test1" if i == 0 else None, index=0, name="Test-Function", arguments=d) + for i, d in enumerate(deltas) + ] + result = reduce(lambda a, b: a + b, fcs) + assert result.arguments == "".join(deltas) + assert result.parse_arguments() is not None + + +def test_add_trailing_empty_placeholder_after_complete_object(): + fc1 = FunctionCallContent(id="test1", name="Test-Function", arguments="""{"input2": "world2"}""") + fc2 = FunctionCallContent(id="test1", name="Test-Function", arguments="{}") + assert (fc1 + fc2).arguments == """{"input2": "world2"}""" + + def test_add_none(function_call: FunctionCallContent): # Test adding two function calls with one being None fc2 = None From 04878684697265bb910818276b63dbc177ea38b6 Mon Sep 17 00:00:00 2001 From: VANDRANKI Date: Thu, 1 Oct 2026 13:01:22 -0400 Subject: [PATCH 2/2] Python: treat single-quoted complete objects as complete when dropping a trailing '{}' placeholder --- python/semantic_kernel/contents/function_call_content.py | 5 +++++ python/tests/unit/contents/test_function_call_content.py | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/python/semantic_kernel/contents/function_call_content.py b/python/semantic_kernel/contents/function_call_content.py index 7a1d8cbec535..506c47fc8573 100644 --- a/python/semantic_kernel/contents/function_call_content.py +++ b/python/semantic_kernel/contents/function_call_content.py @@ -161,6 +161,11 @@ def _is_complete_object(value: str | None) -> bool: """Check if the value is a complete JSON object.""" try: return isinstance(json.loads(value or ""), Mapping) + except json.JSONDecodeError: + pass + try: + # Same single quote preprocessing as parse_arguments. + return isinstance(json.loads(re.sub(r"(?