Python: serialize the result as string, consistent with behavior before 1.2 (#7528)

### Motivation and Context

<!-- Thank you for your contribution to the semantic-kernel repo!
Please help reviewers and future users, providing the following
information:
  1. Why is this change required?
  2. What problem does it solve?
  3. What scenario does it contribute to?
  4. If it fixes an open issue, please link to the issue here.
-->
Fix #7340

This adds a serializer to the FunctionResultContent for the result
field.

Before SK 1.2 all results where stored as string using `str(value)`,
after that was changed to Any, to allow more complex types to be stored
with full details.

This changes things back to that behavior for serialization by adding a
pydantic field_serializer for that field which uses the same method
(`str(value)`). Further this allows one to subclass
FunctionResultContent and overwrite the built-in serializer with your
own.

### Description

<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->

### Contribution Checklist

<!-- Before submitting this PR, please make sure: -->

- [x] The code builds clean without any errors or warnings
- [x] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [x] All unit tests pass, and I have added new tests where possible
- [x] I didn't break anyone 😄
This commit is contained in:
Eduard van Valkenburg
2024-07-29 18:00:25 +00:00
committed by GitHub
parent d3172f8f36
commit d54f39100d
3 changed files with 40 additions and 1 deletions
@@ -3,7 +3,7 @@
from typing import TYPE_CHECKING, Any, ClassVar, Literal, TypeVar
from xml.etree.ElementTree import Element # nosec
from pydantic import Field
from pydantic import Field, field_serializer
from typing_extensions import deprecated
from semantic_kernel.contents.const import FUNCTION_RESULT_CONTENT_TAG, TEXT_CONTENT_TAG, ContentTypes
@@ -169,3 +169,8 @@ class FunctionResultContent(KernelContent):
def split_name(self) -> list[str]:
"""Split the name into a plugin and function name."""
return [self.plugin_name or "", self.function_name]
@field_serializer("result")
def serialize_result(self, value: Any) -> str:
"""Serialize the result."""
return str(value)
@@ -572,3 +572,21 @@ def test_to_from_file(chat_history: ChatHistory, tmp_path):
assert chat_history_2.messages[2] == chat_history.messages[2]
assert chat_history_2.messages[3] == chat_history.messages[3]
assert chat_history_2.messages[4] == chat_history.messages[4]
def test_chat_history_serialize(chat_history: ChatHistory):
class CustomResultClass:
def __init__(self, result):
self.result = result
def __str__(self) -> str:
return self.result
custom_result = CustomResultClass(result="CustomResultTestValue")
chat_history.add_system_message("You are an AI assistant")
chat_history.add_user_message("What is the weather in Seattle?")
chat_history.add_assistant_message(
[FunctionCallContent(id="test1", name="WeatherPlugin-GetWeather", arguments='{{ "location": "Seattle" }}')]
)
chat_history.add_tool_message([FunctionResultContent(id="test1", result=custom_result)])
assert "CustomResultTestValue" in chat_history.serialize()
@@ -83,3 +83,19 @@ def test_to_cmc(unwrap: bool):
assert cmc.items[0].text == "test-result"
else:
assert cmc.items[0].result == "test-result"
def test_serialize():
class CustomResultClass:
def __init__(self, result):
self.result = result
def __str__(self) -> str:
return self.result
custom_result = CustomResultClass(result="test")
frc = FunctionResultContent(id="test", name="test-function", result=custom_result)
assert (
frc.model_dump_json(exclude_none=True)
== """{"metadata":{},"content_type":"function_result","id":"test","result":"test","name":"test-function","function_name":"function","plugin_name":"test"}""" # noqa: E501
)