chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run

This commit is contained in:
wehub-resource-sync
2026-07-13 13:21:23 +08:00
commit b957a53def
5423 changed files with 863745 additions and 0 deletions
@@ -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]]