213 lines
8.5 KiB
Python
213 lines
8.5 KiB
Python
# Copyright (c) Microsoft. All rights reserved.
|
|
|
|
from typing import TYPE_CHECKING, Any, ClassVar, Literal, TypeVar
|
|
from xml.etree.ElementTree import Element # nosec
|
|
|
|
from pydantic import Field, field_serializer
|
|
from typing_extensions import deprecated
|
|
|
|
from semantic_kernel.const import DEFAULT_FULLY_QUALIFIED_NAME_SEPARATOR
|
|
from semantic_kernel.contents.const import FUNCTION_RESULT_CONTENT_TAG, TEXT_CONTENT_TAG, ContentTypes
|
|
from semantic_kernel.contents.image_content import ImageContent
|
|
from semantic_kernel.contents.kernel_content import KernelContent
|
|
from semantic_kernel.contents.text_content import TextContent
|
|
from semantic_kernel.contents.utils.author_role import AuthorRole
|
|
from semantic_kernel.contents.utils.hashing import make_hashable
|
|
from semantic_kernel.exceptions.content_exceptions import ContentInitializationError
|
|
|
|
if TYPE_CHECKING:
|
|
from semantic_kernel.contents.chat_message_content import ChatMessageContent
|
|
from semantic_kernel.contents.function_call_content import FunctionCallContent
|
|
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
|
|
from semantic_kernel.functions.function_result import FunctionResult
|
|
|
|
TAG_CONTENT_MAP = {
|
|
TEXT_CONTENT_TAG: TextContent,
|
|
}
|
|
|
|
_T = TypeVar("_T", bound="FunctionResultContent")
|
|
|
|
|
|
class FunctionResultContent(KernelContent):
|
|
"""This class represents function result content."""
|
|
|
|
content_type: Literal[ContentTypes.FUNCTION_RESULT_CONTENT] = Field(FUNCTION_RESULT_CONTENT_TAG, init=False) # type: ignore
|
|
tag: ClassVar[str] = FUNCTION_RESULT_CONTENT_TAG
|
|
id: str | None = None
|
|
call_id: str | None = None
|
|
result: Any
|
|
name: str | None = None
|
|
function_name: str
|
|
plugin_name: str | None = None
|
|
encoding: str | None = None
|
|
|
|
def __init__(
|
|
self,
|
|
inner_content: Any | None = None,
|
|
ai_model_id: str | None = None,
|
|
id: str | None = None,
|
|
call_id: str | None = None,
|
|
name: str | None = None,
|
|
function_name: str | None = None,
|
|
plugin_name: str | None = None,
|
|
result: Any | None = None,
|
|
encoding: str | None = None,
|
|
metadata: dict[str, Any] | None = None,
|
|
**kwargs: Any,
|
|
) -> None:
|
|
"""Create function result content.
|
|
|
|
Args:
|
|
inner_content (Any | None): The inner content.
|
|
ai_model_id (str | None): The id of the AI model.
|
|
id (str | None): The id of the function call that the result relates to.
|
|
call_id (str | None): The call id of the function call from the Responses API.
|
|
name (str | None): The name of the function.
|
|
When not supplied function_name and plugin_name should be supplied.
|
|
function_name (str | None): The function name.
|
|
Not used when 'name' is supplied.
|
|
plugin_name (str | None): The plugin name.
|
|
Not used when 'name' is supplied.
|
|
result (Any | None): The result of the function.
|
|
encoding (str | None): The encoding of the result.
|
|
metadata (dict[str, Any] | None): The metadata of the function call.
|
|
kwargs (Any): Additional arguments.
|
|
"""
|
|
if function_name and plugin_name and not name:
|
|
name = f"{plugin_name}{DEFAULT_FULLY_QUALIFIED_NAME_SEPARATOR}{function_name}"
|
|
if name and not function_name and not plugin_name:
|
|
if DEFAULT_FULLY_QUALIFIED_NAME_SEPARATOR in name:
|
|
plugin_name, function_name = name.split(DEFAULT_FULLY_QUALIFIED_NAME_SEPARATOR, maxsplit=1)
|
|
else:
|
|
function_name = name
|
|
args = {
|
|
"inner_content": inner_content,
|
|
"ai_model_id": ai_model_id,
|
|
"id": id,
|
|
"name": name,
|
|
"function_name": function_name or "",
|
|
"plugin_name": plugin_name,
|
|
"result": result,
|
|
"encoding": encoding,
|
|
}
|
|
if call_id:
|
|
args["call_id"] = call_id
|
|
if metadata:
|
|
args["metadata"] = metadata
|
|
|
|
super().__init__(**args)
|
|
|
|
def __str__(self) -> str:
|
|
"""Return the text of the response."""
|
|
return str(self.result)
|
|
|
|
def to_element(self) -> Element:
|
|
"""Convert the instance to an Element."""
|
|
element = Element(self.tag)
|
|
if self.id:
|
|
element.set("id", self.id)
|
|
if self.name:
|
|
element.set("name", self.name)
|
|
element.text = str(self.result)
|
|
return element
|
|
|
|
@classmethod
|
|
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))
|
|
|
|
@classmethod
|
|
def from_function_call_content_and_result(
|
|
cls: type[_T],
|
|
function_call_content: "FunctionCallContent",
|
|
result: "FunctionResult | TextContent | ChatMessageContent | Any",
|
|
metadata: dict[str, Any] | None = None,
|
|
) -> _T:
|
|
"""Create an instance from a FunctionCallContent and a result."""
|
|
from semantic_kernel.contents.chat_message_content import ChatMessageContent
|
|
from semantic_kernel.functions.function_result import FunctionResult
|
|
|
|
metadata = metadata or {}
|
|
metadata = metadata | (function_call_content.metadata or {})
|
|
metadata = metadata | getattr(result, "metadata", {})
|
|
inner_content = result
|
|
if isinstance(result, FunctionResult):
|
|
result = result.value
|
|
if isinstance(result, TextContent):
|
|
res = result.text
|
|
elif isinstance(result, ChatMessageContent):
|
|
if isinstance(result.items[0], TextContent):
|
|
res = result.items[0].text
|
|
elif isinstance(result.items[0], ImageContent):
|
|
res = result.items[0].data_uri
|
|
elif isinstance(result.items[0], FunctionResultContent):
|
|
res = result.items[0].result
|
|
res = str(result)
|
|
else:
|
|
res = result
|
|
return cls(
|
|
id=function_call_content.id or "unknown",
|
|
call_id=function_call_content.call_id if hasattr(function_call_content, "call_id") else None,
|
|
inner_content=inner_content,
|
|
result=res,
|
|
function_name=function_call_content.function_name,
|
|
plugin_name=function_call_content.plugin_name,
|
|
ai_model_id=function_call_content.ai_model_id,
|
|
metadata=metadata,
|
|
)
|
|
|
|
def to_chat_message_content(self) -> "ChatMessageContent":
|
|
"""Convert the instance to a ChatMessageContent."""
|
|
from semantic_kernel.contents.chat_message_content import ChatMessageContent
|
|
|
|
return ChatMessageContent(role=AuthorRole.TOOL, items=[self])
|
|
|
|
def to_streaming_chat_message_content(self) -> "StreamingChatMessageContent":
|
|
"""Convert the instance to a StreamingChatMessageContent."""
|
|
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
|
|
|
|
return StreamingChatMessageContent(role=AuthorRole.TOOL, choice_index=0, items=[self])
|
|
|
|
def to_dict(self) -> dict[str, str | Any]:
|
|
"""Convert the instance to a dictionary."""
|
|
return {
|
|
"tool_call_id": self.id,
|
|
"content": self.result,
|
|
}
|
|
|
|
@deprecated("The function_name and plugin_name attributes should be used instead.")
|
|
def split_name(self) -> list[str]:
|
|
"""Split the name into a plugin and function name."""
|
|
return [self.plugin_name or "", self.function_name]
|
|
|
|
def custom_fully_qualified_name(self, separator: str) -> str:
|
|
"""Get the fully qualified name of the function with a custom separator.
|
|
|
|
Args:
|
|
separator (str): The custom separator.
|
|
|
|
Returns:
|
|
The fully qualified name of the function with a custom separator.
|
|
"""
|
|
return f"{self.plugin_name}{separator}{self.function_name}" if self.plugin_name else self.function_name
|
|
|
|
@field_serializer("result")
|
|
def serialize_result(self, value: Any) -> str:
|
|
"""Serialize the result."""
|
|
return str(value)
|
|
|
|
def __hash__(self) -> int:
|
|
"""Return the hash of the function result content."""
|
|
hashable_result = make_hashable(self.result)
|
|
return hash((
|
|
self.tag,
|
|
self.id,
|
|
hashable_result,
|
|
self.name,
|
|
self.function_name,
|
|
self.plugin_name,
|
|
self.encoding,
|
|
))
|