Files
semantic-kernel/python/semantic_kernel/contents/function_call_content.py
T
Evan Mattson 9a44506220 Python: add new samples and fix streaming tool call FunctionCallContent formation (#5877)
### Motivation and Context

We're working towards creating a core set of syntax examples (there are
four total). The core examples will be available in all 3 SK languages.

<!-- 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.
-->

### Description

This PR introduces two (of the four) new kernel syntax examples, which
will align with the new kernel examples coming soon for both dotnet and
Java. #5784
- Introduce a custom weather plugin that in conjunction with the core
TimePlugin, make use of auto function calling.
- Introduce a kernel syntax example that shows how to integrate with the
Microsoft Graph API to create a "restaurant booking." Note: this doesn't
actually place a real reservation, but shows how to interact with
msgraph.
- Also fixes an issue where the streaming tool call argument formation
was broken. Closes #6106

<!-- 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 😄
2024-05-02 22:15:07 +00:00

98 lines
3.6 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import json
import logging
from typing import TYPE_CHECKING, Any
from xml.etree.ElementTree import Element
from semantic_kernel.contents.const import FUNCTION_CALL_CONTENT_TAG
from semantic_kernel.contents.kernel_content import KernelContent
from semantic_kernel.exceptions import FunctionCallInvalidArgumentsException, FunctionCallInvalidNameException
if TYPE_CHECKING:
from semantic_kernel.functions.kernel_arguments import KernelArguments
logger = logging.getLogger(__name__)
class FunctionCallContent(KernelContent):
"""Class to hold a function call response."""
id: str | None
index: int | None = None
name: str | None = None
arguments: str | None = None
def __str__(self) -> str:
return f"{self.name}({self.arguments})"
def __add__(self, other: "FunctionCallContent | None") -> "FunctionCallContent":
"""Add two function calls together, combines the arguments, ignores the name."""
if not other:
return self
if self.id and other.id and self.id != other.id:
raise ValueError("Function calls have different ids.")
if self.index != other.index:
raise ValueError("Function calls have different indexes.")
return FunctionCallContent(
id=self.id or other.id,
index=self.index or other.index,
name=self.name or other.name,
arguments=(self.arguments or "") + (other.arguments or ""),
)
def parse_arguments(self) -> dict[str, Any] | None:
"""Parse the arguments into a dictionary."""
if not self.arguments:
return None
try:
return json.loads(self.arguments)
except json.JSONDecodeError as exc:
raise FunctionCallInvalidArgumentsException("Function Call arguments are not valid JSON.") from exc
def to_kernel_arguments(self) -> "KernelArguments":
"""Return the arguments as a KernelArguments instance."""
from semantic_kernel.functions.kernel_arguments import KernelArguments
args = self.parse_arguments()
if not args:
return KernelArguments()
return KernelArguments(**args)
def split_name(self) -> list[str]:
"""Split the name into a plugin and function name."""
if not self.name:
raise FunctionCallInvalidNameException("Name is not set.")
if "-" not in self.name:
return ["", self.name]
return self.name.split("-", maxsplit=1)
def split_name_dict(self) -> dict:
"""Split the name into a plugin and function name."""
parts = self.split_name()
return {"plugin_name": parts[0], "function_name": parts[1]}
def to_element(self) -> Element:
"""Convert the function call to an Element."""
element = Element(FUNCTION_CALL_CONTENT_TAG)
if self.id:
element.set("id", self.id)
if self.name:
element.set("name", self.name)
if self.arguments:
element.text = self.arguments
return element
@classmethod
def from_element(cls, element: Element) -> "FunctionCallContent":
"""Create an instance from an Element."""
if element.tag != FUNCTION_CALL_CONTENT_TAG:
raise ValueError(f"Element tag is not {FUNCTION_CALL_CONTENT_TAG}")
return cls(name=element.get("name"), id=element.get("id"), arguments=element.text or "")
def to_dict(self) -> dict[str, str | Any]:
"""Convert the instance to a dictionary."""
return {"id": self.id, "type": "function", "function": {"name": self.name, "arguments": self.arguments}}