chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from semantic_kernel.functions.function_result import FunctionResult
|
||||
from semantic_kernel.functions.kernel_arguments import KernelArguments
|
||||
from semantic_kernel.functions.kernel_function import KernelFunction
|
||||
from semantic_kernel.functions.kernel_function_decorator import kernel_function
|
||||
from semantic_kernel.functions.kernel_function_from_method import KernelFunctionFromMethod
|
||||
from semantic_kernel.functions.kernel_function_from_prompt import KernelFunctionFromPrompt
|
||||
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
|
||||
from semantic_kernel.functions.kernel_parameter_metadata import KernelParameterMetadata
|
||||
from semantic_kernel.functions.kernel_plugin import KernelPlugin
|
||||
|
||||
__all__ = [
|
||||
"FunctionResult",
|
||||
"KernelArguments",
|
||||
"KernelFunction",
|
||||
"KernelFunctionFromMethod",
|
||||
"KernelFunctionFromPrompt",
|
||||
"KernelFunctionMetadata",
|
||||
"KernelParameterMetadata",
|
||||
"KernelPlugin",
|
||||
"kernel_function",
|
||||
]
|
||||
@@ -0,0 +1,68 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from semantic_kernel.contents.kernel_content import KernelContent
|
||||
from semantic_kernel.exceptions import FunctionResultError
|
||||
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
|
||||
from semantic_kernel.kernel_pydantic import KernelBaseModel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FunctionResult(KernelBaseModel):
|
||||
"""The result of a function.
|
||||
|
||||
Args:
|
||||
function: The metadata of the function that was invoked.
|
||||
value: The value of the result.
|
||||
rendered_prompt: The rendered prompt of the result.
|
||||
metadata: The metadata of the result.
|
||||
|
||||
Methods:
|
||||
__str__: Get the string representation of the result, will call str() on the value,
|
||||
or if the value is a list, will call str() on the first element of the list.
|
||||
get_inner_content: Get the inner content of the function result
|
||||
when that is a KernelContent or subclass of the first item of the value if it is a list.
|
||||
|
||||
"""
|
||||
|
||||
function: KernelFunctionMetadata
|
||||
value: Any
|
||||
rendered_prompt: str | None = None
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Get the string representation of the result."""
|
||||
if self.value:
|
||||
try:
|
||||
if isinstance(self.value, list):
|
||||
return (
|
||||
str(self.value[0])
|
||||
if isinstance(self.value[0], KernelContent)
|
||||
else ",".join(map(str, self.value))
|
||||
)
|
||||
if isinstance(self.value, dict):
|
||||
# TODO (eavanvalkenburg): remove this once function result doesn't include input args
|
||||
# This is so an integration test can pass.
|
||||
return str(list(self.value.values())[-1])
|
||||
return str(self.value)
|
||||
except Exception as e:
|
||||
raise FunctionResultError(f"Failed to convert value to string: {e}") from e
|
||||
else:
|
||||
return ""
|
||||
|
||||
def get_inner_content(self, index: int = 0) -> Any | None:
|
||||
"""Get the inner content of the function result.
|
||||
|
||||
Args:
|
||||
index (int): The index of the inner content if the inner content is a list, default 0.
|
||||
"""
|
||||
if isinstance(self.value, list) and isinstance(self.value[index], KernelContent):
|
||||
return self.value[index].inner_content
|
||||
if isinstance(self.value, KernelContent):
|
||||
return self.value.inner_content
|
||||
return None
|
||||
@@ -0,0 +1,122 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import json
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from semantic_kernel.const import DEFAULT_SERVICE_NAME
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable
|
||||
|
||||
from _typeshed import SupportsKeysAndGetItem
|
||||
|
||||
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
|
||||
|
||||
|
||||
class KernelArguments(dict):
|
||||
"""The arguments sent to the KernelFunction."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
settings: (
|
||||
"PromptExecutionSettings | list[PromptExecutionSettings] | dict[str, PromptExecutionSettings] | None"
|
||||
) = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""Initializes a new instance of the KernelArguments class.
|
||||
|
||||
This is a dict-like class with the additional field for the execution_settings.
|
||||
|
||||
This class is derived from a dict, hence behaves the same way,
|
||||
just adds the execution_settings as a dict, with service_id and the settings.
|
||||
|
||||
Args:
|
||||
settings (PromptExecutionSettings | List[PromptExecutionSettings] | None):
|
||||
The settings for the execution.
|
||||
If a list is given, make sure all items in the list have a unique service_id
|
||||
as that is used as the key for the dict.
|
||||
**kwargs (dict[str, Any]): The arguments for the function invocation, works similar to a regular dict.
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
settings_dict = None
|
||||
if settings:
|
||||
settings_dict = {}
|
||||
if isinstance(settings, dict):
|
||||
settings_dict = settings
|
||||
elif isinstance(settings, list):
|
||||
settings_dict = {s.service_id or DEFAULT_SERVICE_NAME: s for s in settings}
|
||||
else:
|
||||
settings_dict = {settings.service_id or DEFAULT_SERVICE_NAME: settings}
|
||||
self.execution_settings: dict[str, "PromptExecutionSettings"] | None = settings_dict
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
"""Returns True if the arguments have any values."""
|
||||
has_arguments = self.__len__() > 0
|
||||
has_execution_settings = self.execution_settings is not None and len(self.execution_settings) > 0
|
||||
return has_arguments or has_execution_settings
|
||||
|
||||
def __or__(self, value: dict) -> "KernelArguments":
|
||||
"""Merges a KernelArguments with another KernelArguments or dict.
|
||||
|
||||
This implements the `|` operator for KernelArguments.
|
||||
"""
|
||||
if not isinstance(value, dict):
|
||||
raise TypeError(
|
||||
f"TypeError: unsupported operand type(s) for |: '{type(self).__name__}' and '{type(value).__name__}'"
|
||||
)
|
||||
|
||||
# Merge execution settings
|
||||
new_execution_settings = (self.execution_settings or {}).copy()
|
||||
if isinstance(value, KernelArguments) and value.execution_settings:
|
||||
new_execution_settings |= value.execution_settings
|
||||
# Create a new KernelArguments with merged dict values
|
||||
return KernelArguments(settings=new_execution_settings, **(dict(self) | dict(value)))
|
||||
|
||||
def __ror__(self, value: dict) -> "KernelArguments":
|
||||
"""Merges a dict with a KernelArguments.
|
||||
|
||||
This implements the right-side `|` operator for KernelArguments.
|
||||
"""
|
||||
if not isinstance(value, dict):
|
||||
raise TypeError(
|
||||
f"TypeError: unsupported operand type(s) for |: '{type(value).__name__}' and '{type(self).__name__}'"
|
||||
)
|
||||
|
||||
# Merge execution settings
|
||||
new_execution_settings = {}
|
||||
if isinstance(value, KernelArguments) and value.execution_settings:
|
||||
new_execution_settings = value.execution_settings.copy()
|
||||
if self.execution_settings:
|
||||
new_execution_settings |= self.execution_settings
|
||||
|
||||
# Create a new KernelArguments with merged dict values
|
||||
return KernelArguments(settings=new_execution_settings, **(dict(value) | dict(self)))
|
||||
|
||||
def __ior__(self, value: "SupportsKeysAndGetItem[Any, Any] | Iterable[tuple[Any, Any]]") -> "KernelArguments":
|
||||
"""Merges into this KernelArguments with another KernelArguments or dict (in-place)."""
|
||||
self.update(value)
|
||||
|
||||
# In-place merge execution settings
|
||||
if isinstance(value, KernelArguments) and value.execution_settings:
|
||||
if self.execution_settings:
|
||||
self.execution_settings.update(value.execution_settings)
|
||||
else:
|
||||
self.execution_settings = value.execution_settings.copy()
|
||||
|
||||
return self
|
||||
|
||||
def dumps(self, include_execution_settings: bool = False) -> str:
|
||||
"""Serializes the KernelArguments to a JSON string."""
|
||||
data = dict(self)
|
||||
if include_execution_settings and self.execution_settings:
|
||||
data["execution_settings"] = self.execution_settings
|
||||
|
||||
def default(obj):
|
||||
if isinstance(obj, BaseModel):
|
||||
return obj.model_dump()
|
||||
|
||||
return str(obj)
|
||||
|
||||
return json.dumps(data, default=default)
|
||||
@@ -0,0 +1,483 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from abc import abstractmethod
|
||||
from collections.abc import AsyncGenerator, Callable, Mapping, Sequence
|
||||
from copy import copy, deepcopy
|
||||
from inspect import isasyncgen, isgenerator
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from opentelemetry import metrics, trace
|
||||
from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from semantic_kernel.filters.filter_types import FilterTypes
|
||||
from semantic_kernel.filters.functions.function_invocation_context import FunctionInvocationContext
|
||||
from semantic_kernel.filters.kernel_filters_extension import _rebuild_function_invocation_context
|
||||
from semantic_kernel.functions.function_result import FunctionResult
|
||||
from semantic_kernel.functions.kernel_arguments import KernelArguments
|
||||
from semantic_kernel.functions.kernel_function_log_messages import KernelFunctionLogMessages
|
||||
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
|
||||
from semantic_kernel.functions.kernel_parameter_metadata import KernelParameterMetadata
|
||||
from semantic_kernel.kernel_pydantic import KernelBaseModel
|
||||
from semantic_kernel.prompt_template.const import (
|
||||
HANDLEBARS_TEMPLATE_FORMAT_NAME,
|
||||
JINJA2_TEMPLATE_FORMAT_NAME,
|
||||
KERNEL_TEMPLATE_FORMAT_NAME,
|
||||
TEMPLATE_FORMAT_TYPES,
|
||||
)
|
||||
from semantic_kernel.prompt_template.handlebars_prompt_template import HandlebarsPromptTemplate
|
||||
from semantic_kernel.prompt_template.jinja2_prompt_template import Jinja2PromptTemplate
|
||||
from semantic_kernel.prompt_template.kernel_prompt_template import KernelPromptTemplate
|
||||
from semantic_kernel.prompt_template.prompt_template_base import PromptTemplateBase
|
||||
from semantic_kernel.utils.telemetry.model_diagnostics import function_tracer
|
||||
from semantic_kernel.utils.telemetry.model_diagnostics.gen_ai_attributes import TOOL_CALL_ARGUMENTS, TOOL_CALL_RESULT
|
||||
|
||||
from ..contents.chat_message_content import ChatMessageContent
|
||||
from ..contents.text_content import TextContent
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
|
||||
from semantic_kernel.contents.streaming_content_mixin import StreamingContentMixin
|
||||
from semantic_kernel.functions.kernel_function_from_method import KernelFunctionFromMethod
|
||||
from semantic_kernel.functions.kernel_function_from_prompt import KernelFunctionFromPrompt
|
||||
from semantic_kernel.kernel import Kernel
|
||||
from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig
|
||||
|
||||
# Logger, tracer and meter for observability
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
tracer: trace.Tracer = trace.get_tracer(__name__)
|
||||
meter: metrics.Meter = metrics.get_meter_provider().get_meter(__name__)
|
||||
MEASUREMENT_FUNCTION_TAG_NAME: str = "semantic_kernel.function.name"
|
||||
|
||||
TEMPLATE_FORMAT_MAP: dict[TEMPLATE_FORMAT_TYPES, type[PromptTemplateBase]] = {
|
||||
KERNEL_TEMPLATE_FORMAT_NAME: KernelPromptTemplate,
|
||||
HANDLEBARS_TEMPLATE_FORMAT_NAME: HandlebarsPromptTemplate,
|
||||
JINJA2_TEMPLATE_FORMAT_NAME: Jinja2PromptTemplate,
|
||||
}
|
||||
|
||||
|
||||
def _create_function_duration_histogram():
|
||||
return meter.create_histogram(
|
||||
"semantic_kernel.function.invocation.duration",
|
||||
unit="s",
|
||||
description="Measures the duration of a function's execution",
|
||||
)
|
||||
|
||||
|
||||
def _create_function_streaming_duration_histogram():
|
||||
return meter.create_histogram(
|
||||
"semantic_kernel.function.streaming.duration",
|
||||
unit="s",
|
||||
description="Measures the duration of a function's streaming execution",
|
||||
)
|
||||
|
||||
|
||||
class KernelFunction(KernelBaseModel):
|
||||
"""Semantic Kernel function.
|
||||
|
||||
Attributes:
|
||||
name (str): The name of the function. Must be upper/lower case letters and
|
||||
underscores with a minimum length of 1.
|
||||
plugin_name (str): The name of the plugin that contains this function. Must be upper/lower
|
||||
case letters and underscores with a minimum length of 1.
|
||||
description (Optional[str]): The description of the function.
|
||||
is_prompt (bool): Whether the function is semantic.
|
||||
stream_function (Optional[Callable[..., Any]]): The stream function for the function.
|
||||
parameters (List[KernelParameterMetadata]): The parameters for the function.
|
||||
return_parameter (Optional[KernelParameterMetadata]): The return parameter for the function.
|
||||
function (Callable[..., Any]): The function to call.
|
||||
prompt_execution_settings (PromptExecutionSettings): The AI prompt execution settings.
|
||||
prompt_template_config (PromptTemplateConfig): The prompt template configuration.
|
||||
metadata (Optional[KernelFunctionMetadata]): The metadata for the function.
|
||||
"""
|
||||
|
||||
# some attributes are now properties, still listed here for documentation purposes
|
||||
|
||||
metadata: KernelFunctionMetadata
|
||||
|
||||
invocation_duration_histogram: metrics.Histogram = Field(
|
||||
default_factory=_create_function_duration_histogram, exclude=True
|
||||
)
|
||||
streaming_duration_histogram: metrics.Histogram = Field(
|
||||
default_factory=_create_function_streaming_duration_histogram, exclude=True
|
||||
)
|
||||
|
||||
def __deepcopy__(self, memo: dict[int, Any] | None = None) -> "KernelFunction":
|
||||
"""Create a deep copy of the kernel function, recreating uncopyable fields."""
|
||||
if memo is None:
|
||||
memo = {}
|
||||
if id(self) in memo:
|
||||
return memo[id(self)]
|
||||
|
||||
# Use model_copy to create a shallow copy of the pydantic model
|
||||
# this is the recommended way to copy pydantic models
|
||||
new_obj = self.model_copy(deep=False)
|
||||
memo[id(self)] = new_obj
|
||||
|
||||
# now deepcopy the fields that are not the histograms
|
||||
for key, value in self.__dict__.items():
|
||||
if key not in ("invocation_duration_histogram", "streaming_duration_histogram"):
|
||||
setattr(new_obj, key, deepcopy(value, memo))
|
||||
|
||||
return new_obj
|
||||
|
||||
@classmethod
|
||||
def from_prompt(
|
||||
cls,
|
||||
function_name: str,
|
||||
plugin_name: str,
|
||||
description: str | None = None,
|
||||
prompt: str | None = None,
|
||||
template_format: TEMPLATE_FORMAT_TYPES = KERNEL_TEMPLATE_FORMAT_NAME,
|
||||
prompt_template: "PromptTemplateBase | None " = None,
|
||||
prompt_template_config: "PromptTemplateConfig | None" = None,
|
||||
prompt_execution_settings: (
|
||||
"PromptExecutionSettings | Sequence[PromptExecutionSettings] | Mapping[str, PromptExecutionSettings] | None"
|
||||
) = None,
|
||||
) -> "KernelFunctionFromPrompt":
|
||||
"""Create a new instance of the KernelFunctionFromPrompt class."""
|
||||
from semantic_kernel.functions.kernel_function_from_prompt import KernelFunctionFromPrompt
|
||||
|
||||
return KernelFunctionFromPrompt(
|
||||
function_name=function_name,
|
||||
plugin_name=plugin_name,
|
||||
description=description,
|
||||
prompt=prompt,
|
||||
template_format=template_format,
|
||||
prompt_template=prompt_template,
|
||||
prompt_template_config=prompt_template_config,
|
||||
prompt_execution_settings=prompt_execution_settings,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_method(
|
||||
cls,
|
||||
method: Callable[..., Any],
|
||||
plugin_name: str | None = None,
|
||||
stream_method: Callable[..., Any] | None = None,
|
||||
) -> "KernelFunctionFromMethod":
|
||||
"""Create a new instance of the KernelFunctionFromMethod class."""
|
||||
from semantic_kernel.functions.kernel_function_from_method import KernelFunctionFromMethod
|
||||
|
||||
return KernelFunctionFromMethod(
|
||||
plugin_name=plugin_name,
|
||||
method=method,
|
||||
stream_method=stream_method,
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""The name of the function."""
|
||||
return self.metadata.name
|
||||
|
||||
@property
|
||||
def plugin_name(self) -> str:
|
||||
"""The name of the plugin that contains this function."""
|
||||
return self.metadata.plugin_name or ""
|
||||
|
||||
@property
|
||||
def fully_qualified_name(self) -> str:
|
||||
"""The fully qualified name of the function."""
|
||||
return self.metadata.fully_qualified_name
|
||||
|
||||
@property
|
||||
def description(self) -> str | None:
|
||||
"""The description of the function."""
|
||||
return self.metadata.description
|
||||
|
||||
@property
|
||||
def is_prompt(self) -> bool:
|
||||
"""Whether the function is based on a prompt."""
|
||||
return self.metadata.is_prompt
|
||||
|
||||
@property
|
||||
def parameters(self) -> list["KernelParameterMetadata"]:
|
||||
"""The parameters for the function."""
|
||||
return self.metadata.parameters
|
||||
|
||||
@property
|
||||
def return_parameter(self) -> "KernelParameterMetadata | None":
|
||||
"""The return parameter for the function."""
|
||||
return self.metadata.return_parameter
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
kernel: "Kernel",
|
||||
arguments: "KernelArguments | None" = None,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> FunctionResult | None:
|
||||
"""Invoke the function with the given arguments.
|
||||
|
||||
Args:
|
||||
kernel (Kernel): The kernel
|
||||
arguments (KernelArguments | None): The Kernel arguments.
|
||||
Optional, defaults to None.
|
||||
metadata (Dict[str, Any]): Additional metadata.
|
||||
kwargs (Dict[str, Any]): Additional keyword arguments that will be
|
||||
|
||||
Returns:
|
||||
FunctionResult: The result of the function
|
||||
"""
|
||||
return await self.invoke(kernel, arguments, metadata, **kwargs)
|
||||
|
||||
@abstractmethod
|
||||
async def _invoke_internal(self, context: FunctionInvocationContext) -> None:
|
||||
"""Internal invoke method of the the function with the given arguments.
|
||||
|
||||
This function should be implemented by the subclass.
|
||||
It relies on updating the context with the result from the function.
|
||||
|
||||
Args:
|
||||
context (FunctionInvocationContext): The invocation context.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
async def invoke(
|
||||
self,
|
||||
kernel: "Kernel",
|
||||
arguments: "KernelArguments | None" = None,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> "FunctionResult | None":
|
||||
"""Invoke the function with the given arguments.
|
||||
|
||||
Args:
|
||||
kernel (Kernel): The kernel
|
||||
arguments (KernelArguments): The Kernel arguments
|
||||
metadata (Dict[str, Any]): Additional metadata.
|
||||
kwargs (Any): Additional keyword arguments that will be
|
||||
added to the KernelArguments.
|
||||
|
||||
Returns:
|
||||
FunctionResult: The result of the function
|
||||
"""
|
||||
if arguments is None:
|
||||
arguments = KernelArguments(**kwargs)
|
||||
_rebuild_function_invocation_context()
|
||||
function_context = FunctionInvocationContext(function=self, kernel=kernel, arguments=arguments)
|
||||
|
||||
with function_tracer.start_as_current_span(tracer, self, metadata) as current_span:
|
||||
KernelFunctionLogMessages.log_function_invoking(logger, self.fully_qualified_name)
|
||||
KernelFunctionLogMessages.log_function_arguments(logger, arguments)
|
||||
|
||||
if function_tracer.are_sensitive_events_enabled():
|
||||
current_span.set_attribute(TOOL_CALL_ARGUMENTS, arguments.dumps())
|
||||
|
||||
attributes = {MEASUREMENT_FUNCTION_TAG_NAME: self.fully_qualified_name}
|
||||
starting_time_stamp = time.perf_counter()
|
||||
try:
|
||||
stack = kernel.construct_call_stack(
|
||||
filter_type=FilterTypes.FUNCTION_INVOCATION,
|
||||
inner_function=self._invoke_internal,
|
||||
)
|
||||
await stack(function_context)
|
||||
|
||||
KernelFunctionLogMessages.log_function_invoked_success(logger, self.fully_qualified_name)
|
||||
KernelFunctionLogMessages.log_function_result_value(logger, function_context.result)
|
||||
|
||||
if function_tracer.are_sensitive_events_enabled():
|
||||
try:
|
||||
result = str(function_context.result.value) if function_context.result else None
|
||||
except Exception as e:
|
||||
result = str(e)
|
||||
current_span.set_attribute(TOOL_CALL_RESULT, result)
|
||||
|
||||
return function_context.result
|
||||
except Exception as e:
|
||||
self._handle_exception(current_span, e, attributes)
|
||||
raise e
|
||||
finally:
|
||||
duration = time.perf_counter() - starting_time_stamp
|
||||
self.invocation_duration_histogram.record(duration, attributes)
|
||||
KernelFunctionLogMessages.log_function_completed(logger, duration)
|
||||
|
||||
@abstractmethod
|
||||
async def _invoke_internal_stream(self, context: FunctionInvocationContext) -> None:
|
||||
"""Internal invoke method of the the function with the given arguments.
|
||||
|
||||
The abstract method is defined without async because otherwise the typing fails.
|
||||
A implementation of this function should be async.
|
||||
"""
|
||||
...
|
||||
|
||||
async def invoke_stream(
|
||||
self,
|
||||
kernel: "Kernel",
|
||||
arguments: "KernelArguments | None" = None,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> "AsyncGenerator[FunctionResult | list[StreamingContentMixin | Any], Any]":
|
||||
"""Invoke a stream async function with the given arguments.
|
||||
|
||||
Args:
|
||||
kernel (Kernel): The kernel
|
||||
arguments (KernelArguments): The Kernel arguments
|
||||
metadata (Dict[str, Any]): Additional metadata.
|
||||
kwargs (Any): Additional keyword arguments that will be
|
||||
added to the KernelArguments.
|
||||
|
||||
Yields:
|
||||
KernelContent with the StreamingKernelMixin or FunctionResult:
|
||||
The results of the function,
|
||||
if there is an error a FunctionResult is yielded.
|
||||
"""
|
||||
if arguments is None:
|
||||
arguments = KernelArguments(**kwargs)
|
||||
_rebuild_function_invocation_context()
|
||||
function_context = FunctionInvocationContext(
|
||||
function=self, kernel=kernel, arguments=arguments, is_streaming=True
|
||||
)
|
||||
|
||||
with function_tracer.start_as_current_span(tracer, self, metadata) as current_span:
|
||||
KernelFunctionLogMessages.log_function_streaming_invoking(logger, self.fully_qualified_name)
|
||||
KernelFunctionLogMessages.log_function_arguments(logger, arguments)
|
||||
|
||||
if function_tracer.are_sensitive_events_enabled():
|
||||
current_span.set_attribute(TOOL_CALL_ARGUMENTS, arguments.dumps())
|
||||
|
||||
attributes = {MEASUREMENT_FUNCTION_TAG_NAME: self.fully_qualified_name}
|
||||
starting_time_stamp = time.perf_counter()
|
||||
try:
|
||||
stack = kernel.construct_call_stack(
|
||||
filter_type=FilterTypes.FUNCTION_INVOCATION,
|
||||
inner_function=self._invoke_internal_stream,
|
||||
)
|
||||
await stack(function_context)
|
||||
|
||||
function_results: list[Any] = []
|
||||
if function_context.result is not None:
|
||||
if isasyncgen(function_context.result.value):
|
||||
async for partial in function_context.result.value:
|
||||
function_results.append(partial)
|
||||
yield partial
|
||||
elif isgenerator(function_context.result.value):
|
||||
for partial in function_context.result.value:
|
||||
function_results.append(partial)
|
||||
yield partial
|
||||
else:
|
||||
function_results.append(function_context.result.value)
|
||||
yield function_context.result
|
||||
|
||||
if function_tracer.are_sensitive_events_enabled():
|
||||
results: list[str] = []
|
||||
try:
|
||||
results.append(str(function_results))
|
||||
except Exception as e:
|
||||
results.append(str(e))
|
||||
current_span.set_attribute(TOOL_CALL_RESULT, json.dumps(results))
|
||||
except Exception as e:
|
||||
self._handle_exception(current_span, e, attributes)
|
||||
raise e
|
||||
finally:
|
||||
duration = time.perf_counter() - starting_time_stamp
|
||||
self.streaming_duration_histogram.record(duration, attributes)
|
||||
KernelFunctionLogMessages.log_function_streaming_completed(logger, duration)
|
||||
|
||||
def function_copy(self, plugin_name: str | None = None) -> "KernelFunction":
|
||||
"""Copy the function, can also override the plugin_name.
|
||||
|
||||
Args:
|
||||
plugin_name (str): The new plugin name.
|
||||
|
||||
Returns:
|
||||
KernelFunction: The copied function.
|
||||
"""
|
||||
cop: KernelFunction = copy(self)
|
||||
cop.metadata = deepcopy(self.metadata)
|
||||
if plugin_name:
|
||||
cop.metadata.plugin_name = plugin_name
|
||||
return cop
|
||||
|
||||
def _handle_exception(self, current_span: trace.Span, exception: Exception, attributes: dict[str, str]) -> None:
|
||||
"""Handle the exception.
|
||||
|
||||
Args:
|
||||
current_span (trace.Span): The current span.
|
||||
exception (Exception): The exception.
|
||||
attributes (Attributes): The attributes to be modified.
|
||||
"""
|
||||
attributes[ERROR_TYPE] = type(exception).__name__
|
||||
|
||||
current_span.record_exception(exception)
|
||||
current_span.set_attribute(ERROR_TYPE, type(exception).__name__)
|
||||
current_span.set_status(trace.StatusCode.ERROR, description=str(exception))
|
||||
|
||||
KernelFunctionLogMessages.log_function_error(logger, exception)
|
||||
|
||||
def as_agent_framework_tool(
|
||||
self,
|
||||
*,
|
||||
name: str | None = None,
|
||||
description: str | None = None,
|
||||
kernel: "Kernel | None" = None,
|
||||
) -> Any:
|
||||
"""Convert the function to an agent framework tool.
|
||||
|
||||
Args:
|
||||
name: The name of the tool, if None, the function name is used.
|
||||
description: The description of the tool, if None, the tool description is used.
|
||||
kernel: The kernel to use, if None, a kernel is created.
|
||||
|
||||
Returns:
|
||||
AIFunction: The agent framework tool.
|
||||
"""
|
||||
import json
|
||||
|
||||
from pydantic import Field, create_model
|
||||
|
||||
from semantic_kernel.kernel import Kernel
|
||||
|
||||
try:
|
||||
from agent_framework import AIFunction
|
||||
|
||||
except ImportError as e:
|
||||
raise ImportError(
|
||||
"agent_framework is not installed. Please install it with 'pip install agent-framework-core'"
|
||||
) from e
|
||||
|
||||
if not kernel:
|
||||
kernel = Kernel()
|
||||
name = name or self.name
|
||||
description = description or self.description
|
||||
fields = {}
|
||||
for param in self.parameters:
|
||||
if param.include_in_function_choices:
|
||||
if param.default_value is not None:
|
||||
fields[param.name] = (
|
||||
param.type_,
|
||||
Field(description=param.description, default=param.default_value),
|
||||
)
|
||||
fields[param.name] = (param.type_, Field(description=param.description))
|
||||
input_model = create_model("InputModel", **fields) # type: ignore
|
||||
|
||||
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||
result = await self.invoke(kernel, *args, **kwargs)
|
||||
if result and result.value is not None:
|
||||
if isinstance(result.value, list):
|
||||
results: list[Any] = []
|
||||
for value in result.value:
|
||||
if isinstance(value, ChatMessageContent):
|
||||
results.append(str(value))
|
||||
continue
|
||||
if isinstance(value, TextContent):
|
||||
results.append(value.text)
|
||||
continue
|
||||
if isinstance(value, BaseModel):
|
||||
results.append(value.model_dump())
|
||||
continue
|
||||
results.append(value)
|
||||
return json.dumps(results) if len(results) > 1 else json.dumps(results[0])
|
||||
return json.dumps(result.value)
|
||||
return "The function did not return a result."
|
||||
|
||||
return AIFunction(
|
||||
name=name,
|
||||
description=description,
|
||||
input_model=input_model,
|
||||
func=wrapper,
|
||||
)
|
||||
@@ -0,0 +1,193 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import logging
|
||||
import types
|
||||
from collections.abc import Callable
|
||||
from inspect import Parameter, Signature, isasyncgenfunction, isclass, isgeneratorfunction, signature
|
||||
from typing import Annotated, Any, ForwardRef, Union, get_args, get_origin
|
||||
|
||||
NoneType = type(None)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def kernel_function(
|
||||
func: Callable[..., object] | None = None,
|
||||
name: str | None = None,
|
||||
description: str | None = None,
|
||||
) -> Callable[..., Any]:
|
||||
"""Decorator for kernel functions.
|
||||
|
||||
Can be used directly as @kernel_function
|
||||
or with parameters @kernel_function(name='function', description='I am a function.').
|
||||
|
||||
This decorator is used to mark a function as a kernel function. It also provides metadata for the function.
|
||||
The name and description can be left empty, and then the function name and docstring will be used.
|
||||
|
||||
The parameters are parsed from the function signature, use typing.Annotated to provide a description for the
|
||||
parameter.
|
||||
|
||||
To parse the type, first it checks if the parameter is annotated.
|
||||
|
||||
If there are annotations, the first annotation that is a string is used as the description.
|
||||
Any other annotations are checked if they are a dict, if so, they will be added to the parameter info.
|
||||
If the keys align with the KernelParameterMetadata, they will be added to the parameter info.
|
||||
This is useful for things like parameters like `kernel`, `service` and `arguments`, for instance
|
||||
if you set `{"include_in_function_choices": False}` in the annotation, that parameter will not be included in
|
||||
the representation of the function towards LLM's or MCP Servers. If you do set this and the parameter is required
|
||||
but you do not set it in a invoke level arguments, the function will raise an error.
|
||||
|
||||
After the annotations, it checks recursively until it reaches the lowest level, and it combines
|
||||
the types into a single comma-separated string, a forwardRef is also supported.
|
||||
|
||||
All of this is are stored in __kernel_function_parameters__.
|
||||
|
||||
The return type and description are parsed from the function signature,
|
||||
and that is stored in __kernel_function_return_type__, __kernel_function_return_description__
|
||||
and __kernel_function_return_required__.
|
||||
|
||||
It also checks if the function is a streaming type (generator or iterable, async or not),
|
||||
and that is stored as a bool in __kernel_function_streaming__.
|
||||
|
||||
Args:
|
||||
func (Callable[..., object] | None): The function to decorate, can be None (if used as @kernel_function
|
||||
name (str | None): The name of the function, if not supplied, the function name will be used.
|
||||
description (str | None): The description of the function,
|
||||
if not supplied, the function docstring will be used, can be None.
|
||||
|
||||
"""
|
||||
|
||||
def decorator(func: Callable[..., object]) -> Callable[..., object]:
|
||||
"""The actual decorator function."""
|
||||
setattr(func, "__kernel_function__", True)
|
||||
setattr(func, "__kernel_function_description__", description or func.__doc__)
|
||||
setattr(func, "__kernel_function_name__", name or getattr(func, "__name__", "unknown"))
|
||||
setattr(func, "__kernel_function_streaming__", isasyncgenfunction(func) or isgeneratorfunction(func))
|
||||
logger.debug(f"Parsing decorator for function: {getattr(func, '__kernel_function_name__')}")
|
||||
func_sig = signature(func, eval_str=True)
|
||||
|
||||
annotations = _process_signature(func_sig)
|
||||
logger.debug(f"{annotations=}")
|
||||
|
||||
setattr(func, "__kernel_function_parameters__", annotations)
|
||||
|
||||
return_annotation = (
|
||||
_parse_parameter("return", func_sig.return_annotation, None) if func_sig.return_annotation else {}
|
||||
)
|
||||
setattr(func, "__kernel_function_return_type__", return_annotation.get("type_", "None"))
|
||||
setattr(func, "__kernel_function_return_type_object__", return_annotation.get("type_object", None))
|
||||
setattr(func, "__kernel_function_return_description__", return_annotation.get("description", ""))
|
||||
setattr(func, "__kernel_function_return_required__", return_annotation.get("is_required", False))
|
||||
return func
|
||||
|
||||
if func:
|
||||
return decorator(func)
|
||||
return decorator
|
||||
|
||||
|
||||
def _get_non_none_type(args: tuple) -> Any:
|
||||
"""Return the first non-None type from args, or None if no such type exists or multiple non-None types are present.""" # noqa: E501
|
||||
non_none_types = [arg for arg in args if arg is not type(None)]
|
||||
# If we have more than one non-none type, we can't determine the single underlying type
|
||||
# so we rely on the type_ attribute, which means it's a Union and will be properly handled
|
||||
# later during schema generation
|
||||
if len(non_none_types) == 1:
|
||||
return non_none_types[0]
|
||||
return None
|
||||
|
||||
|
||||
def _get_underlying_type(annotation: Any) -> Any:
|
||||
"""Get the underlying type of the annotation."""
|
||||
if isinstance(annotation, types.UnionType):
|
||||
return _get_non_none_type(annotation.__args__)
|
||||
|
||||
if hasattr(annotation, "__origin__"):
|
||||
if annotation.__origin__ is Union:
|
||||
return _get_non_none_type(get_args(annotation))
|
||||
|
||||
if isinstance(annotation.__origin__, types.UnionType):
|
||||
return _get_non_none_type(annotation.__origin__.__args__)
|
||||
|
||||
return annotation.__origin__
|
||||
|
||||
return annotation
|
||||
|
||||
|
||||
def _process_signature(func_sig: Signature) -> list[dict[str, Any]]:
|
||||
"""Process the signature of the function."""
|
||||
annotations = []
|
||||
|
||||
for arg in func_sig.parameters.values():
|
||||
if arg.name == "self":
|
||||
continue
|
||||
annotation = arg.annotation
|
||||
default = arg.default if arg.default != arg.empty else None
|
||||
parsed_annotation = _parse_parameter(arg.name, annotation, default)
|
||||
if get_origin(annotation) is Annotated or get_origin(annotation) in {Union, types.UnionType}:
|
||||
underlying_type = _get_underlying_type(annotation)
|
||||
else:
|
||||
underlying_type = annotation
|
||||
parsed_annotation["type_object"] = underlying_type
|
||||
annotations.append(parsed_annotation)
|
||||
|
||||
return annotations
|
||||
|
||||
|
||||
def _parse_parameter(name: str, param: Any, default: Any) -> dict[str, Any]:
|
||||
"""Parse the parameter annotation."""
|
||||
logger.debug(f"Parsing param: {name}")
|
||||
logger.debug(f"Parsing annotation: {param}")
|
||||
ret: dict[str, Any] = {"name": name}
|
||||
if default is not None:
|
||||
ret["default_value"] = default
|
||||
ret["is_required"] = False
|
||||
else:
|
||||
ret["is_required"] = True
|
||||
if not param or param == Parameter.empty:
|
||||
ret["type_"] = "Any"
|
||||
return ret
|
||||
if not isinstance(param, str):
|
||||
if hasattr(param, "__metadata__"):
|
||||
for meta in param.__metadata__:
|
||||
if isinstance(meta, str):
|
||||
ret["description"] = meta
|
||||
elif isinstance(meta, dict):
|
||||
# only override from the metadata if it is not already set
|
||||
if "description" not in ret and (description := meta.pop("description", None)):
|
||||
ret["description"] = description
|
||||
ret.update(meta)
|
||||
else:
|
||||
logger.debug(f"Unknown metadata type: {meta}")
|
||||
if hasattr(param, "__origin__"):
|
||||
ret.update(_parse_parameter(name, param.__origin__, default))
|
||||
if hasattr(param, "__args__"):
|
||||
args = []
|
||||
for arg in param.__args__:
|
||||
if arg == NoneType:
|
||||
ret["is_required"] = False
|
||||
if "default_value" not in ret:
|
||||
ret["default_value"] = None
|
||||
continue
|
||||
if isinstance(arg, ForwardRef):
|
||||
arg = arg.__forward_arg__
|
||||
args.append(_parse_parameter(name, arg, default))
|
||||
if ret.get("type_") in ["list", "dict"]:
|
||||
ret["type_"] = f"{ret['type_']}[{', '.join([arg['type_'] for arg in args])}]"
|
||||
elif len(args) > 1:
|
||||
ret["type_"] = ", ".join([arg["type_"] for arg in args])
|
||||
else:
|
||||
ret["type_"] = args[0]["type_"]
|
||||
ret["type_object"] = args[0].get("type_object", None)
|
||||
elif isclass(param):
|
||||
ret["type_"] = param.__name__
|
||||
ret["type_object"] = param
|
||||
else:
|
||||
ret["type_"] = str(param).replace(" |", ",")
|
||||
else:
|
||||
if "|" in param:
|
||||
param = param.replace(" |", ",")
|
||||
ret["type_"] = param
|
||||
ret["is_required"] = True
|
||||
# if the include_in_function_choices is set to false, we set the is_required to false
|
||||
if not ret.get("include_in_function_choices", True):
|
||||
ret["is_required"] = False
|
||||
return ret
|
||||
@@ -0,0 +1,413 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import logging
|
||||
from abc import ABC
|
||||
from collections.abc import Mapping, Sequence
|
||||
from functools import singledispatchmethod
|
||||
from typing import TYPE_CHECKING, Any, Literal, Protocol, runtime_checkable
|
||||
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
|
||||
from semantic_kernel.exceptions import KernelFunctionNotFoundError, KernelPluginNotFoundError
|
||||
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
|
||||
from semantic_kernel.functions.kernel_plugin import KernelPlugin
|
||||
from semantic_kernel.kernel_pydantic import KernelBaseModel
|
||||
from semantic_kernel.prompt_template.const import KERNEL_TEMPLATE_FORMAT_NAME, TEMPLATE_FORMAT_TYPES
|
||||
from semantic_kernel.prompt_template.prompt_template_base import PromptTemplateBase
|
||||
from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from semantic_kernel.connectors.openapi_plugin.openapi_function_execution_parameters import (
|
||||
OpenAPIFunctionExecutionParameters,
|
||||
)
|
||||
from semantic_kernel.functions.kernel_function import KernelFunction
|
||||
from semantic_kernel.functions.types import KERNEL_FUNCTION_TYPE
|
||||
from semantic_kernel.kernel import Kernel
|
||||
|
||||
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class AddToKernelCallbackProtocol(Protocol):
|
||||
"""Protocol for the callback to be called when the plugin is added to the kernel."""
|
||||
|
||||
def added_to_kernel(self, kernel: "Kernel") -> None:
|
||||
"""Called when the plugin is added to the kernel.
|
||||
|
||||
Args:
|
||||
kernel (Kernel): The kernel instance
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class KernelFunctionExtension(KernelBaseModel, ABC):
|
||||
"""Kernel function extension."""
|
||||
|
||||
plugins: dict[str, KernelPlugin] = Field(default_factory=dict)
|
||||
|
||||
@field_validator("plugins", mode="before")
|
||||
@classmethod
|
||||
def rewrite_plugins(
|
||||
cls, plugins: KernelPlugin | list[KernelPlugin] | dict[str, KernelPlugin] | None = None
|
||||
) -> dict[str, KernelPlugin]:
|
||||
"""Rewrite plugins to a dictionary."""
|
||||
if not plugins:
|
||||
return {}
|
||||
if isinstance(plugins, KernelPlugin):
|
||||
return {plugins.name: plugins}
|
||||
if isinstance(plugins, list):
|
||||
return {p.name: p for p in plugins}
|
||||
return plugins
|
||||
|
||||
def add_plugin(
|
||||
self,
|
||||
plugin: KernelPlugin | object | dict[str, Any] | None = None,
|
||||
plugin_name: str | None = None,
|
||||
parent_directory: str | None = None,
|
||||
description: str | None = None,
|
||||
class_init_arguments: dict[str, dict[str, Any]] | None = None,
|
||||
encoding: str = "utf-8",
|
||||
) -> "KernelPlugin":
|
||||
"""Adds a plugin to the kernel's collection of plugins.
|
||||
|
||||
If a plugin is provided, it uses that instance instead of creating a new KernelPlugin.
|
||||
See KernelPlugin.from_directory for more details on how the directory is parsed.
|
||||
|
||||
Args:
|
||||
plugin: The plugin to add.
|
||||
This can be a KernelPlugin, in which case it is added straightaway and other parameters are ignored,
|
||||
a custom class that contains methods with the kernel_function decorator
|
||||
or a dictionary of functions with the kernel_function decorator for one or
|
||||
several methods.
|
||||
if the custom class has a `added_to_kernel` method, it will be called with the kernel instance.
|
||||
plugin_name: The name of the plugin, used if the plugin is not a KernelPlugin,
|
||||
if the plugin is None and the parent_directory is set,
|
||||
KernelPlugin.from_directory is called with those parameters,
|
||||
see `KernelPlugin.from_directory` for details.
|
||||
parent_directory: The parent directory path where the plugin directory resides
|
||||
description: The description of the plugin, used if the plugin is not a KernelPlugin.
|
||||
class_init_arguments: The class initialization arguments
|
||||
encoding: The encoding to use when reading text files. Defaults to "utf-8".
|
||||
|
||||
Returns:
|
||||
KernelPlugin: The plugin that was added.
|
||||
|
||||
Raises:
|
||||
ValidationError: If a KernelPlugin needs to be created, but it is not valid.
|
||||
|
||||
"""
|
||||
if isinstance(plugin, KernelPlugin):
|
||||
self.plugins[plugin.name] = plugin
|
||||
return self.plugins[plugin.name]
|
||||
if not plugin_name:
|
||||
plugin_name = getattr(plugin, "name", plugin.__class__.__name__)
|
||||
if not isinstance(plugin_name, str):
|
||||
raise TypeError("plugin_name must be a string.")
|
||||
if plugin:
|
||||
self.plugins[plugin_name] = KernelPlugin.from_object(
|
||||
plugin_name=plugin_name, plugin_instance=plugin, description=description
|
||||
)
|
||||
if isinstance(plugin, AddToKernelCallbackProtocol):
|
||||
plugin.added_to_kernel(self) # type: ignore
|
||||
return self.plugins[plugin_name]
|
||||
if plugin is None and parent_directory is not None:
|
||||
self.plugins[plugin_name] = KernelPlugin.from_directory(
|
||||
plugin_name=plugin_name,
|
||||
parent_directory=parent_directory,
|
||||
description=description,
|
||||
class_init_arguments=class_init_arguments,
|
||||
encoding=encoding,
|
||||
)
|
||||
return self.plugins[plugin_name]
|
||||
raise ValueError("plugin or parent_directory must be provided.")
|
||||
|
||||
def add_plugins(self, plugins: list[KernelPlugin | object] | dict[str, KernelPlugin | object]) -> None:
|
||||
"""Adds a list of plugins to the kernel's collection of plugins.
|
||||
|
||||
Args:
|
||||
plugins (list[KernelPlugin] | dict[str, KernelPlugin]): The plugins to add to the kernel
|
||||
"""
|
||||
if isinstance(plugins, list):
|
||||
for plug in plugins:
|
||||
self.add_plugin(plug)
|
||||
return
|
||||
for name, plugin in plugins.items():
|
||||
self.add_plugin(plugin, plugin_name=name)
|
||||
|
||||
def add_function(
|
||||
self,
|
||||
plugin_name: str,
|
||||
function: "KERNEL_FUNCTION_TYPE | None" = None,
|
||||
function_name: str | None = None,
|
||||
description: str | None = None,
|
||||
prompt: str | None = None,
|
||||
prompt_template_config: PromptTemplateConfig | None = None,
|
||||
prompt_execution_settings: (
|
||||
PromptExecutionSettings | Sequence[PromptExecutionSettings] | Mapping[str, PromptExecutionSettings] | None
|
||||
) = None,
|
||||
template_format: TEMPLATE_FORMAT_TYPES = KERNEL_TEMPLATE_FORMAT_NAME,
|
||||
prompt_template: PromptTemplateBase | None = None,
|
||||
return_plugin: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> "KernelFunction | KernelPlugin":
|
||||
"""Adds a function to the specified plugin.
|
||||
|
||||
Args:
|
||||
plugin_name (str): The name of the plugin to add the function to
|
||||
function (KernelFunction | Callable[..., Any]): The function to add
|
||||
function_name (str): The name of the function
|
||||
plugin_name (str): The name of the plugin
|
||||
description (str | None): The description of the function
|
||||
prompt (str | None): The prompt template.
|
||||
prompt_template_config (PromptTemplateConfig | None): The prompt template configuration
|
||||
prompt_execution_settings: The execution settings, will be parsed into a dict.
|
||||
template_format (str | None): The format of the prompt template
|
||||
prompt_template (PromptTemplateBase | None): The prompt template
|
||||
return_plugin (bool): If True, the plugin is returned instead of the function
|
||||
kwargs (Any): Additional arguments
|
||||
|
||||
Returns:
|
||||
KernelFunction | KernelPlugin: The function that was added, or the plugin if return_plugin is True
|
||||
|
||||
"""
|
||||
from semantic_kernel.functions.kernel_function import KernelFunction
|
||||
|
||||
if function is None:
|
||||
if not function_name or (not prompt and not prompt_template_config and not prompt_template):
|
||||
raise ValueError(
|
||||
"function_name and prompt, prompt_template_config or prompt_template must be provided if a function is not supplied." # noqa: E501
|
||||
)
|
||||
if prompt_execution_settings is None and (
|
||||
prompt_template_config is None or prompt_template_config.execution_settings is None
|
||||
):
|
||||
prompt_execution_settings = PromptExecutionSettings(extension_data=kwargs)
|
||||
|
||||
function = KernelFunction.from_prompt(
|
||||
function_name=function_name,
|
||||
plugin_name=plugin_name,
|
||||
description=description or (prompt_template_config.description if prompt_template_config else None),
|
||||
prompt=prompt,
|
||||
template_format=template_format,
|
||||
prompt_template=prompt_template,
|
||||
prompt_template_config=prompt_template_config,
|
||||
prompt_execution_settings=prompt_execution_settings,
|
||||
)
|
||||
elif not isinstance(function, KernelFunction):
|
||||
function = KernelFunction.from_method(plugin_name=plugin_name, method=function)
|
||||
if plugin_name not in self.plugins:
|
||||
plugin = KernelPlugin(name=plugin_name, functions=function)
|
||||
self.add_plugin(plugin)
|
||||
return plugin if return_plugin else plugin[function.name]
|
||||
self.plugins[plugin_name][function.name] = function
|
||||
return self.plugins[plugin_name] if return_plugin else self.plugins[plugin_name][function.name]
|
||||
|
||||
def add_functions(
|
||||
self,
|
||||
plugin_name: str,
|
||||
functions: "list[KERNEL_FUNCTION_TYPE] | dict[str, KERNEL_FUNCTION_TYPE]",
|
||||
) -> "KernelPlugin":
|
||||
"""Adds a list of functions to the specified plugin.
|
||||
|
||||
Args:
|
||||
plugin_name (str): The name of the plugin to add the functions to
|
||||
functions (list[KernelFunction] | dict[str, KernelFunction]): The functions to add
|
||||
|
||||
Returns:
|
||||
KernelPlugin: The plugin that the functions were added to.
|
||||
|
||||
"""
|
||||
if plugin_name in self.plugins:
|
||||
self.plugins[plugin_name].update(functions)
|
||||
return self.plugins[plugin_name]
|
||||
return self.add_plugin(KernelPlugin(name=plugin_name, functions=functions)) # type: ignore
|
||||
|
||||
def add_plugin_from_openapi(
|
||||
self,
|
||||
plugin_name: str,
|
||||
openapi_document_path: str | None = None,
|
||||
openapi_parsed_spec: dict[str, Any] | None = None,
|
||||
execution_settings: "OpenAPIFunctionExecutionParameters | None" = None,
|
||||
description: str | None = None,
|
||||
) -> KernelPlugin:
|
||||
"""Add a plugin from the OpenAPI manifest.
|
||||
|
||||
Args:
|
||||
plugin_name: The name of the plugin
|
||||
openapi_document_path: The path to the OpenAPI document
|
||||
openapi_parsed_spec: The parsed OpenAPI spec
|
||||
execution_settings: The execution parameters
|
||||
description: The description of the plugin
|
||||
|
||||
Returns:
|
||||
KernelPlugin: The imported plugin
|
||||
|
||||
Raises:
|
||||
PluginInitializationError: if the plugin URL or plugin JSON/YAML is not provided
|
||||
"""
|
||||
return self.add_plugin(
|
||||
KernelPlugin.from_openapi(
|
||||
plugin_name=plugin_name,
|
||||
openapi_document_path=openapi_document_path,
|
||||
openapi_parsed_spec=openapi_parsed_spec,
|
||||
execution_settings=execution_settings,
|
||||
description=description,
|
||||
)
|
||||
)
|
||||
|
||||
def get_plugin(self, plugin_name: str) -> "KernelPlugin":
|
||||
"""Get a plugin by name.
|
||||
|
||||
Args:
|
||||
plugin_name (str): The name of the plugin
|
||||
|
||||
Returns:
|
||||
KernelPlugin: The plugin
|
||||
|
||||
Raises:
|
||||
KernelPluginNotFoundError: If the plugin is not found
|
||||
|
||||
"""
|
||||
if plugin_name not in self.plugins:
|
||||
raise KernelPluginNotFoundError(f"Plugin '{plugin_name}' not found")
|
||||
return self.plugins[plugin_name]
|
||||
|
||||
def get_function(self, plugin_name: str | None, function_name: str) -> "KernelFunction":
|
||||
"""Get a function by plugin_name and function_name.
|
||||
|
||||
Args:
|
||||
plugin_name (str | None): The name of the plugin
|
||||
function_name (str): The name of the function
|
||||
|
||||
Returns:
|
||||
KernelFunction: The function
|
||||
|
||||
Raises:
|
||||
KernelPluginNotFoundError: If the plugin is not found
|
||||
KernelFunctionNotFoundError: If the function is not found
|
||||
|
||||
"""
|
||||
if plugin_name is None:
|
||||
matches = [
|
||||
(name, plugin[function_name]) for name, plugin in self.plugins.items() if function_name in plugin
|
||||
]
|
||||
if not matches:
|
||||
raise KernelFunctionNotFoundError(f"Function '{function_name}' not found in any plugin.")
|
||||
if len(matches) > 1:
|
||||
logger.warning(
|
||||
"Function '%s' is ambiguous: it exists in multiple plugins (%s). Resolving to '%s-%s' "
|
||||
"(first registered). Specify a plugin_name for security-relevant lookups to avoid shadowing.",
|
||||
function_name,
|
||||
", ".join(name for name, _ in matches),
|
||||
matches[0][0],
|
||||
function_name,
|
||||
)
|
||||
return matches[0][1]
|
||||
if plugin_name not in self.plugins:
|
||||
raise KernelPluginNotFoundError(f"Plugin '{plugin_name}' not found")
|
||||
if function_name not in self.plugins[plugin_name]:
|
||||
raise KernelFunctionNotFoundError(f"Function '{function_name}' not found in plugin '{plugin_name}'")
|
||||
return self.plugins[plugin_name][function_name]
|
||||
|
||||
def get_function_from_fully_qualified_function_name(self, fully_qualified_function_name: str) -> "KernelFunction":
|
||||
"""Get a function by its fully qualified name (<plugin_name>-<function_name>).
|
||||
|
||||
Args:
|
||||
fully_qualified_function_name (str): The fully qualified name of the function,
|
||||
if there is no '-' in the name, it is assumed that it is only a function_name.
|
||||
|
||||
Returns:
|
||||
KernelFunction: The function
|
||||
|
||||
Raises:
|
||||
KernelPluginNotFoundError: If the plugin is not found
|
||||
KernelFunctionNotFoundError: If the function is not found
|
||||
|
||||
"""
|
||||
names = fully_qualified_function_name.split("-", maxsplit=1)
|
||||
if len(names) == 1:
|
||||
plugin_name = None
|
||||
function_name = names[0]
|
||||
else:
|
||||
plugin_name = names[0]
|
||||
function_name = names[1]
|
||||
return self.get_function(plugin_name, function_name)
|
||||
|
||||
def get_full_list_of_function_metadata(self) -> list["KernelFunctionMetadata"]:
|
||||
"""Get a list of all function metadata in the plugins."""
|
||||
if not self.plugins:
|
||||
return []
|
||||
return [func.metadata for plugin in self.plugins.values() for func in plugin]
|
||||
|
||||
@singledispatchmethod
|
||||
def get_list_of_function_metadata(self, *args: Any, **kwargs: Any) -> list["KernelFunctionMetadata"]:
|
||||
"""Get a list of all function metadata in the plugin collection."""
|
||||
raise NotImplementedError("This method is not implemented for the provided arguments.")
|
||||
|
||||
@get_list_of_function_metadata.register(bool)
|
||||
def get_list_of_function_metadata_bool(
|
||||
self, include_prompt: bool = True, include_native: bool = True
|
||||
) -> list["KernelFunctionMetadata"]:
|
||||
"""Get a list of the function metadata in the plugin collection.
|
||||
|
||||
Args:
|
||||
include_prompt (bool): Whether to include semantic functions in the list.
|
||||
include_native (bool): Whether to include native functions in the list.
|
||||
|
||||
Returns:
|
||||
A list of KernelFunctionMetadata objects in the collection.
|
||||
"""
|
||||
if not self.plugins:
|
||||
return []
|
||||
return [
|
||||
func.metadata
|
||||
for plugin in self.plugins.values()
|
||||
for func in plugin.functions.values()
|
||||
if (include_prompt and func.is_prompt) or (include_native and not func.is_prompt)
|
||||
]
|
||||
|
||||
@get_list_of_function_metadata.register(dict)
|
||||
def get_list_of_function_metadata_filters(
|
||||
self,
|
||||
filters: dict[
|
||||
Literal["excluded_plugins", "included_plugins", "excluded_functions", "included_functions"], list[str]
|
||||
],
|
||||
) -> list["KernelFunctionMetadata"]:
|
||||
"""Get a list of Kernel Function Metadata based on filters.
|
||||
|
||||
Args:
|
||||
filters (dict[str, list[str]]): The filters to apply to the function list.
|
||||
The keys are:
|
||||
- included_plugins: A list of plugin names to include.
|
||||
- excluded_plugins: A list of plugin names to exclude.
|
||||
- included_functions: A list of function names to include.
|
||||
- excluded_functions: A list of function names to exclude.
|
||||
The included and excluded parameters are mutually exclusive.
|
||||
The function names are checked against the fully qualified name of a function.
|
||||
|
||||
Returns:
|
||||
list[KernelFunctionMetadata]: The list of Kernel Function Metadata that match the filters.
|
||||
"""
|
||||
if not self.plugins:
|
||||
return []
|
||||
included_plugins = filters.get("included_plugins")
|
||||
excluded_plugins = filters.get("excluded_plugins", [])
|
||||
included_functions = filters.get("included_functions")
|
||||
excluded_functions = filters.get("excluded_functions", [])
|
||||
if included_plugins and excluded_plugins:
|
||||
raise ValueError("Cannot use both included_plugins and excluded_plugins at the same time.")
|
||||
if included_functions and excluded_functions:
|
||||
raise ValueError("Cannot use both included_functions and excluded_functions at the same time.")
|
||||
|
||||
result: list["KernelFunctionMetadata"] = []
|
||||
for plugin_name, plugin in self.plugins.items():
|
||||
if plugin_name in excluded_plugins or (included_plugins and plugin_name not in included_plugins):
|
||||
continue
|
||||
for function in plugin:
|
||||
if function.fully_qualified_name in excluded_functions or (
|
||||
included_functions and function.fully_qualified_name not in included_functions
|
||||
):
|
||||
continue
|
||||
result.append(function.metadata)
|
||||
return result
|
||||
@@ -0,0 +1,192 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import inspect
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from inspect import isasyncgen, isasyncgenfunction, isawaitable, iscoroutinefunction, isgenerator, isgeneratorfunction
|
||||
from typing import Any
|
||||
|
||||
from pydantic import Field, ValidationError
|
||||
|
||||
from semantic_kernel.exceptions import FunctionExecutionException, FunctionInitializationError
|
||||
from semantic_kernel.filters.functions.function_invocation_context import FunctionInvocationContext
|
||||
from semantic_kernel.functions.function_result import FunctionResult
|
||||
from semantic_kernel.functions.kernel_function import KernelFunction
|
||||
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
|
||||
from semantic_kernel.functions.kernel_parameter_metadata import KernelParameterMetadata
|
||||
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class KernelFunctionFromMethod(KernelFunction):
|
||||
"""Semantic Kernel Function from a method."""
|
||||
|
||||
method: Callable[..., Any] = Field(exclude=True)
|
||||
stream_method: Callable[..., Any] | None = Field(default=None, exclude=True)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
method: Callable[..., Any],
|
||||
plugin_name: str | None = None,
|
||||
stream_method: Callable[..., Any] | None = None,
|
||||
parameters: list[KernelParameterMetadata] | None = None,
|
||||
return_parameter: KernelParameterMetadata | None = None,
|
||||
additional_metadata: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Initializes a new instance of the KernelFunctionFromMethod class.
|
||||
|
||||
Args:
|
||||
method (Callable[..., Any]): The method to be called
|
||||
plugin_name (str | None): The name of the plugin
|
||||
stream_method (Callable[..., Any] | None): The stream method for the function
|
||||
parameters (list[KernelParameterMetadata] | None): The parameters of the function
|
||||
return_parameter (KernelParameterMetadata | None): The return parameter of the function
|
||||
additional_metadata (dict[str, Any] | None): Additional metadata for the function
|
||||
"""
|
||||
if method is None:
|
||||
raise FunctionInitializationError("Method cannot be `None`")
|
||||
|
||||
if not hasattr(method, "__kernel_function__") or method.__kernel_function__ is None:
|
||||
raise FunctionInitializationError("Method is not a Kernel function")
|
||||
|
||||
# all these fields are created when the kernel function decorator is used,
|
||||
# so no need to check before using, will raise an exception if not set
|
||||
function_name = method.__kernel_function_name__ # type: ignore
|
||||
description = method.__kernel_function_description__ # type: ignore
|
||||
if parameters is None:
|
||||
parameters = [KernelParameterMetadata(**param) for param in method.__kernel_function_parameters__] # type: ignore
|
||||
if return_parameter is None:
|
||||
return_parameter = KernelParameterMetadata(
|
||||
name="return",
|
||||
description=method.__kernel_function_return_description__, # type: ignore
|
||||
default_value=None,
|
||||
type_=method.__kernel_function_return_type__, # type: ignore
|
||||
type_object=method.__kernel_function_return_type_object__, # type: ignore
|
||||
is_required=method.__kernel_function_return_required__, # type: ignore
|
||||
)
|
||||
|
||||
try:
|
||||
metadata = KernelFunctionMetadata(
|
||||
name=function_name,
|
||||
description=description,
|
||||
parameters=parameters,
|
||||
return_parameter=return_parameter,
|
||||
is_prompt=False,
|
||||
is_asynchronous=isasyncgenfunction(method) or iscoroutinefunction(method),
|
||||
plugin_name=plugin_name,
|
||||
additional_properties=additional_metadata if additional_metadata is not None else {},
|
||||
)
|
||||
except ValidationError as exc:
|
||||
# reraise the exception to clarify it comes from KernelFunction init
|
||||
raise FunctionInitializationError("Failed to create KernelFunctionMetadata") from exc
|
||||
|
||||
args: dict[str, Any] = {
|
||||
"metadata": metadata,
|
||||
"method": method,
|
||||
"stream_method": (
|
||||
stream_method
|
||||
if stream_method is not None
|
||||
else method
|
||||
if isasyncgenfunction(method) or isgeneratorfunction(method)
|
||||
else None
|
||||
),
|
||||
}
|
||||
|
||||
super().__init__(**args)
|
||||
|
||||
async def _invoke_internal(
|
||||
self,
|
||||
context: FunctionInvocationContext,
|
||||
) -> None:
|
||||
"""Invoke the function with the given arguments."""
|
||||
function_arguments = self.gather_function_parameters(context)
|
||||
result = self.method(**function_arguments)
|
||||
if isasyncgen(result):
|
||||
result = [x async for x in result]
|
||||
elif isawaitable(result):
|
||||
result = await result
|
||||
elif isgenerator(result):
|
||||
result = list(result)
|
||||
if not isinstance(result, FunctionResult):
|
||||
result = FunctionResult(
|
||||
function=self.metadata,
|
||||
value=result,
|
||||
metadata={"arguments": context.arguments, "used_arguments": function_arguments},
|
||||
)
|
||||
context.result = result
|
||||
|
||||
async def _invoke_internal_stream(self, context: FunctionInvocationContext) -> None:
|
||||
if self.stream_method is None:
|
||||
raise NotImplementedError("Stream method not implemented")
|
||||
function_arguments = self.gather_function_parameters(context)
|
||||
context.result = FunctionResult(function=self.metadata, value=self.stream_method(**function_arguments))
|
||||
|
||||
def _parse_parameter(self, value: Any, param_type: Any) -> Any:
|
||||
"""Parses the value into the specified param_type, including handling lists of types."""
|
||||
# Handle Any or object type explicitly
|
||||
if param_type in {Any, object, inspect._empty}:
|
||||
return value
|
||||
|
||||
if isinstance(param_type, type) and hasattr(param_type, "model_validate"):
|
||||
try:
|
||||
return param_type.model_validate(value)
|
||||
except Exception as exc:
|
||||
raise FunctionExecutionException(
|
||||
f"Parameter is expected to be parsed to {param_type} but is not."
|
||||
) from exc
|
||||
elif hasattr(param_type, "__origin__") and param_type.__origin__ is list:
|
||||
if isinstance(value, list):
|
||||
item_type = param_type.__args__[0]
|
||||
return [self._parse_parameter(item, item_type) for item in value]
|
||||
raise FunctionExecutionException(f"Expected a list for {param_type}, but got {type(value)}")
|
||||
else:
|
||||
try:
|
||||
if isinstance(value, dict) and hasattr(param_type, "__init__"):
|
||||
return param_type(**value)
|
||||
return param_type(value)
|
||||
except Exception as exc:
|
||||
raise FunctionExecutionException(
|
||||
f"Parameter is expected to be parsed to {param_type} but is not."
|
||||
) from exc
|
||||
|
||||
def gather_function_parameters(self, context: FunctionInvocationContext) -> dict[str, Any]:
|
||||
"""Gathers the function parameters from the arguments."""
|
||||
function_arguments: dict[str, Any] = {}
|
||||
for param in self.parameters:
|
||||
if param.name is None:
|
||||
raise FunctionExecutionException("Parameter name cannot be None")
|
||||
if param.name == "kernel":
|
||||
function_arguments[param.name] = context.kernel
|
||||
continue
|
||||
if param.name == "service":
|
||||
function_arguments[param.name] = context.kernel.select_ai_service(self, context.arguments)[0]
|
||||
continue
|
||||
if param.name == "execution_settings":
|
||||
function_arguments[param.name] = context.kernel.select_ai_service(self, context.arguments)[1]
|
||||
continue
|
||||
if param.name == "arguments":
|
||||
function_arguments[param.name] = context.arguments
|
||||
continue
|
||||
if param.name in context.arguments:
|
||||
value: Any = context.arguments[param.name]
|
||||
if (
|
||||
param.type_
|
||||
and "," not in param.type_
|
||||
and param.type_object
|
||||
and param.type_object is not inspect._empty
|
||||
and param.type_object is not Any
|
||||
):
|
||||
try:
|
||||
value = self._parse_parameter(value, param.type_object)
|
||||
except Exception as exc:
|
||||
raise FunctionExecutionException(
|
||||
f"Parameter {param.name} is expected to be parsed to {param.type_object} but is not."
|
||||
) from exc
|
||||
function_arguments[param.name] = value
|
||||
continue
|
||||
if param.is_required:
|
||||
raise FunctionExecutionException(
|
||||
f"Parameter {param.name} is required but not provided in the arguments."
|
||||
)
|
||||
logger.debug(f"Parameter {param.name} is not provided, using default value {param.default_value}")
|
||||
return function_arguments
|
||||
@@ -0,0 +1,416 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import logging
|
||||
import os
|
||||
from collections.abc import AsyncGenerator, Mapping, Sequence
|
||||
from html import unescape
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import yaml
|
||||
from pydantic import Field, ValidationError, model_validator
|
||||
|
||||
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
|
||||
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
|
||||
from semantic_kernel.connectors.ai.text_completion_client_base import TextCompletionClientBase
|
||||
from semantic_kernel.connectors.ai.text_to_audio_client_base import TextToAudioClientBase
|
||||
from semantic_kernel.connectors.ai.text_to_image_client_base import TextToImageClientBase
|
||||
from semantic_kernel.const import DEFAULT_SERVICE_NAME
|
||||
from semantic_kernel.contents.audio_content import AudioContent
|
||||
from semantic_kernel.contents.chat_history import ChatHistory
|
||||
from semantic_kernel.contents.chat_message_content import ChatMessageContent
|
||||
from semantic_kernel.contents.image_content import ImageContent
|
||||
from semantic_kernel.contents.text_content import TextContent
|
||||
from semantic_kernel.exceptions import FunctionExecutionException, FunctionInitializationError
|
||||
from semantic_kernel.exceptions.function_exceptions import PromptRenderingException
|
||||
from semantic_kernel.filters.filter_types import FilterTypes
|
||||
from semantic_kernel.filters.functions.function_invocation_context import FunctionInvocationContext
|
||||
from semantic_kernel.filters.kernel_filters_extension import _rebuild_prompt_render_context
|
||||
from semantic_kernel.filters.prompts.prompt_render_context import PromptRenderContext
|
||||
from semantic_kernel.functions.function_result import FunctionResult
|
||||
from semantic_kernel.functions.kernel_arguments import KernelArguments
|
||||
from semantic_kernel.functions.kernel_function import TEMPLATE_FORMAT_MAP, KernelFunction
|
||||
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
|
||||
from semantic_kernel.functions.kernel_parameter_metadata import KernelParameterMetadata
|
||||
from semantic_kernel.functions.prompt_rendering_result import PromptRenderingResult
|
||||
from semantic_kernel.prompt_template.const import KERNEL_TEMPLATE_FORMAT_NAME, TEMPLATE_FORMAT_TYPES
|
||||
from semantic_kernel.prompt_template.prompt_template_base import PromptTemplateBase
|
||||
from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from semantic_kernel.services.ai_service_client_base import AIServiceClientBase
|
||||
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
PROMPT_FILE_NAME = "skprompt.txt"
|
||||
CONFIG_FILE_NAME = "config.json"
|
||||
PROMPT_RETURN_PARAM = KernelParameterMetadata(
|
||||
name="return",
|
||||
description="The completion result",
|
||||
default_value=None,
|
||||
type="FunctionResult", # type: ignore
|
||||
is_required=True,
|
||||
)
|
||||
|
||||
|
||||
class KernelFunctionFromPrompt(KernelFunction):
|
||||
"""Semantic Kernel Function from a prompt."""
|
||||
|
||||
prompt_template: PromptTemplateBase
|
||||
prompt_execution_settings: dict[str, PromptExecutionSettings] = Field(default_factory=dict)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
function_name: str,
|
||||
plugin_name: str | None = None,
|
||||
description: str | None = None,
|
||||
prompt: str | None = None,
|
||||
template_format: TEMPLATE_FORMAT_TYPES = KERNEL_TEMPLATE_FORMAT_NAME,
|
||||
prompt_template: PromptTemplateBase | None = None,
|
||||
prompt_template_config: PromptTemplateConfig | None = None,
|
||||
prompt_execution_settings: PromptExecutionSettings
|
||||
| Sequence[PromptExecutionSettings]
|
||||
| Mapping[str, PromptExecutionSettings]
|
||||
| None = None,
|
||||
) -> None:
|
||||
"""Initializes a new instance of the KernelFunctionFromPrompt class.
|
||||
|
||||
Args:
|
||||
function_name (str): The name of the function
|
||||
plugin_name (str): The name of the plugin
|
||||
description (str): The description for the function
|
||||
|
||||
prompt (Optional[str]): The prompt
|
||||
template_format (Optional[str]): The template format, default is "semantic-kernel"
|
||||
prompt_template (Optional[KernelPromptTemplate]): The prompt template
|
||||
prompt_template_config (Optional[PromptTemplateConfig]): The prompt template configuration
|
||||
prompt_execution_settings (Optional): instance, list or dict of PromptExecutionSettings to be used
|
||||
by the function, can also be supplied through prompt_template_config,
|
||||
but the supplied one is used if both are present.
|
||||
prompt_template_config (Optional[PromptTemplateConfig]): the prompt template config.
|
||||
"""
|
||||
if not prompt and not prompt_template_config and not prompt_template:
|
||||
raise FunctionInitializationError(
|
||||
"The prompt cannot be empty, must be supplied directly, \
|
||||
through prompt_template_config or in the prompt_template."
|
||||
)
|
||||
|
||||
if prompt and prompt_template_config and prompt_template_config.template != prompt:
|
||||
logger.warning(
|
||||
f"Prompt ({prompt}) and PromptTemplateConfig ({prompt_template_config.template}) both supplied, "
|
||||
"using the template in PromptTemplateConfig, ignoring prompt."
|
||||
)
|
||||
if template_format and prompt_template_config and prompt_template_config.template_format != template_format:
|
||||
logger.warning(
|
||||
f"Template ({template_format}) and PromptTemplateConfig ({prompt_template_config.template_format}) "
|
||||
"both supplied, using the template format in PromptTemplateConfig, ignoring template."
|
||||
)
|
||||
if not prompt_template:
|
||||
if not prompt_template_config:
|
||||
# prompt must be there if prompt_template and prompt_template_config is not supplied
|
||||
prompt_template_config = PromptTemplateConfig(
|
||||
name=function_name,
|
||||
description=description,
|
||||
template=prompt,
|
||||
template_format=template_format,
|
||||
)
|
||||
elif not prompt_template_config.template:
|
||||
prompt_template_config.template = prompt
|
||||
prompt_template = TEMPLATE_FORMAT_MAP[prompt_template_config.template_format](
|
||||
prompt_template_config=prompt_template_config
|
||||
) # type: ignore
|
||||
|
||||
try:
|
||||
metadata = KernelFunctionMetadata(
|
||||
name=function_name,
|
||||
plugin_name=plugin_name,
|
||||
description=description,
|
||||
parameters=prompt_template.prompt_template_config.get_kernel_parameter_metadata(), # type: ignore
|
||||
is_prompt=True,
|
||||
is_asynchronous=True,
|
||||
return_parameter=PROMPT_RETURN_PARAM,
|
||||
)
|
||||
except ValidationError as exc:
|
||||
raise FunctionInitializationError("Failed to create KernelFunctionMetadata") from exc
|
||||
super().__init__(
|
||||
metadata=metadata,
|
||||
prompt_template=prompt_template, # type: ignore
|
||||
prompt_execution_settings=prompt_execution_settings or {}, # type: ignore
|
||||
)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def rewrite_execution_settings(
|
||||
cls,
|
||||
data: Any,
|
||||
) -> dict[str, PromptExecutionSettings]:
|
||||
"""Rewrite execution settings to a dictionary.
|
||||
|
||||
If the prompt_execution_settings is not a dictionary, it is converted to a dictionary.
|
||||
If it is not supplied, but prompt_template is, the prompt_template's execution settings are used.
|
||||
"""
|
||||
if isinstance(data, dict):
|
||||
prompt_execution_settings = data.get("prompt_execution_settings")
|
||||
prompt_template = data.get("prompt_template")
|
||||
if not prompt_execution_settings:
|
||||
if prompt_template:
|
||||
prompt_execution_settings = prompt_template.prompt_template_config.execution_settings
|
||||
data["prompt_execution_settings"] = prompt_execution_settings
|
||||
if not prompt_execution_settings:
|
||||
return data
|
||||
if isinstance(prompt_execution_settings, PromptExecutionSettings):
|
||||
data["prompt_execution_settings"] = {
|
||||
prompt_execution_settings.service_id or DEFAULT_SERVICE_NAME: prompt_execution_settings
|
||||
}
|
||||
if isinstance(prompt_execution_settings, Sequence):
|
||||
data["prompt_execution_settings"] = {
|
||||
s.service_id or DEFAULT_SERVICE_NAME: s for s in prompt_execution_settings
|
||||
}
|
||||
return data
|
||||
|
||||
async def _invoke_internal(self, context: FunctionInvocationContext) -> None:
|
||||
"""Invokes the function with the given arguments."""
|
||||
prompt_render_result = await self._render_prompt(context)
|
||||
if prompt_render_result.function_result is not None:
|
||||
context.result = prompt_render_result.function_result
|
||||
return
|
||||
|
||||
if isinstance(prompt_render_result.ai_service, ChatCompletionClientBase):
|
||||
chat_history = ChatHistory.from_rendered_prompt(prompt_render_result.rendered_prompt)
|
||||
try:
|
||||
chat_message_contents = await prompt_render_result.ai_service.get_chat_message_contents(
|
||||
chat_history=chat_history,
|
||||
settings=prompt_render_result.execution_settings,
|
||||
**{"kernel": context.kernel, "arguments": context.arguments},
|
||||
)
|
||||
except Exception as exc:
|
||||
raise FunctionExecutionException(f"Error occurred while invoking function {self.name}: {exc}") from exc
|
||||
|
||||
if not chat_message_contents:
|
||||
raise FunctionExecutionException(f"No completions returned while invoking function {self.name}")
|
||||
|
||||
context.result = self._create_function_result(
|
||||
completions=chat_message_contents,
|
||||
chat_history=chat_history,
|
||||
arguments=context.arguments,
|
||||
prompt=prompt_render_result.rendered_prompt,
|
||||
)
|
||||
return
|
||||
|
||||
if isinstance(prompt_render_result.ai_service, TextCompletionClientBase):
|
||||
try:
|
||||
texts = await prompt_render_result.ai_service.get_text_contents(
|
||||
prompt=unescape(prompt_render_result.rendered_prompt),
|
||||
settings=prompt_render_result.execution_settings,
|
||||
)
|
||||
except Exception as exc:
|
||||
raise FunctionExecutionException(f"Error occurred while invoking function {self.name}: {exc}") from exc
|
||||
|
||||
context.result = self._create_function_result(
|
||||
completions=texts, arguments=context.arguments, prompt=prompt_render_result.rendered_prompt
|
||||
)
|
||||
return
|
||||
|
||||
if isinstance(prompt_render_result.ai_service, TextToImageClientBase):
|
||||
try:
|
||||
images = await prompt_render_result.ai_service.get_image_content(
|
||||
description=unescape(prompt_render_result.rendered_prompt),
|
||||
settings=prompt_render_result.execution_settings,
|
||||
)
|
||||
except Exception as exc:
|
||||
raise FunctionExecutionException(f"Error occurred while invoking function {self.name}: {exc}") from exc
|
||||
|
||||
context.result = self._create_function_result(
|
||||
completions=[images], arguments=context.arguments, prompt=prompt_render_result.rendered_prompt
|
||||
)
|
||||
return
|
||||
|
||||
if isinstance(prompt_render_result.ai_service, TextToAudioClientBase):
|
||||
try:
|
||||
audio = await prompt_render_result.ai_service.get_audio_content(
|
||||
text=unescape(prompt_render_result.rendered_prompt),
|
||||
settings=prompt_render_result.execution_settings,
|
||||
)
|
||||
except Exception as exc:
|
||||
raise FunctionExecutionException(f"Error occurred while invoking function {self.name}: {exc}") from exc
|
||||
|
||||
context.result = self._create_function_result(
|
||||
completions=[audio], arguments=context.arguments, prompt=prompt_render_result.rendered_prompt
|
||||
)
|
||||
return
|
||||
|
||||
raise ValueError(f"Service `{type(prompt_render_result.ai_service).__name__}` is not a valid AI service")
|
||||
|
||||
async def _invoke_internal_stream(self, context: FunctionInvocationContext) -> None:
|
||||
"""Invokes the function stream with the given arguments."""
|
||||
prompt_render_result = await self._render_prompt(context, is_streaming=True)
|
||||
if prompt_render_result.function_result is not None:
|
||||
context.result = prompt_render_result.function_result
|
||||
return
|
||||
|
||||
if isinstance(prompt_render_result.ai_service, ChatCompletionClientBase):
|
||||
chat_history = ChatHistory.from_rendered_prompt(prompt_render_result.rendered_prompt)
|
||||
value: AsyncGenerator = prompt_render_result.ai_service.get_streaming_chat_message_contents(
|
||||
chat_history=chat_history,
|
||||
settings=prompt_render_result.execution_settings,
|
||||
**{"kernel": context.kernel, "arguments": context.arguments},
|
||||
)
|
||||
elif isinstance(prompt_render_result.ai_service, TextCompletionClientBase):
|
||||
value = prompt_render_result.ai_service.get_streaming_text_contents(
|
||||
prompt=prompt_render_result.rendered_prompt, settings=prompt_render_result.execution_settings
|
||||
)
|
||||
else:
|
||||
raise FunctionExecutionException(
|
||||
f"Service `{type(prompt_render_result.ai_service)}` is not a valid AI service"
|
||||
)
|
||||
|
||||
context.result = FunctionResult(
|
||||
function=self.metadata, value=value, rendered_prompt=prompt_render_result.rendered_prompt
|
||||
)
|
||||
|
||||
async def _render_prompt(
|
||||
self, context: FunctionInvocationContext, is_streaming: bool = False
|
||||
) -> PromptRenderingResult:
|
||||
"""Render the prompt and apply the prompt rendering filters."""
|
||||
self.update_arguments_with_defaults(context.arguments)
|
||||
|
||||
_rebuild_prompt_render_context()
|
||||
prompt_render_context = PromptRenderContext(
|
||||
function=self, kernel=context.kernel, arguments=context.arguments, is_streaming=is_streaming
|
||||
)
|
||||
|
||||
stack = context.kernel.construct_call_stack(
|
||||
filter_type=FilterTypes.PROMPT_RENDERING,
|
||||
inner_function=self._inner_render_prompt,
|
||||
)
|
||||
await stack(prompt_render_context)
|
||||
|
||||
if prompt_render_context.rendered_prompt is None:
|
||||
raise PromptRenderingException("Prompt rendering failed, no rendered prompt was returned.")
|
||||
selected_service: tuple["AIServiceClientBase", PromptExecutionSettings] = context.kernel.select_ai_service(
|
||||
function=self,
|
||||
arguments=context.arguments,
|
||||
type=(TextCompletionClientBase, ChatCompletionClientBase) if prompt_render_context.is_streaming else None,
|
||||
)
|
||||
return PromptRenderingResult(
|
||||
rendered_prompt=prompt_render_context.rendered_prompt,
|
||||
ai_service=selected_service[0],
|
||||
execution_settings=selected_service[1],
|
||||
function_result=prompt_render_context.function_result,
|
||||
)
|
||||
|
||||
async def _inner_render_prompt(self, context: PromptRenderContext) -> None:
|
||||
"""Render the prompt using the prompt template."""
|
||||
context.rendered_prompt = await self.prompt_template.render(context.kernel, context.arguments)
|
||||
|
||||
def _create_function_result(
|
||||
self,
|
||||
completions: list[ChatMessageContent] | list[TextContent] | list[ImageContent] | list[AudioContent],
|
||||
arguments: KernelArguments,
|
||||
chat_history: ChatHistory | None = None,
|
||||
prompt: str | None = None,
|
||||
) -> FunctionResult:
|
||||
"""Creates a function result with the given completions."""
|
||||
metadata: dict[str, Any] = {
|
||||
"arguments": arguments,
|
||||
"metadata": [completion.metadata for completion in completions],
|
||||
}
|
||||
if chat_history:
|
||||
metadata["messages"] = chat_history
|
||||
if prompt:
|
||||
metadata["prompt"] = prompt
|
||||
return FunctionResult(
|
||||
function=self.metadata,
|
||||
value=completions,
|
||||
metadata=metadata,
|
||||
rendered_prompt=prompt,
|
||||
)
|
||||
|
||||
def update_arguments_with_defaults(self, arguments: KernelArguments) -> None:
|
||||
"""Update any missing values with their defaults."""
|
||||
for parameter in self.prompt_template.prompt_template_config.input_variables:
|
||||
if parameter.name not in arguments and parameter.default not in {None, "", False, 0}:
|
||||
arguments[parameter.name] = parameter.default
|
||||
|
||||
@classmethod
|
||||
def from_yaml(cls, yaml_str: str, plugin_name: str | None = None) -> "KernelFunctionFromPrompt":
|
||||
"""Creates a new instance of the KernelFunctionFromPrompt class from a YAML string."""
|
||||
try:
|
||||
data = yaml.safe_load(yaml_str)
|
||||
except yaml.YAMLError as exc: # pragma: no cover
|
||||
raise FunctionInitializationError(f"Invalid YAML content: {yaml_str}, error: {exc}") from exc
|
||||
|
||||
if not isinstance(data, dict):
|
||||
raise FunctionInitializationError(f"The YAML content must represent a dictionary, got {yaml_str}")
|
||||
|
||||
try:
|
||||
prompt_template_config = PromptTemplateConfig(**data)
|
||||
except ValidationError as exc:
|
||||
raise FunctionInitializationError(
|
||||
f"Error initializing PromptTemplateConfig: {exc} from yaml data: {data}"
|
||||
) from exc
|
||||
return cls(
|
||||
function_name=prompt_template_config.name,
|
||||
plugin_name=plugin_name,
|
||||
description=prompt_template_config.description,
|
||||
prompt_template_config=prompt_template_config,
|
||||
template_format=prompt_template_config.template_format,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_directory(
|
||||
cls, path: str, plugin_name: str | None = None, encoding: str = "utf-8"
|
||||
) -> "KernelFunctionFromPrompt":
|
||||
"""Creates a new instance of the KernelFunctionFromPrompt class from a directory.
|
||||
|
||||
The directory needs to contain:
|
||||
- A prompt file named `skprompt.txt`
|
||||
- A config file named `config.json`
|
||||
|
||||
Args:
|
||||
path: The path to the directory containing the prompt and config files.
|
||||
plugin_name: The name of the plugin.
|
||||
encoding: The encoding to use when reading the files. Defaults to "utf-8".
|
||||
|
||||
Returns:
|
||||
KernelFunctionFromPrompt: The kernel function from prompt
|
||||
"""
|
||||
prompt_path = os.path.join(path, PROMPT_FILE_NAME)
|
||||
config_path = os.path.join(path, CONFIG_FILE_NAME)
|
||||
prompt_exists = os.path.exists(prompt_path)
|
||||
config_exists = os.path.exists(config_path)
|
||||
if not config_exists and not prompt_exists:
|
||||
raise FunctionInitializationError(
|
||||
f"{PROMPT_FILE_NAME} and {CONFIG_FILE_NAME} files are required to create a "
|
||||
f"function from a directory, path: {path!s}."
|
||||
)
|
||||
if not config_exists:
|
||||
raise FunctionInitializationError(
|
||||
f"{CONFIG_FILE_NAME} files are required to create a function from a directory, "
|
||||
f"path: {path!s}, prompt file is there."
|
||||
)
|
||||
if not prompt_exists:
|
||||
raise FunctionInitializationError(
|
||||
f"{PROMPT_FILE_NAME} files are required to create a function from a directory, "
|
||||
f"path: {path!s}, config file is there."
|
||||
)
|
||||
|
||||
function_name = os.path.basename(path)
|
||||
|
||||
with open(config_path, encoding=encoding) as config_file:
|
||||
prompt_template_config = PromptTemplateConfig.from_json(config_file.read())
|
||||
prompt_template_config.name = function_name
|
||||
|
||||
with open(prompt_path, encoding=encoding) as prompt_file:
|
||||
prompt_template_config.template = prompt_file.read()
|
||||
|
||||
prompt_template = TEMPLATE_FORMAT_MAP[prompt_template_config.template_format]( # type: ignore
|
||||
prompt_template_config=prompt_template_config
|
||||
)
|
||||
return cls(
|
||||
function_name=function_name,
|
||||
plugin_name=plugin_name,
|
||||
prompt_template=prompt_template,
|
||||
prompt_template_config=prompt_template_config,
|
||||
template_format=prompt_template_config.template_format,
|
||||
description=prompt_template_config.description,
|
||||
)
|
||||
@@ -0,0 +1,63 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import logging
|
||||
from logging import Logger
|
||||
|
||||
from semantic_kernel.functions.function_result import FunctionResult
|
||||
from semantic_kernel.functions.kernel_arguments import KernelArguments
|
||||
|
||||
|
||||
class KernelFunctionLogMessages:
|
||||
"""Kernel function log messages.
|
||||
|
||||
This class contains static methods to log messages related to kernel functions.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def log_function_invoking(logger: Logger, kernel_function_name: str):
|
||||
"""Log message when a kernel function is invoked."""
|
||||
logger.info("Function %s invoking.", kernel_function_name)
|
||||
|
||||
@staticmethod
|
||||
def log_function_arguments(logger: Logger, arguments: KernelArguments):
|
||||
"""Log message when a kernel function is invoked."""
|
||||
logger.debug("Function arguments: %s", arguments)
|
||||
|
||||
@staticmethod
|
||||
def log_function_invoked_success(logger: Logger, kernel_function_name: str):
|
||||
"""Log message when a kernel function is invoked successfully."""
|
||||
logger.info("Function %s succeeded.", kernel_function_name)
|
||||
|
||||
@staticmethod
|
||||
def log_function_result_value(logger: Logger, function_result: FunctionResult | None):
|
||||
"""Log message when a kernel function result is returned."""
|
||||
if not logger.isEnabledFor(logging.DEBUG):
|
||||
return
|
||||
|
||||
if function_result is not None:
|
||||
try:
|
||||
logger.debug("Function result: %s", function_result)
|
||||
except Exception:
|
||||
logger.error("Function result: Failed to convert result value to string")
|
||||
else:
|
||||
logger.debug("Function result: None")
|
||||
|
||||
@staticmethod
|
||||
def log_function_error(logger: Logger, error: Exception):
|
||||
"""Log message when a kernel function fails."""
|
||||
logger.error("Function failed. Error: %s", error)
|
||||
|
||||
@staticmethod
|
||||
def log_function_completed(logger: Logger, duration: float):
|
||||
"""Log message when a kernel function is completed."""
|
||||
logger.info("Function completed. Duration: %fs", duration)
|
||||
|
||||
@staticmethod
|
||||
def log_function_streaming_invoking(logger: Logger, kernel_function_name: str):
|
||||
"""Log message when a kernel function is invoked via streaming."""
|
||||
logger.info("Function %s streaming.", kernel_function_name)
|
||||
|
||||
@staticmethod
|
||||
def log_function_streaming_completed(logger: Logger, duration: float):
|
||||
"""Log message when a kernel function is completed via streaming."""
|
||||
logger.info("Function streaming completed. Duration: %fs", duration)
|
||||
@@ -0,0 +1,69 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from semantic_kernel.const import DEFAULT_FULLY_QUALIFIED_NAME_SEPARATOR
|
||||
from semantic_kernel.functions.kernel_parameter_metadata import KernelParameterMetadata
|
||||
from semantic_kernel.kernel_pydantic import KernelBaseModel
|
||||
from semantic_kernel.utils.validation import FUNCTION_NAME_REGEX, PLUGIN_NAME_REGEX
|
||||
|
||||
|
||||
class KernelFunctionMetadata(KernelBaseModel):
|
||||
"""The kernel function metadata."""
|
||||
|
||||
name: str = Field(..., pattern=FUNCTION_NAME_REGEX)
|
||||
plugin_name: str | None = Field(default=None, pattern=PLUGIN_NAME_REGEX)
|
||||
description: str | None = Field(default=None)
|
||||
parameters: list[KernelParameterMetadata] = Field(default_factory=list)
|
||||
is_prompt: bool
|
||||
is_asynchronous: bool | None = Field(default=True)
|
||||
return_parameter: KernelParameterMetadata | None = None
|
||||
additional_properties: dict[str, Any] | None = Field(default=None)
|
||||
|
||||
@property
|
||||
def fully_qualified_name(self) -> str:
|
||||
"""Get the fully qualified name of the function.
|
||||
|
||||
A fully qualified name is the name of the combination of the plugin name and
|
||||
the function name, separated by a hyphen, if the plugin name is present.
|
||||
Otherwise, it is just the function name.
|
||||
|
||||
Returns:
|
||||
The fully qualified name of the function.
|
||||
"""
|
||||
return self.custom_fully_qualified_name(DEFAULT_FULLY_QUALIFIED_NAME_SEPARATOR)
|
||||
|
||||
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.name}" if self.plugin_name else self.name
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
"""Compare to another KernelFunctionMetadata instance.
|
||||
|
||||
Args:
|
||||
other (KernelFunctionMetadata): The other KernelFunctionMetadata instance.
|
||||
|
||||
Returns:
|
||||
True if the two instances are equal, False otherwise.
|
||||
"""
|
||||
if not isinstance(other, KernelFunctionMetadata):
|
||||
return False
|
||||
|
||||
return (
|
||||
self.name == other.name
|
||||
and self.plugin_name == other.plugin_name
|
||||
and self.description == other.description
|
||||
and self.parameters == other.parameters
|
||||
and self.is_prompt == other.is_prompt
|
||||
and self.is_asynchronous == other.is_asynchronous
|
||||
and self.return_parameter == other.return_parameter
|
||||
)
|
||||
@@ -0,0 +1,62 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import Field, model_validator
|
||||
|
||||
from semantic_kernel.kernel_pydantic import KernelBaseModel
|
||||
from semantic_kernel.schema.kernel_json_schema_builder import KernelJsonSchemaBuilder
|
||||
from semantic_kernel.utils.validation import FUNCTION_PARAM_NAME_REGEX
|
||||
|
||||
|
||||
class KernelParameterMetadata(KernelBaseModel):
|
||||
"""The kernel parameter metadata."""
|
||||
|
||||
name: str | None = Field(..., pattern=FUNCTION_PARAM_NAME_REGEX)
|
||||
description: str | None = None
|
||||
default_value: Any | None = None
|
||||
type_: str | None = Field(default="str", alias="type")
|
||||
is_required: bool | None = False
|
||||
type_object: Any | None = Field(default=None, exclude=True)
|
||||
schema_data: dict[str, Any] | None = None
|
||||
include_in_function_choices: bool = True
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def form_schema(cls, data: Any) -> Any:
|
||||
"""Create a schema for the parameter metadata."""
|
||||
if isinstance(data, dict) and data.get("schema_data") is None:
|
||||
type_object = data.get("type_object", None)
|
||||
type_ = data.get("type_", None)
|
||||
default_value = data.get("default_value", None)
|
||||
description = data.get("description", None)
|
||||
inferred_schema = cls.infer_schema(type_object, type_, default_value, description)
|
||||
data["schema_data"] = inferred_schema
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def infer_schema(
|
||||
cls,
|
||||
type_object: type | None = None,
|
||||
parameter_type: str | None = None,
|
||||
default_value: Any | None = None,
|
||||
description: str | None = None,
|
||||
structured_output: bool = False,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Infer the schema for the parameter metadata."""
|
||||
schema = None
|
||||
|
||||
if type_object is not None:
|
||||
schema = KernelJsonSchemaBuilder.build(type_object, description, structured_output)
|
||||
elif parameter_type is not None:
|
||||
string_default = str(default_value) if default_value is not None else None
|
||||
if string_default and string_default.strip():
|
||||
needs_space = bool(description and description.strip())
|
||||
description = (
|
||||
f"{description}{' ' if needs_space else ''}(default value: {string_default})"
|
||||
if description
|
||||
else f"(default value: {string_default})"
|
||||
)
|
||||
|
||||
schema = KernelJsonSchemaBuilder.build_from_type_name(parameter_type, description)
|
||||
return schema
|
||||
@@ -0,0 +1,470 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import importlib
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
from collections.abc import Generator, ItemsView
|
||||
from functools import singledispatchmethod
|
||||
from glob import glob
|
||||
from types import MethodType
|
||||
from typing import TYPE_CHECKING, Annotated, Any, TypeVar
|
||||
|
||||
from pydantic import Field, StringConstraints
|
||||
|
||||
from semantic_kernel.exceptions import PluginInitializationError
|
||||
from semantic_kernel.exceptions.function_exceptions import FunctionInitializationError
|
||||
from semantic_kernel.functions.kernel_function import KernelFunction
|
||||
from semantic_kernel.functions.kernel_function_from_method import KernelFunctionFromMethod
|
||||
from semantic_kernel.functions.kernel_function_from_prompt import KernelFunctionFromPrompt
|
||||
from semantic_kernel.functions.types import KERNEL_FUNCTION_TYPE
|
||||
from semantic_kernel.kernel_pydantic import KernelBaseModel
|
||||
from semantic_kernel.kernel_types import OptionalOneOrMany
|
||||
from semantic_kernel.utils.validation import PLUGIN_NAME_REGEX
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from semantic_kernel.connectors.openapi_plugin.openapi_function_execution_parameters import (
|
||||
OpenAPIFunctionExecutionParameters,
|
||||
)
|
||||
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_T = TypeVar("_T", bound="KernelPlugin")
|
||||
|
||||
|
||||
class KernelPlugin(KernelBaseModel):
|
||||
"""Represents a Kernel Plugin with functions.
|
||||
|
||||
This class behaves mostly like a dictionary, with functions as values and their names as keys.
|
||||
When you add a function, through `.set` or `__setitem__`, the function is copied, the metadata is deep-copied
|
||||
and the name of the plugin is set in the metadata and added to the dict of functions.
|
||||
This is done in the same way as a normal dict, so a existing key will be overwritten.
|
||||
|
||||
Attributes:
|
||||
name (str): The name of the plugin. The name can be upper/lower
|
||||
case letters and underscores.
|
||||
description (str): The description of the plugin.
|
||||
functions (Dict[str, KernelFunction]): The functions in the plugin,
|
||||
indexed by their name.
|
||||
|
||||
Methods:
|
||||
set: Set a function in the plugin.
|
||||
__setitem__: Set a function in the plugin.
|
||||
get: Get a function from the plugin.
|
||||
__getitem__: Get a function from the plugin.
|
||||
__contains__: Check if a function is in the plugin.
|
||||
__iter__: Iterate over the functions in the plugin.
|
||||
update: Update the plugin with the functions from another.
|
||||
setdefault: Set a default value for a key.
|
||||
get_functions_metadata: Get the metadata for the functions in the plugin.
|
||||
|
||||
Class methods:
|
||||
from_object(plugin_name: str, plugin_instance: Any | dict[str, Any], description: str | None = None):
|
||||
Create a plugin from a existing object, like a custom class with annotated functions.
|
||||
from_directory(plugin_name: str, parent_directory: str, description: str | None = None):
|
||||
Create a plugin from a directory, parsing:
|
||||
.py files, .yaml files and directories with skprompt.txt and config.json files.
|
||||
from_openapi(
|
||||
plugin_name: str,
|
||||
openapi_document_path: str,
|
||||
execution_settings: OpenAPIFunctionExecutionParameters | None = None,
|
||||
description: str | None = None):
|
||||
Create a plugin from an OpenAPI document.
|
||||
|
||||
"""
|
||||
|
||||
name: Annotated[str, StringConstraints(pattern=PLUGIN_NAME_REGEX, min_length=1)]
|
||||
description: str | None = None
|
||||
functions: dict[str, KernelFunction] = Field(default_factory=dict)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
description: str | None = None,
|
||||
functions: (OptionalOneOrMany[KERNEL_FUNCTION_TYPE | "KernelPlugin"] | dict[str, KERNEL_FUNCTION_TYPE]) = None,
|
||||
):
|
||||
"""Create a KernelPlugin.
|
||||
|
||||
Args:
|
||||
name: The name of the plugin. The name can be upper/lower case letters and underscores.
|
||||
description: The description of the plugin.
|
||||
functions: The functions in the plugin, will be rewritten to a dictionary of functions.
|
||||
|
||||
Raises:
|
||||
ValueError: If the functions are not of the correct type.
|
||||
PydanticError: If the name is not a valid plugin name.
|
||||
"""
|
||||
super().__init__(
|
||||
name=name,
|
||||
description=description,
|
||||
functions=self._validate_functions(functions=functions, plugin_name=name),
|
||||
)
|
||||
|
||||
# region Dict-like methods
|
||||
|
||||
def __setitem__(self, key: str, value: KERNEL_FUNCTION_TYPE) -> None:
|
||||
"""Sets a function in the plugin.
|
||||
|
||||
This function uses plugin[function_name] = function syntax.
|
||||
|
||||
Args:
|
||||
key (str): The name of the function.
|
||||
value (KernelFunction): The function to set.
|
||||
|
||||
"""
|
||||
self.functions[key] = KernelPlugin._parse_or_copy(value, self.name)
|
||||
|
||||
def set(self, key: str, value: KERNEL_FUNCTION_TYPE) -> None:
|
||||
"""Set a function in the plugin.
|
||||
|
||||
This function uses plugin.set(function_name, function) syntax.
|
||||
|
||||
Args:
|
||||
key (str): The name of the function.
|
||||
value (KernelFunction): The function to set.
|
||||
|
||||
"""
|
||||
self[key] = value
|
||||
|
||||
def __getitem__(self, key: str) -> KernelFunction:
|
||||
"""Get a function from the plugin.
|
||||
|
||||
Using plugin[function_name] syntax.
|
||||
"""
|
||||
return self.functions[key]
|
||||
|
||||
def get(self, key: str, default: KernelFunction | None = None) -> KernelFunction | None:
|
||||
"""Get a function from the plugin.
|
||||
|
||||
Args:
|
||||
key (str): The name of the function.
|
||||
default (KernelFunction, optional): The default function to return if the key is not found.
|
||||
"""
|
||||
return self.functions.get(key, default)
|
||||
|
||||
def update(self, *args: Any, **kwargs: KernelFunction) -> None:
|
||||
"""Update the plugin with the functions from another.
|
||||
|
||||
Args:
|
||||
*args: The functions to update the plugin with, can be a dict, list or KernelPlugin.
|
||||
**kwargs: The kernel functions to update the plugin with.
|
||||
|
||||
"""
|
||||
if len(args) > 1:
|
||||
raise TypeError("update expected at most 1 arguments, got %d" % len(args))
|
||||
if args:
|
||||
if isinstance(args[0], KernelPlugin):
|
||||
self.add(args[0].functions)
|
||||
else:
|
||||
self.add(args[0])
|
||||
self.add(kwargs)
|
||||
|
||||
@singledispatchmethod
|
||||
def add(self, functions: Any) -> None:
|
||||
"""Add functions to the plugin."""
|
||||
raise TypeError(f"Unknown type being added, type was {type(functions)}")
|
||||
|
||||
@add.register(list)
|
||||
def add_list(self, functions: list[KERNEL_FUNCTION_TYPE | "KernelPlugin"]) -> None:
|
||||
"""Add a list of functions to the plugin."""
|
||||
for function in functions:
|
||||
if isinstance(function, KernelPlugin):
|
||||
self.add(function.functions)
|
||||
continue
|
||||
function = KernelPlugin._parse_or_copy(function, self.name)
|
||||
self[function.name] = function
|
||||
|
||||
@add.register(dict)
|
||||
def add_dict(self, functions: dict[str, KERNEL_FUNCTION_TYPE]) -> None:
|
||||
"""Add a dictionary of functions to the plugin."""
|
||||
for name, function in functions.items():
|
||||
self[name] = function
|
||||
|
||||
def setdefault(self, key: str, value: KernelFunction | None = None):
|
||||
"""Set a default value for a key."""
|
||||
if key not in self.functions:
|
||||
if value is None:
|
||||
raise ValueError("Value must be provided for new key.")
|
||||
self[key] = value
|
||||
return self[key]
|
||||
|
||||
def __iter__(self) -> Generator[KernelFunction, None, None]: # type: ignore
|
||||
"""Iterate over the functions in the plugin."""
|
||||
yield from self.functions.values()
|
||||
|
||||
def __contains__(self, key: str) -> bool:
|
||||
"""Check if a function is in the plugin."""
|
||||
return key in self.functions
|
||||
|
||||
# endregion
|
||||
# region Properties
|
||||
|
||||
def get_functions_metadata(self) -> list["KernelFunctionMetadata"]:
|
||||
"""Get the metadata for the functions in the plugin.
|
||||
|
||||
Returns:
|
||||
A list of KernelFunctionMetadata instances.
|
||||
"""
|
||||
return [func.metadata for func in self]
|
||||
|
||||
# endregion
|
||||
# region Class Methods
|
||||
|
||||
@classmethod
|
||||
def from_object(
|
||||
cls: type[_T],
|
||||
plugin_name: str,
|
||||
plugin_instance: Any | dict[str, Any],
|
||||
description: str | None = None,
|
||||
) -> _T:
|
||||
"""Creates a plugin that wraps the specified target object and imports it into the kernel's plugin collection.
|
||||
|
||||
Args:
|
||||
plugin_name (str): The name of the plugin. Allows chars: upper, lower ASCII and underscores.
|
||||
plugin_instance (Any | dict[str, Any]): The plugin instance. This can be a custom class or a
|
||||
dictionary of classes that contains methods with the kernel_function decorator for one or
|
||||
several methods. See `TextMemoryPlugin` as an example.
|
||||
description (str | None): The description of the plugin.
|
||||
|
||||
Returns:
|
||||
KernelPlugin: The imported plugin of type KernelPlugin.
|
||||
"""
|
||||
functions: list[KernelFunction] = []
|
||||
candidates: list[tuple[str, MethodType]] | ItemsView[str, Any] = []
|
||||
|
||||
if isinstance(plugin_instance, dict):
|
||||
candidates = plugin_instance.items()
|
||||
else:
|
||||
candidates = inspect.getmembers(plugin_instance, inspect.ismethod)
|
||||
candidates.extend(inspect.getmembers(plugin_instance, inspect.isfunction)) # type: ignore
|
||||
candidates.extend(inspect.getmembers(plugin_instance, inspect.iscoroutinefunction)) # type: ignore
|
||||
# Read every method from the plugin instance
|
||||
functions = [
|
||||
KernelFunctionFromMethod(method=candidate, plugin_name=plugin_name)
|
||||
for _, candidate in candidates
|
||||
if hasattr(candidate, "__kernel_function__")
|
||||
]
|
||||
if not description:
|
||||
description = getattr(plugin_instance, "description", None)
|
||||
return cls(name=plugin_name, description=description, functions=functions)
|
||||
|
||||
@classmethod
|
||||
def from_directory(
|
||||
cls: type[_T],
|
||||
plugin_name: str,
|
||||
parent_directory: str,
|
||||
description: str | None = None,
|
||||
class_init_arguments: dict[str, dict[str, Any]] | None = None,
|
||||
encoding: str = "utf-8",
|
||||
) -> _T:
|
||||
"""Create a plugin from a specified directory.
|
||||
|
||||
This method does not recurse into subdirectories beyond one level deep from the specified plugin directory.
|
||||
For YAML files, function names are extracted from the content of the YAML files themselves (the name property).
|
||||
For directories, the function name is assumed to be the name of the directory. Each KernelFunction object is
|
||||
initialized with data parsed from the associated files and added to a list of functions that are then assigned
|
||||
to the created KernelPlugin object.
|
||||
A .py file is parsed and a plugin created,
|
||||
the functions within as then combined with any other functions found.
|
||||
The python file needs to contain a class with one or more kernel_function decorated methods.
|
||||
If this class has a `__init__` method, it will be called with the arguments provided in the
|
||||
`class_init_arguments` dictionary, the key needs to be the same as the name of the class,
|
||||
with the value being a dictionary of arguments to pass to the class (using kwargs).
|
||||
|
||||
Example:
|
||||
Assuming a plugin directory structure as follows:
|
||||
MyPlugins/
|
||||
|--- pluginA.yaml
|
||||
|--- pluginB.yaml
|
||||
|--- native_function.py
|
||||
|--- Directory1/
|
||||
|--- skprompt.txt
|
||||
|--- config.json
|
||||
|--- Directory2/
|
||||
|--- skprompt.txt
|
||||
|--- config.json
|
||||
|
||||
Calling `KernelPlugin.from_directory("MyPlugins", "/path/to")` will create a KernelPlugin object named
|
||||
"MyPlugins", containing KernelFunction objects for `pluginA.yaml`, `pluginB.yaml`,
|
||||
`Directory1`, and `Directory2`, each initialized with their respective configurations.
|
||||
And functions for anything within native_function.py.
|
||||
|
||||
Args:
|
||||
plugin_name (str): The name of the plugin, this is the name of the directory within the parent directory
|
||||
parent_directory (str): The parent directory path where the plugin directory resides
|
||||
description (str | None): The description of the plugin
|
||||
class_init_arguments (dict[str, dict[str, Any]] | None): The class initialization arguments
|
||||
encoding (str): The encoding to use when reading text files. Defaults to "utf-8".
|
||||
|
||||
Returns:
|
||||
KernelPlugin: The created plugin of type KernelPlugin.
|
||||
|
||||
Raises:
|
||||
PluginInitializationError: If the plugin directory does not exist.
|
||||
PluginInvalidNameError: If the plugin name is invalid.
|
||||
"""
|
||||
plugin_directory = os.path.abspath(os.path.join(parent_directory, plugin_name))
|
||||
if not os.path.exists(plugin_directory):
|
||||
raise PluginInitializationError(f"Plugin directory does not exist: {plugin_name}")
|
||||
|
||||
functions: list[KernelFunction] = []
|
||||
for object in glob(os.path.join(plugin_directory, "*")):
|
||||
logger.debug(f"Found object: {object}")
|
||||
if os.path.isdir(object):
|
||||
if os.path.basename(object).startswith("__"):
|
||||
continue
|
||||
try:
|
||||
functions.append(KernelFunctionFromPrompt.from_directory(path=object, encoding=encoding))
|
||||
except FunctionInitializationError:
|
||||
logger.warning(f"Failed to create function from directory: {object}")
|
||||
elif object.endswith(".yaml") or object.endswith(".yml"):
|
||||
with open(object, encoding=encoding) as file:
|
||||
try:
|
||||
functions.append(KernelFunctionFromPrompt.from_yaml(file.read()))
|
||||
except FunctionInitializationError:
|
||||
logger.warning(f"Failed to create function from YAML file: {object}")
|
||||
elif object.endswith(".py"):
|
||||
try:
|
||||
functions.extend(
|
||||
cls.from_python_file(
|
||||
plugin_name=plugin_name,
|
||||
py_file=object,
|
||||
description=description,
|
||||
class_init_arguments=class_init_arguments,
|
||||
)
|
||||
)
|
||||
except PluginInitializationError:
|
||||
logger.warning(f"Failed to create function from Python file: {object}")
|
||||
else:
|
||||
logger.warning(f"Unknown file found: {object}")
|
||||
if not functions:
|
||||
raise PluginInitializationError(f"No functions found in folder: {parent_directory}/{plugin_name}")
|
||||
return cls(name=plugin_name, description=description, functions=functions)
|
||||
|
||||
@classmethod
|
||||
def from_openapi(
|
||||
cls: type[_T],
|
||||
plugin_name: str,
|
||||
openapi_document_path: str | None = None,
|
||||
openapi_parsed_spec: dict[str, Any] | None = None,
|
||||
execution_settings: "OpenAPIFunctionExecutionParameters | None" = None,
|
||||
description: str | None = None,
|
||||
) -> _T:
|
||||
"""Create a plugin from an OpenAPI document.
|
||||
|
||||
Args:
|
||||
plugin_name: The name of the plugin
|
||||
openapi_document_path: The path to the OpenAPI document (optional)
|
||||
openapi_parsed_spec: The parsed OpenAPI spec (optional)
|
||||
execution_settings: The execution parameters
|
||||
description: The description of the plugin
|
||||
|
||||
Returns:
|
||||
KernelPlugin: The created plugin
|
||||
|
||||
Raises:
|
||||
PluginInitializationError: if the plugin URL or plugin JSON/YAML is not provided
|
||||
"""
|
||||
from semantic_kernel.connectors.openapi_plugin.openapi_manager import create_functions_from_openapi
|
||||
|
||||
if not openapi_document_path and not openapi_parsed_spec:
|
||||
raise PluginInitializationError("Either the OpenAPI document path or a parsed OpenAPI spec is required.")
|
||||
|
||||
return cls( # type: ignore
|
||||
name=plugin_name,
|
||||
description=description,
|
||||
functions=create_functions_from_openapi( # type: ignore
|
||||
plugin_name=plugin_name,
|
||||
openapi_document_path=openapi_document_path,
|
||||
openapi_parsed_spec=openapi_parsed_spec,
|
||||
execution_settings=execution_settings,
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_python_file(
|
||||
cls: type[_T],
|
||||
plugin_name: str,
|
||||
py_file: str,
|
||||
description: str | None = None,
|
||||
class_init_arguments: dict[str, dict[str, Any]] | None = None,
|
||||
) -> _T:
|
||||
"""Create a plugin from a Python file."""
|
||||
module_name = os.path.basename(py_file).replace(".py", "")
|
||||
spec = importlib.util.spec_from_file_location(module_name, py_file)
|
||||
if not spec:
|
||||
raise PluginInitializationError(f"Could not load spec from file {py_file}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
if not module or not spec.loader:
|
||||
raise PluginInitializationError(f"No module found in file {py_file}")
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
for name, cls_instance in inspect.getmembers(module, inspect.isclass):
|
||||
if cls_instance.__module__ != module_name:
|
||||
continue
|
||||
# Check whether this class has at least one @kernel_function decorated method
|
||||
has_kernel_function = False
|
||||
for _, method in inspect.getmembers(cls_instance, inspect.isfunction):
|
||||
if getattr(method, "__kernel_function__", False):
|
||||
has_kernel_function = True
|
||||
break
|
||||
if not has_kernel_function:
|
||||
continue
|
||||
init_args = class_init_arguments.get(name, {}) if class_init_arguments else {}
|
||||
instance = getattr(module, name)(**init_args)
|
||||
return cls.from_object(plugin_name=plugin_name, description=description, plugin_instance=instance)
|
||||
raise PluginInitializationError(f"No class found in file: {py_file}")
|
||||
|
||||
# endregion
|
||||
# region Internal Static Methods
|
||||
|
||||
@staticmethod
|
||||
def _validate_functions(
|
||||
functions: OptionalOneOrMany[KERNEL_FUNCTION_TYPE | "KernelPlugin"] | dict[str, KERNEL_FUNCTION_TYPE],
|
||||
plugin_name: str,
|
||||
) -> dict[str, "KernelFunction"]:
|
||||
"""Validates the functions and returns a dictionary of functions."""
|
||||
if not functions or not plugin_name:
|
||||
# if the plugin_name is not present, the validation will fail, so no point in parsing.
|
||||
return {}
|
||||
if isinstance(functions, dict):
|
||||
return {
|
||||
name: KernelPlugin._parse_or_copy(function=function, plugin_name=plugin_name)
|
||||
for name, function in functions.items()
|
||||
}
|
||||
if isinstance(functions, KernelPlugin):
|
||||
return {
|
||||
name: function.function_copy(plugin_name=plugin_name) for name, function in functions.functions.items()
|
||||
}
|
||||
if isinstance(functions, KernelFunction):
|
||||
return {functions.name: KernelPlugin._parse_or_copy(function=functions, plugin_name=plugin_name)}
|
||||
if callable(functions):
|
||||
function = KernelPlugin._parse_or_copy(function=functions, plugin_name=plugin_name)
|
||||
return {function.name: function}
|
||||
if isinstance(functions, list):
|
||||
functions_dict: dict[str, KernelFunction] = {}
|
||||
for function in functions: # type: ignore
|
||||
if isinstance(function, KernelFunction) or callable(function):
|
||||
function = KernelPlugin._parse_or_copy(function=function, plugin_name=plugin_name)
|
||||
functions_dict[function.name] = function
|
||||
elif isinstance(function, KernelPlugin): # type: ignore
|
||||
functions_dict.update({
|
||||
name: KernelPlugin._parse_or_copy(function=function, plugin_name=plugin_name)
|
||||
for name, function in function.functions.items()
|
||||
})
|
||||
else:
|
||||
raise ValueError(f"Invalid type for functions in list: {function} (type: {type(function)})")
|
||||
return functions_dict
|
||||
raise ValueError(f"Invalid type for supplied functions: {functions} (type: {type(functions)})")
|
||||
|
||||
@staticmethod
|
||||
def _parse_or_copy(function: KERNEL_FUNCTION_TYPE, plugin_name: str) -> "KernelFunction":
|
||||
"""Handle the function and return a KernelFunction instance."""
|
||||
if isinstance(function, KernelFunction):
|
||||
return function.function_copy(plugin_name=plugin_name)
|
||||
if callable(function):
|
||||
return KernelFunctionFromMethod(method=function, plugin_name=plugin_name)
|
||||
raise ValueError(f"Invalid type for function: {function} (type: {type(function)})")
|
||||
|
||||
# endregion
|
||||
@@ -0,0 +1,22 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
|
||||
from semantic_kernel.functions.function_result import FunctionResult
|
||||
from semantic_kernel.kernel_pydantic import KernelBaseModel
|
||||
from semantic_kernel.services.ai_service_client_base import AIServiceClientBase
|
||||
|
||||
|
||||
class PromptRenderingResult(KernelBaseModel):
|
||||
"""Represents the result of rendering a prompt template.
|
||||
|
||||
Attributes:
|
||||
rendered_prompt (str): The rendered prompt.
|
||||
ai_service (Any): The AI service that rendered the prompt.
|
||||
execution_settings (PromptExecutionSettings): The execution settings for the prompt.
|
||||
function_result (FunctionResult): The result of executing the prompt.
|
||||
"""
|
||||
|
||||
rendered_prompt: str
|
||||
ai_service: AIServiceClientBase
|
||||
execution_settings: PromptExecutionSettings
|
||||
function_result: FunctionResult | None = None
|
||||
@@ -0,0 +1,8 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any, Union
|
||||
|
||||
from semantic_kernel.functions.kernel_function import KernelFunction
|
||||
|
||||
KERNEL_FUNCTION_TYPE = Union[KernelFunction, Callable[..., Any]]
|
||||
Reference in New Issue
Block a user