# Copyright (c) Microsoft. All rights reserved. import logging from collections.abc import Callable from typing import TYPE_CHECKING, Literal, TypeVar from semantic_kernel.connectors.ai.function_choice_type import FunctionChoiceType from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError from semantic_kernel.kernel_pydantic import KernelBaseModel from semantic_kernel.utils.feature_stage_decorator import experimental if TYPE_CHECKING: from semantic_kernel.connectors.ai.function_call_choice_configuration import FunctionCallChoiceConfiguration from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings from semantic_kernel.kernel import Kernel DEFAULT_MAX_AUTO_INVOKE_ATTEMPTS = 5 logger = logging.getLogger(__name__) _T = TypeVar("_T", bound="FunctionChoiceBehavior") @experimental class FunctionChoiceBehavior(KernelBaseModel): """Class that controls function choice behavior. Attributes: enable_kernel_functions: Enable kernel functions. max_auto_invoke_attempts: The maximum number of auto invoke attempts. filters: Filters for the function choice behavior. Available options are: excluded_plugins, included_plugins, excluded_functions, or included_functions. type_: The type of function choice behavior. Properties: auto_invoke_kernel_functions: Check if the kernel functions should be auto-invoked. Determined as max_auto_invoke_attempts > 0. Methods: configure: Configures the settings for the function call behavior, the default version in this class, does nothing, use subclasses for different behaviors. Class methods: Auto: Returns FunctionChoiceBehavior class with auto_invoke enabled, and the desired functions based on either the specified filters or the full qualified names. The model will decide which function to use, if any. NoneInvoke: Returns FunctionChoiceBehavior class with auto_invoke disabled, and the desired functions based on either the specified filters or the full qualified names. The model does not invoke any functions, but can rather describe how it would invoke a function to complete a given task/query. Required: Returns FunctionChoiceBehavior class with auto_invoke enabled, and the desired functions based on either the specified filters or the full qualified names. The model is required to use one of the provided functions to complete a given task/query. """ enable_kernel_functions: bool = True maximum_auto_invoke_attempts: int = DEFAULT_MAX_AUTO_INVOKE_ATTEMPTS filters: ( dict[Literal["excluded_plugins", "included_plugins", "excluded_functions", "included_functions"], list[str]] | None ) = None type_: FunctionChoiceType | None = None @property def auto_invoke_kernel_functions(self): """Return True if auto_invoke_kernel_functions is enabled.""" return self.maximum_auto_invoke_attempts > 0 @auto_invoke_kernel_functions.setter def auto_invoke_kernel_functions(self, value: bool): """Set the auto_invoke_kernel_functions property.""" self.maximum_auto_invoke_attempts = DEFAULT_MAX_AUTO_INVOKE_ATTEMPTS if value else 0 def _check_and_get_config( self, kernel: "Kernel", filters: dict[ Literal["excluded_plugins", "included_plugins", "excluded_functions", "included_functions"], list[str] ] | None = None, ) -> "FunctionCallChoiceConfiguration": """Check for missing functions and get the function call choice configuration.""" from semantic_kernel.connectors.ai.function_call_choice_configuration import FunctionCallChoiceConfiguration if filters: return FunctionCallChoiceConfiguration(available_functions=kernel.get_list_of_function_metadata(filters)) return FunctionCallChoiceConfiguration(available_functions=kernel.get_full_list_of_function_metadata()) def configure( self, kernel: "Kernel", update_settings_callback: Callable[..., None], settings: "PromptExecutionSettings", ) -> None: """Configure the function choice behavior.""" if not self.enable_kernel_functions: return config = self.get_config(kernel) if config: update_settings_callback(config, settings, self.type_) def get_config(self, kernel: "Kernel") -> "FunctionCallChoiceConfiguration": """Get the function call choice configuration based on the type.""" return self._check_and_get_config(kernel, self.filters) @classmethod def Auto( cls: type[_T], auto_invoke: bool = True, *, filters: dict[ Literal["excluded_plugins", "included_plugins", "excluded_functions", "included_functions"], list[str] ] | None = None, **kwargs, ) -> _T: """Creates a FunctionChoiceBehavior with type AUTO. Returns FunctionChoiceBehavior class with auto_invoke enabled, and the desired functions based on either the specified filters or the full qualified names. The model will decide which function to use, if any. """ kwargs.setdefault("maximum_auto_invoke_attempts", DEFAULT_MAX_AUTO_INVOKE_ATTEMPTS if auto_invoke else 0) return cls( type_=FunctionChoiceType.AUTO, filters=filters, **kwargs, ) @classmethod def NoneInvoke( cls: type[_T], *, filters: dict[ Literal["excluded_plugins", "included_plugins", "excluded_functions", "included_functions"], list[str] ] | None = None, **kwargs, ) -> _T: """Creates a FunctionChoiceBehavior with type NONE. Returns FunctionChoiceBehavior class with auto_invoke disabled, and the desired functions based on either the specified filters or the full qualified names. The model does not invoke any functions, but can rather describe how it would invoke a function to complete a given task/query. """ kwargs.setdefault("maximum_auto_invoke_attempts", 0) return cls( type_=FunctionChoiceType.NONE, filters=filters, **kwargs, ) @classmethod def Required( cls: type[_T], auto_invoke: bool = True, *, filters: dict[ Literal["excluded_plugins", "included_plugins", "excluded_functions", "included_functions"], list[str] ] | None = None, **kwargs, ) -> _T: """Creates a FunctionChoiceBehavior with type REQUIRED. Returns FunctionChoiceBehavior class with auto_invoke enabled, and the desired functions based on either the specified filters or the full qualified names. The model is required to use one of the provided functions to complete a given task/query. """ kwargs.setdefault("maximum_auto_invoke_attempts", 1 if auto_invoke else 0) return cls( type_=FunctionChoiceType.REQUIRED, filters=filters, **kwargs, ) @classmethod def from_dict(cls: type[_T], data: dict) -> _T: """Create a FunctionChoiceBehavior from a dictionary.""" from semantic_kernel.connectors.ai.function_calling_utils import _combine_filter_dicts type_map = { "auto": cls.Auto, "none": cls.NoneInvoke, "required": cls.Required, } behavior_type = data.pop("type", "auto") auto_invoke = data.pop("auto_invoke", False) functions = data.pop("functions", None) filters = data.pop("filters", None) if functions: valid_fqns = [name.replace(".", "-") for name in functions] if filters: filters = _combine_filter_dicts(filters, {"included_functions": valid_fqns}) else: filters = {"included_functions": valid_fqns} return type_map[behavior_type]( # type: ignore auto_invoke=auto_invoke, filters=filters, **data, ) @classmethod def from_string(cls: type[_T], data: str) -> _T: """Create a FunctionChoiceBehavior from a string. This method converts the provided string to a FunctionChoiceBehavior object for the specified type. """ type_value = data.lower() if type_value == "auto": return cls.Auto() if type_value == "none": return cls.NoneInvoke() if type_value == "required": return cls.Required() raise ServiceInitializationError( f"The specified type `{type_value}` is not supported. Allowed types are: `auto`, `none`, `required`." )