Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion python/semantic_kernel/contents/function_call_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ def to_element(self) -> Element:
element = Element(self.tag)
if self.id:
element.set("id", self.id)
if self.call_id:
element.set("call_id", self.call_id)
if self.name:
element.set("name", self.name)
if self.arguments:
Expand All @@ -217,7 +219,12 @@ def from_element(cls: type[_T], element: Element) -> _T:
if element.tag != cls.tag:
raise ContentInitializationError(f"Element tag is not {cls.tag}") # pragma: no cover

return cls(name=element.get("name"), id=element.get("id"), arguments=element.text or "")
return cls(
name=element.get("name"),
id=element.get("id"),
call_id=element.get("call_id"),
arguments=element.text or "",
)

def to_dict(self) -> dict[str, str | Any]:
"""Convert the instance to a dictionary."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ def to_element(self) -> Element:
element = Element(self.tag)
if self.id:
element.set("id", self.id)
if self.call_id:
element.set("call_id", self.call_id)
if self.name:
element.set("name", self.name)
element.text = str(self.result)
Expand All @@ -116,7 +118,12 @@ 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))
return cls(
id=element.get("id", ""),
call_id=element.get("call_id"),
result=element.text,
name=element.get("name", None),
)

@classmethod
def from_function_call_content_and_result(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,40 @@
from semantic_kernel.functions import KernelArguments


@pytest.mark.parametrize("store_enabled", [False, True])
def test_prepare_request_preserves_call_ids_after_xml_roundtrip(store_enabled):
from semantic_kernel.contents import ChatHistory, FunctionCallContent, FunctionResultContent

function_call = FunctionCallContent(
id="fc_item_123", call_id="call_correlation_456", name="weather", arguments='{"city":"Seattle"}'
)
history = ChatHistory(
messages=[
ChatMessageContent(role=AuthorRole.ASSISTANT, items=[function_call]),
ChatMessageContent(
role=AuthorRole.TOOL,
items=[FunctionResultContent.from_function_call_content_and_result(function_call, "Sunny")],
),
]
)

restored = ChatHistory.from_rendered_prompt(history.to_prompt())
request = ResponsesAgentThreadActions._prepare_chat_history_for_request(restored, store_enabled=store_enabled)

expected = [{"type": "function_call_output", "call_id": "call_correlation_456", "output": "Sunny"}]
if not store_enabled:
expected.insert(
0,
{
"type": "function_call",
"call_id": "call_correlation_456",
"name": "weather",
"arguments": '{"city":"Seattle"}',
},
)
assert request == expected


@pytest.fixture
def mock_agent():
agent = AsyncMock(spec=OpenAIResponsesAgent)
Expand Down
28 changes: 28 additions & 0 deletions python/tests/unit/contents/test_chat_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,34 @@ def test_init_with_messages_only():
assert chat_history.messages == msgs, "Chat history should contain exactly the provided messages"


@pytest.mark.parametrize("call_id", [None, "call_correlation_456"])
@pytest.mark.parametrize("serializer", [str, ChatHistory.to_prompt])
def test_xml_roundtrip_preserves_function_call_ids(call_id, serializer):
function_call = FunctionCallContent(
id="fc_item_123", call_id=call_id, name="weather-get_weather", arguments='{"city":"Seattle"}'
)
function_result = FunctionResultContent.from_function_call_content_and_result(function_call, "Sunny")
history = ChatHistory(
messages=[
ChatMessageContent(role=AuthorRole.ASSISTANT, items=[function_call]),
ChatMessageContent(role=AuthorRole.TOOL, items=[function_result]),
]
)

restored = ChatHistory.from_rendered_prompt(serializer(history))

assert len(restored.messages) == 2
restored_call = restored.messages[0].items[0]
restored_result = restored.messages[1].items[0]
assert isinstance(restored_call, FunctionCallContent)
assert isinstance(restored_result, FunctionResultContent)
assert restored_call.id == restored_result.id == "fc_item_123"
assert restored_call.call_id == restored_result.call_id == call_id
assert restored_call.name == restored_result.name == "weather-get_weather"
assert restored_call.arguments == '{"city":"Seattle"}'
assert restored_result.result == "Sunny"


def test_init_with_messages_and_system_message():
system_msg = "a test system prompt"
msgs = [ChatMessageContent(role=AuthorRole.USER, content=f"Message {i}") for i in range(3)]
Expand Down
Loading