diff --git a/python/notebooks/05-using-the-planner.ipynb b/python/notebooks/05-using-the-planner.ipynb index 5d89d50a47dc..61427b4e2917 100644 --- a/python/notebooks/05-using-the-planner.ipynb +++ b/python/notebooks/05-using-the-planner.ipynb @@ -669,7 +669,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.7" + "version": "3.10.12" } }, "nbformat": 4, diff --git a/python/notebooks/08-native-function-inline.ipynb b/python/notebooks/08-native-function-inline.ipynb index 5f7c1c78c875..85fa914061a1 100644 --- a/python/notebooks/08-native-function-inline.ipynb +++ b/python/notebooks/08-native-function-inline.ipynb @@ -169,7 +169,7 @@ " top_p=0.5,\n", ")\n", "\n", - "generate_number_plugin = kernel.import_plugin(GenerateNumberPlugin())" + "generate_number_plugin = kernel.import_plugin(GenerateNumberPlugin(), \"GenerateNumberPlugin\")" ] }, { @@ -309,7 +309,7 @@ "metadata": {}, "outputs": [], "source": [ - "generate_number_plugin = kernel.import_plugin(GenerateNumberPlugin())\n", + "generate_number_plugin = kernel.import_plugin(GenerateNumberPlugin(), \"GenerateNumberPlugin\")\n", "generate_number = generate_number_plugin[\"GenerateNumber\"]" ] }, @@ -496,24 +496,6 @@ "\"\"\"" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "73aca517", - "metadata": {}, - "outputs": [], - "source": [ - "corgi_story = kernel.create_semantic_function(\n", - " prompt_template=sk_prompt,\n", - " function_name=\"CorgiStory\",\n", - " plugin_name=\"CorgiPlugin\",\n", - " description=\"Write a short story about two Corgis on an adventure\",\n", - " max_tokens=500,\n", - " temperature=0.5,\n", - " top_p=0.5,\n", - ")" - ] - }, { "cell_type": "code", "execution_count": null, diff --git a/python/notebooks/third_party/weaviate-persistent-memory.ipynb b/python/notebooks/third_party/weaviate-persistent-memory.ipynb index 3b177e05706a..c2bf2a7b4462 100644 --- a/python/notebooks/third_party/weaviate-persistent-memory.ipynb +++ b/python/notebooks/third_party/weaviate-persistent-memory.ipynb @@ -509,4 +509,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/python/samples/kernel-syntax-examples/google_palm_chat_with_memory.py b/python/samples/kernel-syntax-examples/google_palm_chat_with_memory.py index ff1b64a078c9..ada582f77ee5 100644 --- a/python/samples/kernel-syntax-examples/google_palm_chat_with_memory.py +++ b/python/samples/kernel-syntax-examples/google_palm_chat_with_memory.py @@ -13,7 +13,7 @@ palm_chat_completion = sk_gp.GooglePalmChatCompletion("models/chat-bison-001", apikey) kernel.add_chat_service("models/chat-bison-001", palm_chat_completion) kernel.register_memory_store(memory_store=sk.memory.VolatileMemoryStore()) -kernel.import_plugin(sk.core_plugins.TextMemoryPlugin()) +kernel.import_plugin(sk.core_plugins.TextMemoryPlugin(), "TextMemoryPlugin") async def populate_memory(kernel: sk.Kernel) -> None: diff --git a/python/samples/kernel-syntax-examples/memory.py b/python/samples/kernel-syntax-examples/memory.py index 31cb41d95e5b..e046cd826cad 100644 --- a/python/samples/kernel-syntax-examples/memory.py +++ b/python/samples/kernel-syntax-examples/memory.py @@ -61,7 +61,7 @@ async def setup_chat_with_memory( context["fact5"] = "what do I do for work?" context[sk.core_plugins.TextMemoryPlugin.COLLECTION_PARAM] = "aboutMe" - context[sk.core_plugins.TextMemoryPlugin.RELEVANCE_PARAM] = 0.8 + context[sk.core_plugins.TextMemoryPlugin.RELEVANCE_PARAM] = "0.8" context["chat_history"] = "" @@ -100,7 +100,7 @@ async def main() -> None: ) kernel.register_memory_store(memory_store=sk.memory.VolatileMemoryStore()) - kernel.import_plugin(sk.core_plugins.TextMemoryPlugin()) + kernel.import_plugin(sk.core_plugins.TextMemoryPlugin(), "TextMemoryPlugin") print("Populating memory...") await populate_memory(kernel) diff --git a/python/samples/kernel-syntax-examples/plugins_from_dir.py b/python/samples/kernel-syntax-examples/plugins_from_dir.py index 9bc957c89ed9..08378fa44c9d 100644 --- a/python/samples/kernel-syntax-examples/plugins_from_dir.py +++ b/python/samples/kernel-syntax-examples/plugins_from_dir.py @@ -8,7 +8,7 @@ kernel = sk.Kernel() -useAzureOpenAI = True +useAzureOpenAI = False model = "gpt-35-turbo-instruct" if useAzureOpenAI else "gpt-3.5-turbo-instruct" service_id = model @@ -23,7 +23,7 @@ api_key, org_id = sk.openai_settings_from_dot_env() kernel.add_text_completion_service( service_id, - sk_oai.OpenAITextCompletion(deployment_name=model, api_key=api_key, org_id=org_id), + sk_oai.OpenAITextCompletion(ai_model_id=model, api_key=api_key, org_id=org_id), ) # note: using plugins from the samples folder diff --git a/python/samples/kernel-syntax-examples/self-critique_rag.py b/python/samples/kernel-syntax-examples/self-critique_rag.py index 35ac49a2acba..05b12dd3f524 100644 --- a/python/samples/kernel-syntax-examples/self-critique_rag.py +++ b/python/samples/kernel-syntax-examples/self-critique_rag.py @@ -48,6 +48,8 @@ async def main() -> None: kernel.add_text_completion_service( "dv", AzureTextCompletion( + # Note: text-davinci-003 is deprecated and will be replaced by + # AzureOpenAI's gpt-35-turbo-instruct model. deployment_name="gpt-35-turbo-instruct", endpoint=AZURE_OPENAI_ENDPOINT, api_key=AZURE_OPENAI_API_KEY, diff --git a/python/semantic_kernel/kernel.py b/python/semantic_kernel/kernel.py index f73054aab135..4d13cc0963f4 100644 --- a/python/semantic_kernel/kernel.py +++ b/python/semantic_kernel/kernel.py @@ -6,7 +6,8 @@ import logging import os from typing import Any, Callable, Dict, List, Optional, Type, TypeVar, Union -from uuid import uuid4 + +from pydantic import Field from semantic_kernel.connectors.ai.ai_exception import AIException from semantic_kernel.connectors.ai.chat_completion_client_base import ( @@ -30,12 +31,9 @@ from semantic_kernel.orchestration.kernel_function import KernelFunction from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase from semantic_kernel.plugin_definition.function_view import FunctionView -from semantic_kernel.plugin_definition.plugin_collection import PluginCollection -from semantic_kernel.plugin_definition.plugin_collection_base import ( - PluginCollectionBase, -) -from semantic_kernel.plugin_definition.read_only_plugin_collection_base import ( - ReadOnlyPluginCollectionBase, +from semantic_kernel.plugin_definition.kernel_plugin import KernelPlugin +from semantic_kernel.plugin_definition.kernel_plugin_collection import ( + KernelPluginCollection, ) from semantic_kernel.reliability.pass_through_without_retry import ( PassThroughWithoutRetry, @@ -52,6 +50,7 @@ from semantic_kernel.template_engine.protocols.prompt_templating_engine import ( PromptTemplatingEngine, ) +from semantic_kernel.utils.naming import generate_random_ascii_name from semantic_kernel.utils.validation import ( validate_function_name, validate_plugin_name, @@ -63,20 +62,21 @@ class Kernel: - _plugin_collection: PluginCollectionBase + plugins: Optional[KernelPluginCollection] = Field(default_factory=KernelPluginCollection) + # TODO: pydantic-ify these fields _prompt_template_engine: PromptTemplatingEngine _memory: SemanticTextMemoryBase def __init__( self, - plugin_collection: Optional[PluginCollectionBase] = None, + plugins: Optional[KernelPluginCollection] = None, prompt_template_engine: Optional[PromptTemplatingEngine] = None, memory: Optional[SemanticTextMemoryBase] = None, log: Optional[Any] = None, ) -> None: if log: logger.warning("The `log` parameter is deprecated. Please use the `logging` module instead.") - self._plugin_collection = plugin_collection if plugin_collection else PluginCollection() + self.plugins = plugins if plugins else KernelPluginCollection() self._prompt_template_engine = prompt_template_engine if prompt_template_engine else PromptTemplateEngine() self._memory = memory if memory else NullMemory() @@ -101,9 +101,26 @@ def memory(self) -> SemanticTextMemoryBase: def prompt_template_engine(self) -> PromptTemplatingEngine: return self._prompt_template_engine - @property - def plugins(self) -> ReadOnlyPluginCollectionBase: - return self._plugin_collection.read_only_plugin_collection + def add_plugin( + self, plugin_name: str, functions: List[KernelFunctionBase], plugin: Optional[KernelPlugin] = None + ) -> None: + """ + Adds a plugin to the kernel's collection of plugins. If a plugin instance is provided, + it uses that instance instead of creating a new KernelPlugin. + + Args: + plugin_name (str): The name of the plugin + functions (List[KernelFunctionBase]): The functions to add to the plugin + plugin (Optional[KernelPlugin]): An optional pre-defined plugin instance + """ + if plugin is None: + # If no plugin instance is provided, create a new KernelPlugin + plugin = KernelPlugin(name=plugin_name, functions=functions) + + if plugin_name in self.plugins: + self.plugins.add_functions_to_plugin(functions=functions, plugin_name=plugin_name) + else: + self.plugins.add(plugin) def register_semantic_function( self, @@ -111,15 +128,29 @@ def register_semantic_function( function_name: str, function_config: SemanticFunctionConfig, ) -> KernelFunctionBase: + """ + Creates a semantic function from the plugin name, function name and function config + + Args: + plugin_name (Optional[str]): The name of the plugin. If empty, a random name will be generated. + function_name (str): The name of the function + function_config (SemanticFunctionConfig): The function config + + Returns: + KernelFunctionBase: The created semantic function + + Raises: + ValueError: If the plugin name or function name are invalid + """ if plugin_name is None or plugin_name == "": - plugin_name = PluginCollection.GLOBAL_PLUGIN + plugin_name = f"p_{generate_random_ascii_name()}" assert plugin_name is not None # for type checker validate_plugin_name(plugin_name) validate_function_name(function_name) function = self._create_semantic_function(plugin_name, function_name, function_config) - self._plugin_collection.add_semantic_function(function) + self.add_plugin(plugin_name, [function]) return function @@ -128,6 +159,16 @@ def register_native_function( plugin_name: Optional[str], kernel_function: Callable, ) -> KernelFunctionBase: + """ + Creates a native function from the plugin name and kernel function + + Args: + plugin_name (Optional[str]): The name of the plugin. If empty, a random name will be generated. + kernel_function (Callable): The kernel function + + Returns: + KernelFunctionBase: The created native function + """ if not hasattr(kernel_function, "__kernel_function__"): raise KernelException( KernelException.ErrorCodes.InvalidFunctionType, @@ -136,22 +177,20 @@ def register_native_function( function_name = kernel_function.__kernel_function_name__ if plugin_name is None or plugin_name == "": - plugin_name = PluginCollection.GLOBAL_PLUGIN + plugin_name = f"p_{generate_random_ascii_name()}" assert plugin_name is not None # for type checker validate_plugin_name(plugin_name) validate_function_name(function_name) - function = KernelFunction.from_native_method(kernel_function, plugin_name) - - if self.plugins.has_function(plugin_name, function_name): + if plugin_name in self.plugins and function_name in self.plugins[plugin_name]: raise KernelException( KernelException.ErrorCodes.FunctionOverloadNotSupported, "Overloaded functions are not supported, " "please differentiate function names.", ) - function.set_default_plugin_collection(self.plugins) - self._plugin_collection.add_native_function(function) + function = KernelFunction.from_native_method(kernel_function, plugin_name) + self.add_plugin(plugin_name, [function]) return function @@ -197,9 +236,9 @@ async def run_stream( else: variables = ContextVariables() context = KernelContext( - variables, - self._memory, - self._plugin_collection.read_only_plugin_collection, + variables=variables, + memory=self._memory, + plugins=self.plugins, ) else: raise ValueError("No functions passed to run") @@ -252,9 +291,9 @@ async def run( else: variables = ContextVariables() context = KernelContext( - variables, - self._memory, - self._plugin_collection.read_only_plugin_collection, + variables=variables, + memory=self._memory, + plugins=self.plugins, ) pipeline_step = 0 @@ -335,10 +374,11 @@ async def run( return context def func(self, plugin_name: str, function_name: str) -> KernelFunctionBase: - if self.plugins.has_native_function(plugin_name, function_name): - return self.plugins.get_native_function(plugin_name, function_name) - - return self.plugins.get_semantic_function(plugin_name, function_name) + if plugin_name not in self.plugins: + raise ValueError(f"Plugin '{plugin_name}' not found") + if function_name not in self.plugins[plugin_name]: + raise ValueError(f"Function '{function_name}' not found in plugin '{plugin_name}'") + return self.plugins[plugin_name][function_name] def use_memory( self, @@ -392,12 +432,26 @@ def on_function_invoked(self, function_view: FunctionView, context: KernelContex return args return None - def import_plugin(self, plugin_instance: Any, plugin_name: str = "") -> Dict[str, KernelFunctionBase]: - if plugin_name.strip() == "": - plugin_name = PluginCollection.GLOBAL_PLUGIN - logger.debug(f"Importing plugin {plugin_name} into the global namespace") - else: - logger.debug(f"Importing plugin {plugin_name}") + def import_plugin(self, plugin_instance: Union[Any, Dict[str, Any]], plugin_name: str) -> KernelPlugin: + """ + Import a plugin into the kernel. + + Args: + 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. + plugin_name (str): The name of the plugin. Allows chars: upper, lower ASCII and underscores. + + Returns: + KernelPlugin: The imported plugin of type KernelPlugin. + """ + if not plugin_name.strip(): + logger.warn("Unable to import plugin due to missing plugin_name") + raise KernelException( + KernelException.ErrorCodes.InvalidPluginName, + "Plugin name cannot be empty", + ) + logger.debug(f"Importing plugin {plugin_name}") functions = [] @@ -423,11 +477,18 @@ def import_plugin(self, plugin_instance: Any, plugin_name: str = "") -> Dict[str ("Overloaded functions are not supported, " "please differentiate function names."), ) - plugin = {} - for function in functions: - function.set_default_plugin_collection(self.plugins) - self._plugin_collection.add_native_function(function) - plugin[function.name] = function + # This is legacy - figure out why we're setting all plugins on each function? + for func in functions: + func.set_default_plugin_collection(self.plugins) + + plugin = KernelPlugin(name=plugin_name, functions=functions) + # TODO: we shouldn't have to be adding functions to a plugin after the fact + # This isn't done in dotnet, and needs to be revisited as we move to v1.0 + # This is to support the current state of the code + if plugin_name in self.plugins: + self.plugins.add_functions_to_plugin(functions=functions, plugin_name=plugin_name) + else: + self.plugins.add(plugin) return plugin @@ -643,11 +704,6 @@ def _create_semantic_function( function_config.prompt_template_config.execution_settings ) - # Connect the function to the current kernel plugin - # collection, in case the function is invoked manually - # without a context and without a way to find other functions. - function.set_default_plugin_collection(self.plugins) - if function_config.has_chat_prompt: service = self.get_ai_service( ChatCompletionClientBase, @@ -703,9 +759,7 @@ def _create_semantic_function( return function - def import_native_plugin_from_directory( - self, parent_directory: str, plugin_directory_name: str - ) -> Dict[str, KernelFunctionBase]: + def import_native_plugin_from_directory(self, parent_directory: str, plugin_directory_name: str) -> KernelPlugin: MODULE_NAME = "native_function" validate_plugin_name(plugin_directory_name) @@ -732,9 +786,7 @@ def import_native_plugin_from_directory( return {} - def import_semantic_plugin_from_directory( - self, parent_directory: str, plugin_directory_name: str - ) -> Dict[str, KernelFunctionBase]: + def import_semantic_plugin_from_directory(self, parent_directory: str, plugin_directory_name: str) -> KernelPlugin: CONFIG_FILE = "config.json" PROMPT_FILE = "skprompt.txt" @@ -746,7 +798,7 @@ def import_semantic_plugin_from_directory( if not os.path.exists(plugin_directory): raise ValueError(f"Plugin directory does not exist: {plugin_directory_name}") - plugin = {} + functions = [] directories = glob.glob(plugin_directory + "/*/") for directory in directories: @@ -769,9 +821,13 @@ def import_semantic_plugin_from_directory( # Prepare lambda wrapping AI logic function_config = SemanticFunctionConfig(config, template) - plugin[function_name] = self.register_semantic_function( - plugin_directory_name, function_name, function_config - ) + # TODO: this is an example of where plugins are added to the collection in the kernel + # as part of the register_semantic_function, seems weird to have it hidden? + # should the register function simply register the function and then we can add to the + # plugin collection later? + functions += [self.register_semantic_function(plugin_directory_name, function_name, function_config)] + + plugin = KernelPlugin(name=plugin_directory_name, functions=functions) return plugin @@ -783,7 +839,7 @@ def create_semantic_function( description: Optional[str] = None, **kwargs: Any, ) -> "KernelFunctionBase": - function_name = function_name if function_name is not None else f"f_{str(uuid4()).replace('-', '_')}" + function_name = function_name if function_name is not None else f"f_{generate_random_ascii_name()}" config = PromptTemplateConfig( description=(description if description is not None else "Generic function, unknown purpose"), diff --git a/python/semantic_kernel/kernel_exception.py b/python/semantic_kernel/kernel_exception.py index ee6e7c2b8963..fb8b27542afa 100644 --- a/python/semantic_kernel/kernel_exception.py +++ b/python/semantic_kernel/kernel_exception.py @@ -28,6 +28,8 @@ class ErrorCodes(Enum): FunctionInvokeError = 8 # Ambiguous implementation. AmbiguousImplementation = 9 + # Invalid plugin name + InvalidPluginName = 10 # The error code. _error_code: ErrorCodes diff --git a/python/semantic_kernel/orchestration/__init__.py b/python/semantic_kernel/orchestration/__init__.py new file mode 100644 index 000000000000..4db4f2036d6f --- /dev/null +++ b/python/semantic_kernel/orchestration/__init__.py @@ -0,0 +1,7 @@ +from semantic_kernel.orchestration.kernel_function import KernelFunction +from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase + +__all__ = [ + "KernelFunctionBase", + "KernelFunction", +] diff --git a/python/semantic_kernel/orchestration/kernel_context.py b/python/semantic_kernel/orchestration/kernel_context.py index 1f3bf731b7d7..71e5e33e810b 100644 --- a/python/semantic_kernel/orchestration/kernel_context.py +++ b/python/semantic_kernel/orchestration/kernel_context.py @@ -1,23 +1,17 @@ # Copyright (c) Microsoft. All rights reserved. import logging -from typing import Any, Dict, Generic, Literal, Optional, Tuple, Union +from typing import Any, Dict, Generic, Optional, Union from pydantic import Field, PrivateAttr -from semantic_kernel.kernel_exception import KernelException from semantic_kernel.kernel_pydantic import KernelBaseModel from semantic_kernel.memory.semantic_text_memory_base import ( SemanticTextMemoryBase, SemanticTextMemoryT, ) from semantic_kernel.orchestration.context_variables import ContextVariables -from semantic_kernel.plugin_definition.read_only_plugin_collection import ( - ReadOnlyPluginCollection, -) -from semantic_kernel.plugin_definition.read_only_plugin_collection_base import ( - ReadOnlyPluginCollectionBase, -) +from semantic_kernel.plugin_definition.kernel_plugin_collection import KernelPluginCollection logger: logging.Logger = logging.getLogger(__name__) @@ -28,7 +22,7 @@ class KernelContext(KernelBaseModel, Generic[SemanticTextMemoryT]): memory: SemanticTextMemoryT variables: ContextVariables # This field can be used to hold anything that is not a string - plugin_collection: ReadOnlyPluginCollection = Field(default_factory=ReadOnlyPluginCollection) + plugins: KernelPluginCollection = Field(default_factory=KernelPluginCollection) _objects: Dict[str, Any] = PrivateAttr(default_factory=dict) _error_occurred: bool = PrivateAttr(False) _last_exception: Optional[Exception] = PrivateAttr(None) @@ -38,7 +32,7 @@ def __init__( self, variables: ContextVariables, memory: SemanticTextMemoryBase, - plugin_collection: Union[ReadOnlyPluginCollection, None], + plugins: Union[KernelPluginCollection, None], **kwargs, # TODO: cancellation token? ) -> None: @@ -48,15 +42,15 @@ def __init__( Arguments: variables {ContextVariables} -- The context variables. memory {SemanticTextMemoryBase} -- The semantic text memory. - plugin_collection {ReadOnlyPluginCollectionBase} -- The plugin collection. + plugins {KernelPluginCollection} -- The kernel plugin collection. """ if kwargs.get("logger"): logger.warning("The `logger` parameter is deprecated. Please use the `logging` module instead.") - if plugin_collection is None: - plugin_collection = ReadOnlyPluginCollection() + if plugins is None: + plugins = KernelPluginCollection() - super().__init__(variables=variables, memory=memory, plugin_collection=plugin_collection) + super().__init__(variables=variables, memory=memory, plugins=plugins) def fail(self, error_description: str, exception: Optional[Exception] = None): """ @@ -125,23 +119,6 @@ def objects(self) -> Dict[str, Any]: """ return self._objects - @property - def plugins(self) -> ReadOnlyPluginCollectionBase: - """ - Read only plugins collection. - - Returns: - ReadOnlyPluginCollectionBase -- The plugins collection. - """ - return self.plugin_collection - - @plugins.setter - def plugins(self, value: ReadOnlyPluginCollectionBase) -> None: - """ - Set the value of plugins collection - """ - self.plugin_collection = value - def __setitem__(self, key: str, value: Any) -> None: """ Sets a context variable. @@ -177,58 +154,17 @@ def func(self, plugin_name: str, function_name: str): Returns: KernelFunctionBase -- The function. """ - if self.plugin_collection is None: + if self.plugins is None: raise ValueError("The plugin collection hasn't been set") - assert self.plugin_collection is not None # for type checker + assert self.plugins is not None # for type checker - if self.plugin_collection.has_native_function(plugin_name, function_name): - return self.plugin_collection.get_native_function(plugin_name, function_name) + if self.plugins[plugin_name][function_name].is_native: + return self.plugins.get_native_function(plugin_name, function_name) - return self.plugin_collection.get_semantic_function(plugin_name, function_name) + return self.plugins[plugin_name][function_name] def __str__(self) -> str: if self._error_occurred: return f"Error: {self._last_error_description}" return self.result - - def throw_if_plugin_collection_not_set(self) -> None: - """ - Throws an exception if the plugin collection hasn't been set. - """ - if self.plugin_collection is None: - raise KernelException( - KernelException.ErrorCodes.PluginCollectionNotSet, - "Plugin collection not found in the context", - ) - - def is_function_registered( - self, plugin_name: str, function_name: str - ) -> Union[Tuple[Literal[True], Any], Tuple[Literal[False], None]]: - """ - Checks whether a function is registered in this context. - - Arguments: - plugin_name {str} -- The plugin name. - function_name {str} -- The function name. - - Returns: - Tuple[bool, KernelFunctionBase] -- A tuple with a boolean indicating - whether the function is registered and the function itself (or None). - """ - self.throw_if_plugin_collection_not_set() - assert self.plugin_collection is not None # for type checker - - if self.plugin_collection.has_native_function(plugin_name, function_name): - the_func = self.plugin_collection.get_native_function(plugin_name, function_name) - return True, the_func - - if self.plugin_collection.has_native_function(None, function_name): - the_func = self.plugin_collection.get_native_function(None, function_name) - return True, the_func - - if self.plugin_collection.has_semantic_function(plugin_name, function_name): - the_func = self.plugin_collection.get_semantic_function(plugin_name, function_name) - return True, the_func - - return False, None diff --git a/python/semantic_kernel/orchestration/kernel_function.py b/python/semantic_kernel/orchestration/kernel_function.py index 45762e9635e5..8c2ec78ea0ff 100644 --- a/python/semantic_kernel/orchestration/kernel_function.py +++ b/python/semantic_kernel/orchestration/kernel_function.py @@ -25,9 +25,6 @@ from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase from semantic_kernel.plugin_definition.function_view import FunctionView from semantic_kernel.plugin_definition.parameter_view import ParameterView -from semantic_kernel.plugin_definition.read_only_plugin_collection_base import ( - ReadOnlyPluginCollectionBase, -) from semantic_kernel.semantic_functions.chat_prompt_template import ChatPromptTemplate from semantic_kernel.semantic_functions.semantic_function_config import ( SemanticFunctionConfig, @@ -35,6 +32,7 @@ if TYPE_CHECKING: from semantic_kernel.orchestration.kernel_context import KernelContext + from semantic_kernel.plugin_definition.kernel_plugin_collection import KernelPluginCollection # TODO: is this needed anymore after sync code removal? if platform.system() == "Windows" and sys.version_info >= (3, 8, 0): @@ -65,7 +63,7 @@ class KernelFunction(KernelFunctionBase): _parameters: List[ParameterView] _delegate_type: DelegateTypes _function: Callable[..., Any] - _plugin_collection: Optional[ReadOnlyPluginCollectionBase] + _plugin_collection: Optional["KernelPluginCollection"] _ai_service: Optional[Union[TextCompletionClientBase, ChatCompletionClientBase]] _ai_prompt_execution_settings: PromptExecutionSettings _chat_prompt_template: ChatPromptTemplate @@ -266,7 +264,7 @@ def __init__( self._ai_prompt_execution_settings = PromptExecutionSettings() self._chat_prompt_template = kwargs.get("chat_prompt_template", None) - def set_default_plugin_collection(self, plugins: ReadOnlyPluginCollectionBase) -> "KernelFunction": + def set_default_plugin_collection(self, plugins: "KernelPluginCollection") -> "KernelFunction": self._plugin_collection = plugins return self @@ -375,8 +373,8 @@ async def invoke( if context is None: context = KernelContext( variables=ContextVariables("") if variables is None else variables, - plugin_collection=self._plugin_collection, memory=memory if memory is not None else NullMemory.instance, + plugins=self._plugin_collection, ) else: # If context is passed, we need to merge the variables @@ -450,8 +448,8 @@ async def invoke_stream( if context is None: context = KernelContext( variables=ContextVariables("") if variables is None else variables, - plugin_collection=self._plugin_collection, memory=memory if memory is not None else NullMemory.instance, + plugins=self._plugin_collection, ) else: # If context is passed, we need to merge the variables diff --git a/python/semantic_kernel/orchestration/kernel_function_base.py b/python/semantic_kernel/orchestration/kernel_function_base.py index f9f828dc33a0..c3db619d6a1e 100644 --- a/python/semantic_kernel/orchestration/kernel_function_base.py +++ b/python/semantic_kernel/orchestration/kernel_function_base.py @@ -14,9 +14,6 @@ if TYPE_CHECKING: from semantic_kernel.orchestration.kernel_context import KernelContext - from semantic_kernel.plugin_definition.read_only_plugin_collection_base import ( - ReadOnlyPluginCollectionBase, - ) class KernelFunctionBase(KernelBaseModel): @@ -116,24 +113,6 @@ async def invoke( """ pass - @abstractmethod - def set_default_plugin_collection( - self, - plugins: "ReadOnlyPluginCollectionBase", - ) -> "KernelFunctionBase": - """ - Sets the plugin collection to use when the function is - invoked without a context or with a context that doesn't have - a plugin collection - - Arguments: - plugins {ReadOnlyPluginCollectionBase} -- Kernel's plugin collection - - Returns: - KernelFunctionBase -- The function instance - """ - pass - @abstractmethod def set_ai_service(self, service_factory: Callable[[], TextCompletionClientBase]) -> "KernelFunctionBase": """ diff --git a/python/semantic_kernel/planning/action_planner/action_planner.py b/python/semantic_kernel/planning/action_planner/action_planner.py index 9d82cd26cf4a..5805644721a5 100644 --- a/python/semantic_kernel/planning/action_planner/action_planner.py +++ b/python/semantic_kernel/planning/action_planner/action_planner.py @@ -140,14 +140,15 @@ async def create_plan(self, goal: str) -> Plan: plan = Plan(description=goal) elif "." in generated_plan["plan"]["function"]: plugin, fun = generated_plan["plan"]["function"].split(".") - function_ref = self._context.plugins.get_function(plugin, fun) + function_ref = self._context.plugins[plugin][fun] logger.info( f"ActionPlanner has picked {plugin}.{fun}. Reference to this function" f" found in context: {function_ref}" ) plan = Plan(description=goal, function=function_ref) else: - function_ref = self._context.plugins.get_function(generated_plan["plan"]["function"]) + plugin, func = generated_plan["plan"]["function"] + function_ref = self._context.plugins[plugin][func] logger.info( f"ActionPlanner has picked {generated_plan['plan']['function']}. " " Reference to this function found in context:" diff --git a/python/semantic_kernel/planning/basic_planner.py b/python/semantic_kernel/planning/basic_planner.py index c82bf3bcce84..c33683b9ff18 100644 --- a/python/semantic_kernel/planning/basic_planner.py +++ b/python/semantic_kernel/planning/basic_planner.py @@ -212,7 +212,7 @@ async def execute_plan(self, plan: Plan, kernel: Kernel) -> str: for subtask in subtasks: plugin_name, function_name = subtask["function"].split(".") - kernel_function = kernel.plugins.get_function(plugin_name, function_name) + kernel_function = kernel.plugins[plugin_name][function_name] # Get the arguments dictionary for the function args = subtask.get("args", None) diff --git a/python/semantic_kernel/planning/plan.py b/python/semantic_kernel/planning/plan.py index a0dcb0bda582..b7a04efa7150 100644 --- a/python/semantic_kernel/planning/plan.py +++ b/python/semantic_kernel/planning/plan.py @@ -19,11 +19,8 @@ from semantic_kernel.orchestration.kernel_context import KernelContext from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase from semantic_kernel.plugin_definition.function_view import FunctionView -from semantic_kernel.plugin_definition.read_only_plugin_collection import ( - ReadOnlyPluginCollection, -) -from semantic_kernel.plugin_definition.read_only_plugin_collection_base import ( - ReadOnlyPluginCollectionBase, +from semantic_kernel.plugin_definition.kernel_plugin_collection import ( + KernelPluginCollection, ) logger: logging.Logger = logging.getLogger(__name__) @@ -160,8 +157,8 @@ async def invoke( if context is None: context = KernelContext( variables=self._state, - plugin_collection=ReadOnlyPluginCollection(), memory=memory or NullMemory(), + plugins=KernelPluginCollection(), ) if self._function is not None: @@ -195,13 +192,6 @@ def set_ai_service(self, service: Callable[[], TextCompletionClientBase]) -> Ker if self._function is not None: self._function.set_ai_service(service) - def set_default_plugin_collection( - self, - plugins: ReadOnlyPluginCollectionBase, - ) -> KernelFunctionBase: - if self._function is not None: - self._function.set_default_plugin_collection(plugins) - def describe(self) -> Optional[FunctionView]: if self._function is not None: return self._function.describe() @@ -215,7 +205,7 @@ def set_available_functions(self, plan: "Plan", context: KernelContext) -> "Plan "Plugin collection not found in the context", ) try: - pluginFunction = context.plugins.get_function(plan.plugin_name, plan.name) + pluginFunction = context.plugins[plan.plugin_name][plan.name] plan.set_function(pluginFunction) except Exception: pass @@ -270,7 +260,7 @@ async def invoke_next_step(self, context: KernelContext) -> "Plan": func_context = KernelContext( variables=variables, memory=context.memory, - plugin_collection=context.plugins, + plugins=context.plugins, ) result = await step.invoke(context=func_context) result_value = result.result diff --git a/python/semantic_kernel/planning/sequential_planner/sequential_planner_parser.py b/python/semantic_kernel/planning/sequential_planner/sequential_planner_parser.py index 072cd9e976a8..9088519bb986 100644 --- a/python/semantic_kernel/planning/sequential_planner/sequential_planner_parser.py +++ b/python/semantic_kernel/planning/sequential_planner/sequential_planner_parser.py @@ -26,7 +26,9 @@ def get_plugin_function( ) -> Callable[[str, str], Optional[KernelFunctionBase]]: def function(plugin_name: str, function_name: str) -> Optional[KernelFunctionBase]: try: - return context.plugins.get_function(plugin_name, function_name) + return context.plugins[plugin_name][function_name] + except KeyError: + return None except KernelException: return None diff --git a/python/semantic_kernel/plugin_definition/__init__.py b/python/semantic_kernel/plugin_definition/__init__.py index f7408f8552bb..7da6654d819a 100644 --- a/python/semantic_kernel/plugin_definition/__init__.py +++ b/python/semantic_kernel/plugin_definition/__init__.py @@ -1,4 +1,5 @@ # Copyright (c) Microsoft. All rights reserved. + from semantic_kernel.plugin_definition.kernel_function_context_parameter_decorator import ( kernel_function_context_parameter, ) diff --git a/python/semantic_kernel/plugin_definition/constants.py b/python/semantic_kernel/plugin_definition/constants.py deleted file mode 100644 index cdb1b6cb1388..000000000000 --- a/python/semantic_kernel/plugin_definition/constants.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. -import typing as t - -GLOBAL_PLUGIN: t.Final[str] = "_GLOBAL_FUNCTIONS_" diff --git a/python/semantic_kernel/plugin_definition/function_view.py b/python/semantic_kernel/plugin_definition/function_view.py index 3b697397b8fd..7274711901d7 100644 --- a/python/semantic_kernel/plugin_definition/function_view.py +++ b/python/semantic_kernel/plugin_definition/function_view.py @@ -33,3 +33,25 @@ def __init__( is_semantic=is_semantic, is_asynchronous=is_asynchronous, ) + + def __eq__(self, other): + """ + Compare to another FunctionView instance. + + Args: + other (FunctionView): The other FunctionView instance. + + Returns: + True if the two instances are equal, False otherwise. + """ + if not isinstance(other, FunctionView): + 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_semantic == other.is_semantic + and self.is_asynchronous == other.is_asynchronous + ) diff --git a/python/semantic_kernel/plugin_definition/kernel_plugin.py b/python/semantic_kernel/plugin_definition/kernel_plugin.py new file mode 100644 index 000000000000..bceeea1da3f4 --- /dev/null +++ b/python/semantic_kernel/plugin_definition/kernel_plugin.py @@ -0,0 +1,109 @@ +# Copyright (c) Microsoft. All rights reserved. + +import sys +from typing import Dict, List, Optional + +if sys.version_info >= (3, 9): + from typing import Annotated +else: + from typing_extensions import Annotated + +from pydantic import Field, StringConstraints + +from semantic_kernel.kernel_pydantic import KernelBaseModel +from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase + + +class KernelPlugin(KernelBaseModel): + """ + Represents a Kernel Plugin with functions. + + 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, KernelFunctionBase]): The functions in the plugin, + indexed by their name. + """ + + name: Annotated[str, StringConstraints(pattern=r"^[A-Za-z_]+$", min_length=1)] + description: Optional[str] = Field(default=None) + functions: Optional[Dict[str, "KernelFunctionBase"]] = Field(default_factory=dict) + + def __init__( + self, name: str, description: Optional[str] = None, functions: Optional[List[KernelFunctionBase]] = None + ): + """ + Initialize a new instance of the KernelPlugin class + + Args: + name (str): The name of the plugin. + description (Optional[str]): The description of the plugin. + functions (List[KernelFunctionBase]): The functions in the plugin. + + Raises: + ValueError: If the functions list contains duplicate function names. + """ + functions_dict = {} + if functions is not None: + for function in functions: + if function.name in functions_dict: + raise ValueError(f"Duplicate function name detected: {function.name}") + functions_dict[function.name] = function + super().__init__(name=name, description=description, functions=functions_dict) + + def __len__(self) -> int: + """ + Gets the number of functions in the plugin. + + Returns: + The number of functions in the plugin. + + """ + return len(self.functions) + + def __contains__(self, function_name: str) -> bool: + """ + Checks if the plugin contains a function with the specified name. + + Args: + function_name (str): The name of the function. + + Returns: + True if the plugin contains a function with the specified name, False otherwise. + """ + return function_name in self.functions.keys() + + def __getitem__(self, name: str) -> "KernelFunctionBase": + """Define the [] operator for the plugin + + Args: + name (str): The name of the function to retrieve. + + Returns: + The function if it exists, None otherwise. + + Raises: + KeyError: If the function does not exist. + """ + if name not in self.functions: + raise KeyError(f"Function {name} not found.") + return self.functions[name] + + @classmethod + def from_functions( + cls, functions: List["KernelFunctionBase"], plugin_name: str, description: Optional[str] = None + ) -> "KernelPlugin": + """ + Creates a KernelPlugin from a KernelFunctionBase instance. + + Args: + functions (List[KernelFunctionBase]): The functions to create the plugin from. + plugin_name (Optional[str]): The name of the plugin. If not specified, + the name of the function will be used. + description (Optional[str]): The description of the plugin. + + Returns: + A KernelPlugin instance. + """ + return cls(name=plugin_name, description=description, functions=functions) diff --git a/python/semantic_kernel/plugin_definition/kernel_plugin_collection.py b/python/semantic_kernel/plugin_definition/kernel_plugin_collection.py new file mode 100644 index 000000000000..d93513390066 --- /dev/null +++ b/python/semantic_kernel/plugin_definition/kernel_plugin_collection.py @@ -0,0 +1,229 @@ +# Copyright (c) Microsoft. All rights reserved. + +from typing import Any, Dict, Iterable, List, Optional, TypeVar, Union + +from pydantic import Field + +from semantic_kernel.kernel_pydantic import KernelBaseModel +from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase +from semantic_kernel.plugin_definition.functions_view import FunctionsView +from semantic_kernel.plugin_definition.kernel_plugin import KernelPlugin + +# To support Python 3.8, need to use TypeVar since Iterable is not scriptable +KernelPluginType = TypeVar("KernelPluginType", bound=KernelPlugin) + + +class KernelPluginCollection(KernelBaseModel): + """ + The Kernel Plugin Collection class. This class is used to store a collection of plugins. + + Attributes: + plugins (Dict[str, KernelPlugin]): The plugins in the collection, indexed by their name. + """ + + plugins: Optional[Dict[str, KernelPlugin]] = Field(default_factory=dict) + + def __init__(self, plugins: Union[None, "KernelPluginCollection", Iterable[KernelPluginType]] = None): + """ + Initialize a new instance of the KernelPluginCollection class + + Args: + plugins (Union[None, KernelPluginCollection, Iterable[KernelPlugin]]): The plugins to add + to the collection. If None, an empty collection is created. If a KernelPluginCollection, + the plugins are copied from the other collection. If an iterable of KernelPlugin, + the plugins are added to the collection. + + Raises: + ValueError: If the plugins is not None, a KernelPluginCollection, or an iterable of KernelPlugin. + """ + if plugins is None: + plugins = {} + elif isinstance(plugins, KernelPluginCollection): + # Extract plugins from another KernelPluginCollection instance + plugins = {plugin.name: plugin for plugin in plugins.plugins.values()} + elif isinstance(plugins, Iterable): + # Process an iterable of plugins + plugins = self._process_plugins_iterable(plugins) + else: + raise ValueError("Invalid type for plugins") + + super().__init__(plugins=plugins) + + @staticmethod + def _process_plugins_iterable(plugins_input: Iterable[KernelPlugin]) -> Dict[str, KernelPlugin]: + plugins_dict = {} + for plugin in plugins_input: + if plugin is None: + raise ValueError("Plugin and plugin.name must not be None") + if plugin.name in plugins_dict: + raise ValueError(f"Duplicate plugin name detected: {plugin.name}") + plugins_dict[plugin.name] = plugin + return plugins_dict + + def add(self, plugin: KernelPlugin) -> None: + """ + Add a single plugin to the collection + + Args: + plugin (KernelPlugin): The plugin to add to the collection. + + Raises: + ValueError: If the plugin or plugin.name is None. + """ + if plugin is None: + raise ValueError("Plugin must not be None") + if plugin.name in self.plugins: + raise ValueError(f"Plugin with name {plugin.name} already exists") + self.plugins[plugin.name] = plugin + + def add_plugin_from_functions(self, plugin_name: str, functions: List["KernelFunctionBase"]) -> None: + """ + Add a function to a new plugin in the collection + + Args: + plugin_name (str): The name of the plugin to create. + functions (List[KernelFunctionBase]): The functions to add to the plugin. + + Raises: + ValueError: If the function or plugin_name is None or invalid. + """ + if not functions or not plugin_name: + raise ValueError("Functions or plugin_name must not be None or empty") + if plugin_name in self.plugins: + raise ValueError(f"Plugin with name {plugin_name} already exists") + + plugin = KernelPlugin.from_functions(plugin_name=plugin_name, functions=functions) + self.plugins[plugin_name] = plugin + + def add_functions_to_plugin(self, functions: List["KernelFunctionBase"], plugin_name: str) -> None: + """ + Add functions to a plugin in the collection + + Args: + functions (List[KernelFunctionBase]): The function to add to the plugin. + plugin_name (str): The name of the plugin to add the function to. + + Raises: + ValueError: If the functions or plugin_name is None or invalid. + ValueError: if the function already exists in the plugin. + """ + if not functions or not plugin_name: + raise ValueError("Functions and plugin_name must not be None or empty") + + if plugin_name not in self.plugins: + self.plugins.add(KernelPlugin(name=plugin_name, functions=functions)) + return + + plugin = self.plugins[plugin_name] + for func in functions: + if func.name in plugin.functions: + raise ValueError(f"Function with name '{func.name}' already exists in plugin '{plugin_name}'") + plugin.functions[func.name] = func + + def add_list_of_plugins(self, plugins: List[KernelPlugin]) -> None: + """ + Add a list of plugins to the collection + + Args: + plugins (List[KernelPlugin]): The plugins to add to the collection. + + Raises: + ValueError: If the plugins list is None. + """ + + if plugins is None: + raise ValueError("Plugins must not be None") + for plugin in plugins: + self.add(plugin) + + def remove(self, plugin: KernelPlugin) -> bool: + """ + Remove a plugin from the collection + + Args: + plugin (KernelPlugin): The plugin to remove from the collection. + + Returns: + True if the plugin was removed, False otherwise. + """ + if plugin is None or plugin.name is None: + return False + return self.plugins.pop(plugin.name, None) is not None + + def remove_by_name(self, plugin_name: str) -> bool: + """ + Remove a plugin from the collection by name + + Args: + plugin_name (str): The name of the plugin to remove from the collection. + + Returns: + True if the plugin was removed, False otherwise. + """ + if plugin_name is None: + return False + return self.plugins.pop(plugin_name, None) is not None + + def __getitem__(self, name): + """Define the [] operator for the collection + + Args: + name (str): The name of the plugin to retrieve. + + Returns: + The plugin if it exists, None otherwise. + + Raises: + KeyError: If the plugin does not exist. + """ + if name not in self.plugins: + raise KeyError(f"Plugin {name} not found.") + return self.plugins[name] + + def clear(self): + """Clear the collection of all plugins""" + self.plugins.clear() + + def get_functions_view(self, include_semantic: bool = True, include_native: bool = True) -> FunctionsView: + """ + Get a view of the functions in the collection + + Args: + include_semantic (bool): Whether to include semantic functions in the view. + include_native (bool): Whether to include native functions in the view. + + Returns: + A view of the functions in the collection. + """ + result = FunctionsView() + + for _, plugin in self.plugins.items(): + for _, function in plugin.functions.items(): + if include_semantic and function.is_semantic: + result.add_function(function.describe()) + elif include_native and function.is_native: + result.add_function(function.describe()) + + return result + + def __iter__(self) -> Any: + """Define an iterator for the collection""" + return iter(self.plugins.values()) + + def __len__(self) -> int: + """Define the length of the collection""" + return len(self.plugins) + + def __contains__(self, plugin_name: str) -> bool: + """ + Check if the collection contains a plugin + + Args: + plugin_name (str): The name of the plugin to check for. + + Returns: + True if the collection contains the plugin, False otherwise. + """ + if not plugin_name: + return False + return self.plugins.get(plugin_name) is not None diff --git a/python/semantic_kernel/plugin_definition/plugin_collection.py b/python/semantic_kernel/plugin_definition/plugin_collection.py deleted file mode 100644 index 0823822093c8..000000000000 --- a/python/semantic_kernel/plugin_definition/plugin_collection.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import logging -from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional, Union - -from pydantic import Field - -from semantic_kernel.orchestration.kernel_function import KernelFunction -from semantic_kernel.plugin_definition import constants -from semantic_kernel.plugin_definition.functions_view import FunctionsView -from semantic_kernel.plugin_definition.plugin_collection_base import ( - PluginCollectionBase, -) -from semantic_kernel.plugin_definition.read_only_plugin_collection import ( - ReadOnlyPluginCollection, -) -from semantic_kernel.plugin_definition.read_only_plugin_collection_base import ( - ReadOnlyPluginCollectionBase, -) - -if TYPE_CHECKING: - from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase - -logger: logging.Logger = logging.getLogger(__name__) - - -class PluginCollection(PluginCollectionBase): - GLOBAL_PLUGIN: ClassVar[str] = constants.GLOBAL_PLUGIN - read_only_plugin_collection_: ReadOnlyPluginCollection = Field(alias="read_only_plugin_collection") - - def __init__( - self, - log: Optional[Any] = None, - plugin_collection: Union[Dict[str, Dict[str, KernelFunction]], None] = None, - read_only_plugin_collection_: Optional[ReadOnlyPluginCollection] = None, - ) -> None: - if log: - logger.warning("The `log` parameter is deprecated. Please use the `logging` module instead.") - if plugin_collection and read_only_plugin_collection_: - raise ValueError("Only one of `plugin_collection` and `read_only_plugin_collection` can be" " provided") - elif not plugin_collection and not read_only_plugin_collection_: - read_only_plugin_collection = ReadOnlyPluginCollection({}) - elif not read_only_plugin_collection_: - read_only_plugin_collection = ReadOnlyPluginCollection(plugin_collection) - else: - read_only_plugin_collection = read_only_plugin_collection_ - super().__init__(read_only_plugin_collection=read_only_plugin_collection) - - @property - def read_only_plugin_collection(self) -> ReadOnlyPluginCollectionBase: - return self.read_only_plugin_collection_ - - @property - def plugin_collection(self): - return self.read_only_plugin_collection_.data - - def add_semantic_function(self, function: "KernelFunctionBase") -> None: - if function is None: - raise ValueError("The function provided cannot be `None`") - - s_name, f_name = function.plugin_name, function.name - s_name, f_name = s_name.lower(), f_name.lower() - - self.plugin_collection.setdefault(s_name, {})[f_name] = function - - def add_native_function(self, function: "KernelFunctionBase") -> None: - if function is None: - raise ValueError("The function provided cannot be `None`") - - s_name, f_name = function.plugin_name, function.name - s_name, f_name = self.read_only_plugin_collection_._normalize_names(s_name, f_name, True) - - self.plugin_collection.setdefault(s_name, {})[f_name] = function - - def has_function(self, plugin_name: Optional[str], function_name: str) -> bool: - return self.read_only_plugin_collection_.has_function(plugin_name, function_name) - - def has_semantic_function(self, plugin_name: Optional[str], function_name: str) -> bool: - return self.read_only_plugin_collection_.has_semantic_function(plugin_name, function_name) - - def has_native_function(self, plugin_name: Optional[str], function_name: str) -> bool: - return self.read_only_plugin_collection_.has_native_function(plugin_name, function_name) - - def get_semantic_function(self, plugin_name: Optional[str], function_name: str) -> "KernelFunctionBase": - return self.read_only_plugin_collection_.get_semantic_function(plugin_name, function_name) - - def get_native_function(self, plugin_name: Optional[str], function_name: str) -> "KernelFunctionBase": - return self.read_only_plugin_collection_.get_native_function(plugin_name, function_name) - - def get_functions_view(self, include_semantic: bool = True, include_native: bool = True) -> FunctionsView: - return self.read_only_plugin_collection_.get_functions_view(include_semantic, include_native) - - def get_function(self, plugin_name: Optional[str], function_name: str) -> "KernelFunctionBase": - return self.read_only_plugin_collection_.get_function(plugin_name, function_name) diff --git a/python/semantic_kernel/plugin_definition/plugin_collection_base.py b/python/semantic_kernel/plugin_definition/plugin_collection_base.py deleted file mode 100644 index af4621f56f16..000000000000 --- a/python/semantic_kernel/plugin_definition/plugin_collection_base.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, TypeVar - -from semantic_kernel.plugin_definition.read_only_plugin_collection_base import ( - ReadOnlyPluginCollectionBase, -) - -if TYPE_CHECKING: - from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase - - -PluginCollectionT = TypeVar("PluginCollectionT", bound="PluginCollectionBase") - - -class PluginCollectionBase(ReadOnlyPluginCollectionBase, ABC): - @property - @abstractmethod - def read_only_plugin_collection(self) -> ReadOnlyPluginCollectionBase: - pass - - @abstractmethod - def add_semantic_function(self, semantic_function: "KernelFunctionBase") -> "PluginCollectionBase": - pass - - @abstractmethod - def add_native_function(self, native_function: "KernelFunctionBase") -> "PluginCollectionBase": - pass diff --git a/python/semantic_kernel/plugin_definition/read_only_plugin_collection.py b/python/semantic_kernel/plugin_definition/read_only_plugin_collection.py deleted file mode 100644 index 00d08062b987..000000000000 --- a/python/semantic_kernel/plugin_definition/read_only_plugin_collection.py +++ /dev/null @@ -1,118 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import logging -from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional, Tuple - -from pydantic import ConfigDict, Field - -from semantic_kernel.kernel_exception import KernelException -from semantic_kernel.orchestration.kernel_function import KernelFunction -from semantic_kernel.plugin_definition import constants -from semantic_kernel.plugin_definition.functions_view import FunctionsView -from semantic_kernel.plugin_definition.read_only_plugin_collection_base import ( - ReadOnlyPluginCollectionBase, -) - -if TYPE_CHECKING: - from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase - -logger: logging.Logger = logging.getLogger(__name__) - - -class ReadOnlyPluginCollection(ReadOnlyPluginCollectionBase): - GLOBAL_PLUGIN: ClassVar[str] = constants.GLOBAL_PLUGIN - data: Dict[str, Dict[str, KernelFunction]] = Field(default_factory=dict) - model_config = ConfigDict(frozen=False) - - def __init__( - self, - data: Dict[str, Dict[str, KernelFunction]] = None, - log: Optional[Any] = None, - ) -> None: - super().__init__(data=data or {}) - - if log: - logger.warning("The `log` parameter is deprecated. Please use the `logging` module instead.") - - def has_function(self, plugin_name: Optional[str], function_name: str) -> bool: - s_name, f_name = self._normalize_names(plugin_name, function_name, True) - if s_name not in self.data: - return False - return f_name in self.data[s_name] - - def has_semantic_function(self, plugin_name: str, function_name: str) -> bool: - s_name, f_name = self._normalize_names(plugin_name, function_name) - if s_name not in self.data: - return False - if f_name not in self.data[s_name]: - return False - return self.data[s_name][f_name].is_semantic - - def has_native_function(self, plugin_name: str, function_name: str) -> bool: - s_name, f_name = self._normalize_names(plugin_name, function_name, True) - if s_name not in self.data: - return False - if f_name not in self.data[s_name]: - return False - return self.data[s_name][f_name].is_native - - def get_semantic_function(self, plugin_name: str, function_name: str) -> "KernelFunctionBase": - s_name, f_name = self._normalize_names(plugin_name, function_name) - if self.has_semantic_function(s_name, f_name): - return self.data[s_name][f_name] - - logger.error(f"Function not available: {s_name}.{f_name}") - raise KernelException( - KernelException.ErrorCodes.FunctionNotAvailable, - f"Function not available: {s_name}.{f_name}", - ) - - def get_native_function(self, plugin_name: str, function_name: str) -> "KernelFunctionBase": - s_name, f_name = self._normalize_names(plugin_name, function_name, True) - if self.has_native_function(s_name, f_name): - return self.data[s_name][f_name] - - logger.error(f"Function not available: {s_name}.{f_name}") - raise KernelException( - KernelException.ErrorCodes.FunctionNotAvailable, - f"Function not available: {s_name}.{f_name}", - ) - - def get_functions_view(self, include_semantic: bool = True, include_native: bool = True) -> FunctionsView: - result = FunctionsView() - - for plugin in self.data.values(): - for function in plugin.values(): - if include_semantic and function.is_semantic: - result.add_function(function.describe()) - elif include_native and function.is_native: - result.add_function(function.describe()) - - return result - - def get_function(self, plugin_name: Optional[str], function_name: str) -> "KernelFunctionBase": - s_name, f_name = self._normalize_names(plugin_name, function_name, True) - if self.has_function(s_name, f_name): - return self.data[s_name][f_name] - - logger.error(f"Function not available: {s_name}.{f_name}") - raise KernelException( - KernelException.ErrorCodes.FunctionNotAvailable, - f"Function not available: {s_name}.{f_name}", - ) - - def _normalize_names( - self, - plugin_name: Optional[str], - function_name: str, - allow_substitution: bool = False, - ) -> Tuple[str, str]: - s_name, f_name = plugin_name, function_name - if s_name is None and allow_substitution: - s_name = self.GLOBAL_PLUGIN - - if s_name is None: - raise ValueError("The plugin name provided cannot be `None`") - - s_name, f_name = s_name.lower(), f_name.lower() - return s_name, f_name diff --git a/python/semantic_kernel/plugin_definition/read_only_plugin_collection_base.py b/python/semantic_kernel/plugin_definition/read_only_plugin_collection_base.py deleted file mode 100644 index 101f80ad2816..000000000000 --- a/python/semantic_kernel/plugin_definition/read_only_plugin_collection_base.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Optional - -from semantic_kernel.kernel_pydantic import KernelBaseModel - -if TYPE_CHECKING: - from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase - from semantic_kernel.plugin_definition.functions_view import FunctionsView - - -class ReadOnlyPluginCollectionBase(KernelBaseModel, ABC): - @abstractmethod - def has_function(self, plugin_name: Optional[str], function_name: str) -> bool: - pass - - @abstractmethod - def has_semantic_function(self, plugin_name: Optional[str], function_name: str) -> bool: - pass - - @abstractmethod - def has_native_function(self, plugin_name: Optional[str], function_name: str) -> bool: - pass - - @abstractmethod - def get_semantic_function(self, plugin_name: Optional[str], function_name: str) -> "KernelFunctionBase": - pass - - @abstractmethod - def get_native_function(self, plugin_name: Optional[str], function_name: str) -> "KernelFunctionBase": - pass - - @abstractmethod - def get_functions_view(self, include_semantic: bool = True, include_native: bool = True) -> "FunctionsView": - pass - - @abstractmethod - def get_function(self, plugin_name: Optional[str], function_name: str) -> "KernelFunctionBase": - pass diff --git a/python/semantic_kernel/template_engine/blocks/code_block.py b/python/semantic_kernel/template_engine/blocks/code_block.py index 8eb53a4d1e86..1f0b7f94707e 100644 --- a/python/semantic_kernel/template_engine/blocks/code_block.py +++ b/python/semantic_kernel/template_engine/blocks/code_block.py @@ -6,9 +6,7 @@ import pydantic as pdt from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase -from semantic_kernel.plugin_definition.read_only_plugin_collection_base import ( - ReadOnlyPluginCollectionBase, -) +from semantic_kernel.plugin_definition.kernel_plugin_collection import KernelPluginCollection from semantic_kernel.template_engine.blocks.block import Block from semantic_kernel.template_engine.blocks.block_types import BlockTypes from semantic_kernel.template_engine.blocks.function_id_block import FunctionIdBlock @@ -116,12 +114,24 @@ async def _render_function_call(self, f_block: FunctionIdBlock, context): return result.result def _get_function_from_plugin_collection( - self, plugins: ReadOnlyPluginCollectionBase, f_block: FunctionIdBlock + self, plugins: KernelPluginCollection, f_block: FunctionIdBlock ) -> Optional[KernelFunctionBase]: - if not f_block.plugin_name and plugins.has_function(None, f_block.function_name): - return plugins.get_function(None, f_block.function_name) - - if f_block.plugin_name and plugins.has_function(f_block.plugin_name, f_block.function_name): - return plugins.get_function(f_block.plugin_name, f_block.function_name) + """ + Get the function from the plugin collection + + Args: + plugins: The plugin collection + f_block: The function block that contains the function name + + Returns: + The function if it exists, None otherwise. + """ + if f_block.plugin_name is not None and len(f_block.plugin_name) > 0: + return plugins[f_block.plugin_name][f_block.function_name] + else: + # We now require a plug-in name, but if one isn't set then we'll try to find the function + for plugin in plugins: + if f_block.function_name in plugin: + return plugin[f_block.function_name] return None diff --git a/python/semantic_kernel/utils/naming.py b/python/semantic_kernel/utils/naming.py new file mode 100644 index 000000000000..2ed869392d16 --- /dev/null +++ b/python/semantic_kernel/utils/naming.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft. All rights reserved. + +import random +import string + + +def generate_random_ascii_name(length: int = 16) -> str: + """ + Generate a series of random ASCII characters of the specified length. + As example, plugin/function names can contain upper/lowercase letters, and underscores + + Args: + length (int): The length of the string to generate. + + Returns: + A string of random ASCII characters of the specified length. + """ + letters = string.ascii_letters + return "".join(random.choices(letters, k=length)) diff --git a/python/tests/conftest.py b/python/tests/conftest.py index b5914a2af5a5..395b047b47fa 100644 --- a/python/tests/conftest.py +++ b/python/tests/conftest.py @@ -1,5 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. +from __future__ import annotations + import os import typing as t import warnings @@ -11,9 +13,8 @@ from semantic_kernel.orchestration.context_variables import ContextVariables from semantic_kernel.orchestration.kernel_context import KernelContext from semantic_kernel.orchestration.kernel_function import KernelFunction -from semantic_kernel.plugin_definition.read_only_plugin_collection import ( - ReadOnlyPluginCollection, -) +from semantic_kernel.plugin_definition.kernel_plugin import KernelPlugin +from semantic_kernel.plugin_definition.kernel_plugin_collection import KernelPluginCollection @pytest.fixture(autouse=True) @@ -92,11 +93,14 @@ def context_factory() -> t.Callable[[ContextVariables], KernelContext]: def create_context(context_variables: ContextVariables, *functions: KernelFunction) -> KernelContext: """Return a KernelContext object.""" + + plugin = KernelPlugin(name="test_plugin", functions=functions) + return KernelContext( context_variables, NullMemory(), - plugin_collection=ReadOnlyPluginCollection( - data={ReadOnlyPluginCollection.GLOBAL_PLUGIN.lower(): {f.name: f for f in functions}}, + plugins=KernelPluginCollection( + plugins=[plugin], ), ) diff --git a/python/tests/integration/completions/test_conversation_summary_plugin.py b/python/tests/integration/completions/test_conversation_summary_plugin.py index 488cd13317fd..ac5b055f51ba 100644 --- a/python/tests/integration/completions/test_conversation_summary_plugin.py +++ b/python/tests/integration/completions/test_conversation_summary_plugin.py @@ -23,7 +23,7 @@ async def test_azure_summarize_conversation_using_plugin(setup_summarize_convers else: # Load credentials from .env file deployment_name, api_key, endpoint = get_aoai_config - deployment_name = "text-davinci-003" + deployment_name = "gpt-35-turbo-instruct" kernel.add_text_completion_service( "text_completion", @@ -43,10 +43,15 @@ async def test_azure_summarize_conversation_using_plugin(setup_summarize_convers @pytest.mark.asyncio +@pytest.mark.xfail(reason="This test fails intermittently when run in parallel with other tests") async def test_oai_summarize_conversation_using_plugin( setup_summarize_conversation_using_plugin, ): - kernel, chatTranscript = setup_summarize_conversation_using_plugin + _, chatTranscript = setup_summarize_conversation_using_plugin + + # Defining a new kernel here to avoid using the same kernel as the previous test + # which causes failures. + kernel = sk.Kernel() if "Python_Integration_Tests" in os.environ: api_key = os.environ["OpenAI__ApiKey"] @@ -57,7 +62,7 @@ async def test_oai_summarize_conversation_using_plugin( kernel.add_text_completion_service( "davinci-003", - sk_oai.OpenAITextCompletion("text-davinci-003", api_key, org_id=org_id), + sk_oai.OpenAITextCompletion("gpt-3.5-turbo-instruct", api_key, org_id=org_id), ) conversationSummaryPlugin = kernel.import_plugin(ConversationSummaryPlugin(kernel), "conversationSummary") diff --git a/python/tests/integration/planning/sequential_planner/test_sequential_planner.py b/python/tests/integration/planning/sequential_planner/test_sequential_planner.py index f237cd68af96..2d3fdac632a2 100644 --- a/python/tests/integration/planning/sequential_planner/test_sequential_planner.py +++ b/python/tests/integration/planning/sequential_planner/test_sequential_planner.py @@ -71,13 +71,13 @@ def initialize_kernel(get_aoai_config, use_embeddings=False, use_chat_model=Fals False, "Write a joke and send it in an e-mail to Kai.", "SendEmail", - "_GLOBAL_FUNCTIONS_", + "email_plugin_fake", ), ( True, "Write a joke and send it in an e-mail to Kai.", "SendEmail", - "_GLOBAL_FUNCTIONS_", + "email_plugin_fake", ), ], ) @@ -85,8 +85,8 @@ def initialize_kernel(get_aoai_config, use_embeddings=False, use_chat_model=Fals async def test_create_plan_function_flow(get_aoai_config, use_chat_model, prompt, expected_function, expected_plugin): # Arrange kernel = initialize_kernel(get_aoai_config, False, use_chat_model) - kernel.import_plugin(EmailPluginFake()) - kernel.import_plugin(FunPluginFake()) + kernel.import_plugin(EmailPluginFake(), "email_plugin_fake") + kernel.import_plugin(FunPluginFake(), "fun_plugin_fake") planner = SequentialPlanner(kernel) @@ -116,7 +116,7 @@ async def test_create_plan_function_flow(get_aoai_config, use_chat_model, prompt async def test_create_plan_with_defaults(get_aoai_config, prompt, expected_function, expected_plugin, expected_default): # Arrange kernel = initialize_kernel(get_aoai_config) - kernel.import_plugin(EmailPluginFake()) + kernel.import_plugin(EmailPluginFake(), "email_plugin_fake") kernel.import_plugin(WriterPluginFake(), "WriterPlugin") planner = SequentialPlanner(kernel) @@ -139,7 +139,7 @@ async def test_create_plan_with_defaults(get_aoai_config, prompt, expected_funct ( "Write a poem or joke and send it in an e-mail to Kai.", "SendEmail", - "_GLOBAL_FUNCTIONS_", + "email_plugin_fake", ) ], ) @@ -151,9 +151,9 @@ async def test_create_plan_with_defaults(get_aoai_config, prompt, expected_funct async def test_create_plan_goal_relevant(get_aoai_config, prompt, expected_function, expected_plugin): # Arrange kernel = initialize_kernel(get_aoai_config, use_embeddings=True) - kernel.import_plugin(EmailPluginFake()) - kernel.import_plugin(FunPluginFake()) - kernel.import_plugin(WriterPluginFake()) + kernel.import_plugin(EmailPluginFake(), "email_plugin_fake") + kernel.import_plugin(FunPluginFake(), "fun_plugin_fake") + kernel.import_plugin(WriterPluginFake(), "writer_plugin_fake") planner = SequentialPlanner( kernel, diff --git a/python/tests/integration/planning/stepwise_planner/test_stepwise_planner.py b/python/tests/integration/planning/stepwise_planner/test_stepwise_planner.py index d2bd41c737f6..18c4d4e0245b 100644 --- a/python/tests/integration/planning/stepwise_planner/test_stepwise_planner.py +++ b/python/tests/integration/planning/stepwise_planner/test_stepwise_planner.py @@ -23,7 +23,7 @@ class TempWebSearchEnginePlugin: """ TODO: replace this class with semantic_kernel.core_plugins.web_search_engine_plugin.WebSearchEnginePlugin - SKFunction.describe() does not contains info for arguments. + KernelFunction.describe() does not contains info for arguments. so that `query: str` is not shown in the function description, BUT this argument must be passed to planner to work appropriately. diff --git a/python/tests/template_engine/prompt_template_e2e_tests.py b/python/tests/template_engine/prompt_template_e2e_tests.py index 80056fb53d65..b6efa7ecce10 100644 --- a/python/tests/template_engine/prompt_template_e2e_tests.py +++ b/python/tests/template_engine/prompt_template_e2e_tests.py @@ -143,7 +143,7 @@ async def test_it_allows_to_pass_escaped_values2_to_functions(self): async def test_it_handle_edge_cases(self, template: str, expected_result: str): # Arrange kernel = Kernel() - kernel.import_plugin(MyPlugin()) + kernel.import_plugin(MyPlugin(), "my_plugin") context = kernel.create_new_context() # Act diff --git a/python/tests/unit/core_plugins/test_file_io_plugin.py b/python/tests/unit/core_plugins/test_file_io_plugin.py index a83b3a7b4861..46ee463dd564 100644 --- a/python/tests/unit/core_plugins/test_file_io_plugin.py +++ b/python/tests/unit/core_plugins/test_file_io_plugin.py @@ -16,7 +16,9 @@ def test_can_be_instantiated(): def test_can_be_imported(): kernel = Kernel() assert kernel.import_plugin(FileIOPlugin(), "file") - assert kernel.plugins.has_native_function("file", "readAsync") + assert kernel.plugins["file"] is not None + assert kernel.plugins["file"].name == "file" + assert kernel.plugins["file"]["readAsync"] is not None @pytest.mark.asyncio diff --git a/python/tests/unit/core_plugins/test_http_plugin.py b/python/tests/unit/core_plugins/test_http_plugin.py index 926663b4b7fe..7820c38ddfc6 100644 --- a/python/tests/unit/core_plugins/test_http_plugin.py +++ b/python/tests/unit/core_plugins/test_http_plugin.py @@ -20,8 +20,10 @@ async def test_it_can_be_imported(): kernel = Kernel() plugin = HttpPlugin() assert kernel.import_plugin(plugin, "http") - assert kernel.plugins.has_native_function("http", "getAsync") - assert kernel.plugins.has_native_function("http", "postAsync") + assert kernel.plugins["http"] is not None + assert kernel.plugins["http"].name == "http" + assert kernel.plugins["http"]["getAsync"] is not None + assert kernel.plugins["http"]["postAsync"] is not None @patch("aiohttp.ClientSession.get") diff --git a/python/tests/unit/core_plugins/test_math_plugin.py b/python/tests/unit/core_plugins/test_math_plugin.py index e9957a473afd..bbffbf8203b7 100644 --- a/python/tests/unit/core_plugins/test_math_plugin.py +++ b/python/tests/unit/core_plugins/test_math_plugin.py @@ -15,8 +15,10 @@ def test_can_be_instantiated(): def test_can_be_imported(): kernel = Kernel() assert kernel.import_plugin(MathPlugin(), "math") - assert kernel.plugins.has_native_function("math", "add") - assert kernel.plugins.has_native_function("math", "subtract") + assert kernel.plugins["math"] is not None + assert kernel.plugins["math"].name == "math" + assert kernel.plugins["math"]["Add"] is not None + assert kernel.plugins["math"]["Subtract"] is not None @pytest.mark.parametrize( diff --git a/python/tests/unit/core_plugins/test_text_plugin.py b/python/tests/unit/core_plugins/test_text_plugin.py index f9302902f782..008bd118ca91 100644 --- a/python/tests/unit/core_plugins/test_text_plugin.py +++ b/python/tests/unit/core_plugins/test_text_plugin.py @@ -1,6 +1,5 @@ import semantic_kernel as sk from semantic_kernel.core_plugins.text_plugin import TextPlugin -from semantic_kernel.plugin_definition.plugin_collection import PluginCollection def test_can_be_instantiated(): @@ -9,14 +8,14 @@ def test_can_be_instantiated(): def test_can_be_imported(): kernel = sk.Kernel() - assert kernel.import_plugin(TextPlugin()) - assert kernel.plugins.has_native_function(PluginCollection.GLOBAL_PLUGIN, "trim") + assert kernel.import_plugin(TextPlugin(), "text_plugin") + assert kernel.plugins["text_plugin"]["trim"].is_native def test_can_be_imported_with_name(): kernel = sk.Kernel() assert kernel.import_plugin(TextPlugin(), "text") - assert kernel.plugins.has_native_function("text", "trim") + assert kernel.plugins["text"]["trim"].is_native def test_can_trim(): diff --git a/python/tests/unit/core_plugins/test_time_plugin.py b/python/tests/unit/core_plugins/test_time_plugin.py index 40ae5c1121da..40e58b2bcd3e 100644 --- a/python/tests/unit/core_plugins/test_time_plugin.py +++ b/python/tests/unit/core_plugins/test_time_plugin.py @@ -15,7 +15,9 @@ def test_can_be_instantiated(): def test_can_be_imported(): kernel = sk.Kernel() assert kernel.import_plugin(TimePlugin(), "time") - assert kernel.plugins.has_native_function("time", "now") + assert kernel.plugins["time"] is not None + assert kernel.plugins["time"].name == "time" + assert kernel.plugins["time"]["now"] is not None def test_date(): diff --git a/python/tests/unit/kernel_extensions/test_import_plugins.py b/python/tests/unit/kernel_extensions/test_import_plugins.py index 9bf5d44be33f..038e592ddbec 100644 --- a/python/tests/unit/kernel_extensions/test_import_plugins.py +++ b/python/tests/unit/kernel_extensions/test_import_plugins.py @@ -19,14 +19,11 @@ def test_plugin_can_be_imported(): # import plugins plugins_directory = os.path.join(os.path.dirname(__file__), "../..", "test_plugins") # path to plugins directory - plugin_config_dict = kernel.import_semantic_plugin_from_directory(plugins_directory, "TestPlugin") + plugin = kernel.import_semantic_plugin_from_directory(plugins_directory, "TestPlugin") - assert plugin_config_dict is not None - assert len(plugin_config_dict) == 1 - assert "TestFunction" in plugin_config_dict - plugin_config = plugin_config_dict["TestFunction"] - assert plugin_config.name == "TestFunction" - assert plugin_config.description == "Test Description" + assert plugin is not None + assert len(plugin.functions) == 1 + assert plugin.functions.get("TestFunction") is not None def test_native_plugin_can_be_imported(): @@ -36,11 +33,11 @@ def test_native_plugin_can_be_imported(): # import plugins plugins_directory = os.path.join(os.path.dirname(__file__), "../..", "test_native_plugins") # path to plugins directory - plugin_config_dict = kernel.import_native_plugin_from_directory(plugins_directory, "TestNativePlugin") + plugin = kernel.import_native_plugin_from_directory(plugins_directory, "TestNativePlugin") - assert plugin_config_dict is not None - assert len(plugin_config_dict) == 1 - assert "echoAsync" in plugin_config_dict - plugin_config = plugin_config_dict["echoAsync"] + assert plugin is not None + assert len(plugin.functions) == 1 + assert plugin.functions.get("echoAsync") is not None + plugin_config = plugin.functions["echoAsync"] assert plugin_config.name == "echoAsync" assert plugin_config.description == "Echo for input text" diff --git a/python/tests/unit/kernel_extensions/test_register_functions.py b/python/tests/unit/kernel_extensions/test_register_functions.py index de28d14a82e2..0bb5046cb27c 100644 --- a/python/tests/unit/kernel_extensions/test_register_functions.py +++ b/python/tests/unit/kernel_extensions/test_register_functions.py @@ -7,7 +7,6 @@ from semantic_kernel.kernel_exception import KernelException from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase from semantic_kernel.plugin_definition.kernel_function_decorator import kernel_function -from semantic_kernel.plugin_definition.plugin_collection import PluginCollection def not_decorated_native_function(arg1: str) -> str: @@ -26,7 +25,7 @@ async def test_register_valid_native_function(): registered_func = kernel.register_native_function("TestPlugin", decorated_native_function) assert isinstance(registered_func, KernelFunctionBase) - assert kernel.plugins.get_native_function("TestPlugin", "getLightStatus") == registered_func + assert kernel.plugins["TestPlugin"]["getLightStatus"] == registered_func func_result = await registered_func.invoke("testtest") assert func_result.result == "test" @@ -42,7 +41,8 @@ def test_register_with_none_plugin_name(): kernel = Kernel() registered_func = kernel.register_native_function(None, decorated_native_function) - assert registered_func.plugin_name == PluginCollection.GLOBAL_PLUGIN + assert registered_func.plugin_name is not None + assert registered_func.plugin_name.startswith("p_") def test_register_overloaded_native_function(): @@ -52,7 +52,3 @@ def test_register_overloaded_native_function(): with pytest.raises(KernelException): kernel.register_native_function("TestPlugin", decorated_native_function) - - -if __name__ == "__main__": - pytest.main([__file__]) diff --git a/python/tests/unit/planning/action_planner/test_action_planner.py b/python/tests/unit/planning/action_planner/test_action_planner.py index 425b87cadab2..ba6f2069fe3d 100644 --- a/python/tests/unit/planning/action_planner/test_action_planner.py +++ b/python/tests/unit/planning/action_planner/test_action_planner.py @@ -1,5 +1,5 @@ from textwrap import dedent -from unittest.mock import Mock +from unittest.mock import MagicMock, Mock import pytest @@ -15,8 +15,9 @@ from semantic_kernel.planning.planning_exception import PlanningException from semantic_kernel.plugin_definition.function_view import FunctionView from semantic_kernel.plugin_definition.functions_view import FunctionsView -from semantic_kernel.plugin_definition.plugin_collection_base import ( - PluginCollectionBase, +from semantic_kernel.plugin_definition.kernel_plugin import KernelPlugin +from semantic_kernel.plugin_definition.kernel_plugin_collection import ( + KernelPluginCollection, ) @@ -56,7 +57,7 @@ async def test_plan_creation(): kernel = Mock(spec=Kernel) mock_function = Mock(spec=KernelFunctionBase) memory = Mock(spec=SemanticTextMemoryBase) - plugins = Mock(spec=PluginCollectionBase) + plugins = KernelPluginCollection() function_view = FunctionView( name="Translate", @@ -66,12 +67,11 @@ async def test_plan_creation(): parameters=[], ) mock_function = create_mock_function(function_view) - plugins.get_function.return_value = mock_function - context = KernelContext.model_construct(variables=ContextVariables(), memory=memory, plugin_collection=plugins) - return_context = KernelContext.model_construct( - variables=ContextVariables(), memory=memory, plugin_collection=plugins - ) + plugins.add(plugin=KernelPlugin(name=function_view.plugin_name, functions=[mock_function])) + + context = KernelContext.model_construct(variables=ContextVariables(), memory=memory, plugins=plugins) + return_context = KernelContext.model_construct(variables=ContextVariables(), memory=memory, plugins=plugins) return_context.variables.update(plan_str) @@ -106,24 +106,27 @@ def mock_context(plugins_input): context = Mock(spec=KernelContext) functionsView = FunctionsView() - plugins = Mock(spec=PluginCollectionBase) - mock_functions = [] - for name, pluginName, description, isSemantic in plugins_input: - function_view = FunctionView(name, pluginName, description, [], isSemantic, True) + + plugins = MagicMock(spec=KernelPluginCollection) + + mock_plugins = {} + + for name, plugin_name, description, is_semantic in plugins_input: + function_view = FunctionView(name, plugin_name, description, [], is_semantic, True) mock_function = create_mock_function(function_view) functionsView.add_function(function_view) - _context = KernelContext.model_construct(variables=ContextVariables(), memory=memory, plugin_collection=plugins) + if plugin_name not in mock_plugins: + mock_plugins[plugin_name] = {} + mock_plugins[plugin_name][name] = mock_function + + _context = KernelContext.model_construct(variables=ContextVariables(), memory=memory, plugins=plugins) _context.variables.update("MOCK FUNCTION CALLED") mock_function.invoke.return_value = _context - mock_functions.append(mock_function) - plugins.get_function.side_effect = lambda plugin_name, function_name: next( - (func for func in mock_functions if func.plugin_name == plugin_name and func.name == function_name), - None, - ) - plugins.get_functions_view.return_value = functionsView - context.plugins.return_value = plugins + plugins.__getitem__.side_effect = lambda plugin_name: MagicMock(__getitem__=mock_plugins[plugin_name].__getitem__) + + context.plugins = plugins context.plugins.get_functions_view.return_value = functionsView return context @@ -179,14 +182,13 @@ def test_exclude_functions(plugins_input, mock_context): @pytest.mark.asyncio -async def test_invalid_json_throw(): - goal = "Translate Happy birthday to German." - plan_str = '{"":{""function"": ""WriterPlugin.Translate""}}' +async def test_empty_goal_throw(): + goal = "" kernel = Mock(spec=Kernel) mock_function = Mock(spec=KernelFunctionBase) memory = Mock(spec=SemanticTextMemoryBase) - plugins = Mock(spec=PluginCollectionBase) + plugins = MagicMock(spec=KernelPluginCollection) function_view = FunctionView( name="Translate", @@ -196,15 +198,10 @@ async def test_invalid_json_throw(): parameters=[], ) mock_function = create_mock_function(function_view) - plugins.get_function.return_value = mock_function - - context = KernelContext.model_construct(variables=ContextVariables(), memory=memory, plugin_collection=plugins) - return_context = KernelContext.model_construct( - variables=ContextVariables(), memory=memory, plugin_collection=plugins - ) - - return_context.variables.update(plan_str) + plugins.__getitem__.return_value = MagicMock(__getitem__=MagicMock(return_value=mock_function)) + context = KernelContext.model_construct(variables=ContextVariables(), memory=memory, plugins=plugins) + return_context = KernelContext.model_construct(variables=ContextVariables(), memory=memory, plugins=plugins) mock_function.invoke.return_value = return_context kernel.create_semantic_function.return_value = mock_function @@ -217,28 +214,29 @@ async def test_invalid_json_throw(): @pytest.mark.asyncio -async def test_empty_goal_throw(): - goal = "" +async def test_invalid_json_throw(): + goal = "Translate Happy birthday to German." + plan_str = '{"":{""function"": ""WriterPlugin.Translate""}}' kernel = Mock(spec=Kernel) - mock_function = Mock(spec=KernelFunctionBase) memory = Mock(spec=SemanticTextMemoryBase) - plugins = Mock(spec=PluginCollectionBase) + plugins = MagicMock(spec=KernelPluginCollection) function_view = FunctionView( name="Translate", - description="Translate something", plugin_name="WriterPlugin", + description="Translate something", is_semantic=False, parameters=[], ) mock_function = create_mock_function(function_view) - plugins.get_function.return_value = mock_function - context = KernelContext.model_construct(variables=ContextVariables(), memory=memory, plugin_collection=plugins) - return_context = KernelContext.model_construct( - variables=ContextVariables(), memory=memory, plugin_collection=plugins - ) + plugins.__getitem__.return_value = MagicMock(__getitem__=MagicMock(return_value=mock_function)) + + context = KernelContext.model_construct(variables=ContextVariables(), memory=memory, plugins=plugins) + return_context = KernelContext.model_construct(variables=ContextVariables(), memory=memory, plugins=plugins) + + return_context.variables.update(plan_str) mock_function.invoke.return_value = return_context kernel.create_semantic_function.return_value = mock_function diff --git a/python/tests/unit/planning/sequential_planner/test_sequential_planner.py b/python/tests/unit/planning/sequential_planner/test_sequential_planner.py index b0076109a704..1872c92bb1da 100644 --- a/python/tests/unit/planning/sequential_planner/test_sequential_planner.py +++ b/python/tests/unit/planning/sequential_planner/test_sequential_planner.py @@ -15,8 +15,9 @@ ) from semantic_kernel.plugin_definition.function_view import FunctionView from semantic_kernel.plugin_definition.functions_view import FunctionsView -from semantic_kernel.plugin_definition.plugin_collection_base import ( - PluginCollectionBase, +from semantic_kernel.plugin_definition.kernel_plugin import KernelPlugin +from semantic_kernel.plugin_definition.kernel_plugin_collection import ( + KernelPluginCollection, ) @@ -44,31 +45,27 @@ async def test_it_can_create_plan(goal): ] functionsView = FunctionsView() - plugins = Mock(spec=PluginCollectionBase) + plugins = KernelPluginCollection() mock_functions = [] for name, pluginName, description, isSemantic in input: function_view = FunctionView(name, pluginName, description, [], isSemantic, True) mock_function = create_mock_function(function_view) functionsView.add_function(function_view) - context = KernelContext.model_construct(variables=ContextVariables(), memory=memory, plugin_collection=plugins) + context = KernelContext.model_construct(variables=ContextVariables(), memory=memory, plugins=plugins) context.variables.update("MOCK FUNCTION CALLED") mock_function.invoke.return_value = context mock_functions.append(mock_function) - plugins.get_function.side_effect = lambda plugin_name, function_name: next( - (func for func in mock_functions if func.plugin_name == plugin_name and func.name == function_name), - None, - ) - plugins.get_functions_view.return_value = functionsView + if pluginName not in plugins.plugins: + plugins.add(KernelPlugin(name=pluginName, description="Mock plugin")) + plugins.add_functions_to_plugin([mock_function], pluginName) expected_functions = [x[0] for x in input] expected_plugins = [x[1] for x in input] - context = KernelContext.model_construct(variables=ContextVariables(), memory=memory, plugin_collection=plugins) - return_context = KernelContext.model_construct( - variables=ContextVariables(), memory=memory, plugin_collection=plugins - ) + context = KernelContext.model_construct(variables=ContextVariables(), memory=memory, plugins=plugins) + return_context = KernelContext.model_construct(variables=ContextVariables(), memory=memory, plugins=plugins) plan_string = """ @@ -116,7 +113,7 @@ async def test_invalid_xml_throws(): # Arrange kernel = Mock(spec=Kernel) memory = Mock(spec=SemanticTextMemoryBase) - plugins = Mock(spec=PluginCollectionBase) + plugins = Mock(spec=KernelPluginCollection) functionsView = FunctionsView() plugins.get_functions_view.return_value = functionsView @@ -125,10 +122,10 @@ async def test_invalid_xml_throws(): return_context = KernelContext.model_construct( variables=ContextVariables(plan_string), memory=memory, - plugin_collection=plugins, + plugins=plugins, ) - context = KernelContext.model_construct(variables=ContextVariables(), memory=memory, plugin_collection=plugins) + context = KernelContext.model_construct(variables=ContextVariables(), memory=memory, plugins=plugins) mock_function_flow_function = Mock(spec=KernelFunctionBase) mock_function_flow_function.invoke.return_value = return_context diff --git a/python/tests/unit/planning/sequential_planner/test_sequential_planner_extensions.py b/python/tests/unit/planning/sequential_planner/test_sequential_planner_extensions.py index a41baa0041c1..87e46c312332 100644 --- a/python/tests/unit/planning/sequential_planner/test_sequential_planner_extensions.py +++ b/python/tests/unit/planning/sequential_planner/test_sequential_planner_extensions.py @@ -8,7 +8,6 @@ from semantic_kernel.memory.semantic_text_memory_base import SemanticTextMemoryBase from semantic_kernel.orchestration.context_variables import ContextVariables from semantic_kernel.orchestration.kernel_context import KernelContext -from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase from semantic_kernel.planning.sequential_planner.sequential_planner_config import ( SequentialPlannerConfig, ) @@ -18,9 +17,8 @@ ) from semantic_kernel.plugin_definition.function_view import FunctionView from semantic_kernel.plugin_definition.functions_view import FunctionsView -from semantic_kernel.plugin_definition.plugin_collection import PluginCollection -from semantic_kernel.plugin_definition.read_only_plugin_collection_base import ( - ReadOnlyPluginCollectionBase, +from semantic_kernel.plugin_definition.kernel_plugin_collection import ( + KernelPluginCollection, ) @@ -31,7 +29,7 @@ async def _async_generator(query_result): @pytest.mark.asyncio async def test_can_call_get_available_functions_with_no_functions(): variables = ContextVariables() - plugins = PluginCollection() + plugins = KernelPluginCollection() memory = Mock(spec=SemanticTextMemoryBase) memory_query_result = MemoryQueryResult( @@ -49,7 +47,7 @@ async def test_can_call_get_available_functions_with_no_functions(): memory.search.return_value = async_enumerable # Arrange GetAvailableFunctionsAsync parameters - context = KernelContext(variables, memory, plugins.read_only_plugin_collection) + context = KernelContext(variables=variables, memory=memory, plugins=plugins) config = SequentialPlannerConfig() semantic_query = "test" @@ -65,7 +63,6 @@ async def test_can_call_get_available_functions_with_no_functions(): async def test_can_call_get_available_functions_with_functions(): variables = ContextVariables() - function_mock = Mock(spec=KernelFunctionBase) functions_view = FunctionsView() function_view = FunctionView( "functionName", @@ -86,9 +83,8 @@ async def test_can_call_get_available_functions_with_functions(): functions_view.add_function(function_view) functions_view.add_function(native_function_view) - plugins = Mock(spec=ReadOnlyPluginCollectionBase) - plugins.get_function.return_value = function_mock - plugins.get_functions_view.return_value = functions_view + mock_plugins = Mock(spec=KernelPluginCollection) + mock_plugins.get_functions_view.return_value = functions_view memory_query_result = MemoryQueryResult( is_reference=False, @@ -106,7 +102,7 @@ async def test_can_call_get_available_functions_with_functions(): memory.search.return_value = async_enumerable # Arrange GetAvailableFunctionsAsync parameters - context = KernelContext.model_construct(variables=variables, memory=memory, plugin_collection=plugins) + context = KernelContext.model_construct(variables=variables, memory=memory, plugins=mock_plugins) config = SequentialPlannerConfig() semantic_query = "test" @@ -137,7 +133,6 @@ async def test_can_call_get_available_functions_with_functions_and_relevancy(): variables = ContextVariables() # Arrange FunctionView - function_mock = Mock(spec=KernelFunctionBase) functions_view = FunctionsView() function_view = FunctionView( "functionName", @@ -172,16 +167,14 @@ async def test_can_call_get_available_functions_with_functions_and_relevancy(): memory = Mock(spec=SemanticTextMemoryBase) memory.search.return_value = _async_generator(memory_query_result) - plugins = Mock(spec=ReadOnlyPluginCollectionBase) - plugins.get_function.return_value = function_mock - plugins.get_functions_view.return_value = functions_view - plugins.read_only_plugin_collection = plugins + mock_plugins = Mock(spec=KernelPluginCollection) + mock_plugins.get_functions_view.return_value = functions_view # Arrange GetAvailableFunctionsAsync parameters context = KernelContext.model_construct( variables=variables, memory=memory, - plugin_collection=plugins, + plugins=mock_plugins, ) config = SequentialPlannerConfig(relevancy_threshold=0.78) semantic_query = "test" @@ -212,7 +205,7 @@ async def test_can_call_get_available_functions_with_functions_and_relevancy(): async def test_can_call_get_available_functions_with_default_relevancy(): # Arrange variables = ContextVariables() - plugins = PluginCollection() + plugins = KernelPluginCollection() # Arrange Mock Memory and Result memory_query_result = MemoryQueryResult( @@ -230,7 +223,7 @@ async def test_can_call_get_available_functions_with_default_relevancy(): memory.search.return_value = async_enumerable # Arrange GetAvailableFunctionsAsync parameters - context = KernelContext.model_construct(variables=variables, memory=memory, plugin_collection=plugins) + context = KernelContext.model_construct(variables=variables, memory=memory, plugins=plugins) config = SequentialPlannerConfig(relevancy_threshold=0.78) semantic_query = "test" diff --git a/python/tests/unit/planning/sequential_planner/test_sequential_planner_parser.py b/python/tests/unit/planning/sequential_planner/test_sequential_planner_parser.py index ea79d4519108..8762a8da3f56 100644 --- a/python/tests/unit/planning/sequential_planner/test_sequential_planner_parser.py +++ b/python/tests/unit/planning/sequential_planner/test_sequential_planner_parser.py @@ -12,6 +12,7 @@ ) from semantic_kernel.plugin_definition.function_view import FunctionView from semantic_kernel.plugin_definition.functions_view import FunctionsView +from semantic_kernel.plugin_definition.kernel_plugin import KernelPlugin def create_mock_function(function_view: FunctionView) -> KernelFunctionBase: @@ -34,7 +35,7 @@ def create_kernel_and_functions_mock(functions) -> Kernel: result = kernel.create_new_context() result.variables.update(result_string) mock_function.invoke.return_value = result - kernel._plugin_collection.add_semantic_function(mock_function) + kernel.plugins.add(KernelPlugin(name=plugin_name, functions=[mock_function])) return kernel @@ -51,21 +52,21 @@ def test_can_call_to_plan_from_xml(): ("Translate", "WriterPlugin", "Translate to french", True, "Bonjour!"), ( "GetEmailAddressAsync", - "email", + "get_email", "Get email address", False, "johndoe@email.com", ), - ("SendEmailAsync", "email", "Send email", False, "Email sent."), + ("SendEmailAsync", "send_email", "Send email", False, "Email sent."), ] kernel = create_kernel_and_functions_mock(functions) plan_string = """ - - + """ goal = "Summarize an input, translate to french, and e-mail to John Doe" @@ -86,12 +87,12 @@ def test_can_call_to_plan_from_xml(): assert plan._steps[1].parameters["language"] == "French" assert "TRANSLATED_SUMMARY" in plan._steps[1]._outputs - assert plan._steps[2].plugin_name == "email" + assert plan._steps[2].plugin_name == "get_email" assert plan._steps[2].name == "GetEmailAddressAsync" assert plan._steps[2].parameters["input"] == "John Doe" assert "EMAIL_ADDRESS" in plan._steps[2]._outputs - assert plan._steps[3].plugin_name == "email" + assert plan._steps[3].plugin_name == "send_email" assert plan._steps[3].name == "SendEmailAsync" assert "$TRANSLATED_SUMMARY" in plan._steps[3].parameters["input"] assert "$EMAIL_ADDRESS" in plan._steps[3].parameters["email_address"] diff --git a/python/tests/unit/planning/test_plan_creation.py b/python/tests/unit/planning/test_plan_creation.py index 3cbe4a308f52..f633dd0ae667 100644 --- a/python/tests/unit/planning/test_plan_creation.py +++ b/python/tests/unit/planning/test_plan_creation.py @@ -84,9 +84,9 @@ def test_create_plan_with_name_and_function(): # import test (math) plugin plugin = MathPlugin() - plugin_config_dict = kernel.import_plugin(plugin, "math") + plugin = kernel.import_plugin(plugin, "math") - test_function = plugin_config_dict["Add"] + test_function = plugin["Add"] plan = Plan(name="test", function=test_function) assert plan is not None @@ -110,10 +110,10 @@ def test_create_multistep_plan_with_functions(): # import test (math) plugin plugin = MathPlugin() - plugin_config_dict = kernel.import_plugin(plugin, "math") + plugin = kernel.import_plugin(plugin, "math") - test_function1 = plugin_config_dict["Add"] - test_function2 = plugin_config_dict["Subtract"] + test_function1 = plugin["Add"] + test_function2 = plugin["Subtract"] plan = Plan(name="multistep_test") plan.add_steps([test_function1, test_function2]) @@ -139,10 +139,10 @@ def test_create_multistep_plan_with_plans(): # import test (math) plugin plugin = MathPlugin() - plugin_config_dict = kernel.import_plugin(plugin, "math") + plugin = kernel.import_plugin(plugin, "math") - test_function1 = plugin_config_dict["Add"] - test_function2 = plugin_config_dict["Subtract"] + test_function1 = plugin["Add"] + test_function2 = plugin["Subtract"] plan = Plan(name="multistep_test") plan_step1 = Plan(name="step1", function=test_function1) @@ -170,10 +170,10 @@ def test_add_step_to_plan(): # import test (math) plugin plugin = MathPlugin() - plugin_config_dict = kernel.import_plugin(plugin, "math") + plugin = kernel.import_plugin(plugin, "math") - test_function1 = plugin_config_dict["Add"] - test_function2 = plugin_config_dict["Subtract"] + test_function1 = plugin["Add"] + test_function2 = plugin["Subtract"] plan = Plan(name="multistep_test", function=test_function1) plan.add_steps([test_function2]) diff --git a/python/tests/unit/planning/test_plan_execution.py b/python/tests/unit/planning/test_plan_execution.py index 8832d9041ed8..28c43c5f5cbf 100644 --- a/python/tests/unit/planning/test_plan_execution.py +++ b/python/tests/unit/planning/test_plan_execution.py @@ -29,8 +29,8 @@ async def test_invoke_plan_constructed_with_function(): # import test (text) plugin plugin = TextPlugin() - plugin_config_dict = kernel.import_plugin(plugin, "text") - test_function = plugin_config_dict["uppercase"] + plugin = kernel.import_plugin(plugin, "text") + test_function = plugin["uppercase"] # setup context context = kernel.create_new_context() @@ -48,8 +48,8 @@ async def test_invoke_plan_constructed_with_function_async(): # import test (text) plugin plugin = TextPlugin() - plugin_config_dict = kernel.import_plugin(plugin, "text") - test_function = plugin_config_dict["uppercase"] + plugin = kernel.import_plugin(plugin, "text") + test_function = plugin["uppercase"] # setup context context = kernel.create_new_context() @@ -67,8 +67,8 @@ async def test_invoke_empty_plan_with_added_function_step(): # import test (text) plugin plugin = TextPlugin() - plugin_config_dict = kernel.import_plugin(plugin, "text") - test_function = plugin_config_dict["uppercase"] + plugin = kernel.import_plugin(plugin, "text") + test_function = plugin["uppercase"] # setup context context = kernel.create_new_context() @@ -87,8 +87,8 @@ async def test_invoke_empty_plan_with_added_function_step_async(): # import test (text) plugin plugin = TextPlugin() - plugin_config_dict = kernel.import_plugin(plugin, "text") - test_function = plugin_config_dict["uppercase"] + plugin = kernel.import_plugin(plugin, "text") + test_function = plugin["uppercase"] # setup context context = kernel.create_new_context() @@ -107,8 +107,8 @@ async def test_invoke_empty_plan_with_added_plan_step(): # import test (text) plugin plugin = TextPlugin() - plugin_config_dict = kernel.import_plugin(plugin, "text") - test_function = plugin_config_dict["uppercase"] + plugin = kernel.import_plugin(plugin, "text") + test_function = plugin["uppercase"] # setup context context = kernel.create_new_context() @@ -128,8 +128,8 @@ async def test_invoke_empty_plan_with_added_plan_step_async(): # import test (text) plugin plugin = TextPlugin() - plugin_config_dict = kernel.import_plugin(plugin, "text") - test_function = plugin_config_dict["uppercase"] + plugin = kernel.import_plugin(plugin, "text") + test_function = plugin["uppercase"] # setup context context = kernel.create_new_context() @@ -149,9 +149,9 @@ async def test_invoke_multi_step_plan(): # import test (text) plugin plugin = TextPlugin() - plugin_config_dict = kernel.import_plugin(plugin, "text") - test_function = plugin_config_dict["uppercase"] - test_function2 = plugin_config_dict["trim_end"] + plugin = kernel.import_plugin(plugin, "text") + test_function = plugin["uppercase"] + test_function2 = plugin["trim_end"] # setup context context = kernel.create_new_context() @@ -172,9 +172,9 @@ async def test_invoke_multi_step_plan_async(): # import test (text) plugin plugin = TextPlugin() - plugin_config_dict = kernel.import_plugin(plugin, "text") - test_function = plugin_config_dict["uppercase"] - test_function2 = plugin_config_dict["trim_end"] + plugin = kernel.import_plugin(plugin, "text") + test_function = plugin["uppercase"] + test_function2 = plugin["trim_end"] # setup context context = kernel.create_new_context() @@ -195,9 +195,9 @@ async def test_invoke_multi_step_plan_async_with_variables(): # import test (text) plugin plugin = MathPlugin() - plugin_config_dict = kernel.import_plugin(plugin, "math") - test_function = plugin_config_dict["Add"] - test_function2 = plugin_config_dict["Subtract"] + plugin = kernel.import_plugin(plugin, "math") + test_function = plugin["Add"] + test_function2 = plugin["Subtract"] plan = Plan(name="test") diff --git a/python/tests/unit/plugin_definition/test_kernel_plugin_collection.py b/python/tests/unit/plugin_definition/test_kernel_plugin_collection.py new file mode 100644 index 000000000000..8eadee35cea1 --- /dev/null +++ b/python/tests/unit/plugin_definition/test_kernel_plugin_collection.py @@ -0,0 +1,98 @@ +# Copyright (c) Microsoft. All rights reserved. + +from string import ascii_uppercase + +import pytest + +from semantic_kernel.plugin_definition.kernel_plugin import KernelPlugin +from semantic_kernel.plugin_definition.kernel_plugin_collection import KernelPluginCollection + + +def test_add_plugin(): + collection = KernelPluginCollection() + plugin = KernelPlugin(name="TestPlugin") + collection.add(plugin) + assert len(collection) == 1 + assert plugin.name in collection + + +def test_add_plugin_with_description(): + expected_description = "Test Description" + collection = KernelPluginCollection() + plugin = KernelPlugin(name="TestPlugin", description=expected_description) + collection.add(plugin) + assert len(collection) == 1 + assert plugin.name in collection + assert collection[plugin.name].description == expected_description + + +def test_remove_plugin(): + collection = KernelPluginCollection() + plugin = KernelPlugin(name="TestPlugin") + collection.add(plugin) + collection.remove(plugin) + assert len(collection) == 0 + + +def test_remove_plugin_by_name(): + collection = KernelPluginCollection() + expected_plugin_name = "TestPlugin" + plugin = KernelPlugin(name=expected_plugin_name) + collection.add(plugin) + collection.remove_by_name(expected_plugin_name) + assert len(collection) == 0 + + +def test_add_list_of_plugins(): + num_plugins = 3 + collection = KernelPluginCollection() + plugins = [KernelPlugin(name=f"Plugin_{ascii_uppercase[i]}") for i in range(num_plugins)] + collection.add_list_of_plugins(plugins) + assert len(collection) == num_plugins + + +def test_clear_collection(): + collection = KernelPluginCollection() + plugins = [KernelPlugin(name=f"Plugin_{ascii_uppercase[i]}") for i in range(3)] + collection.add_list_of_plugins(plugins) + collection.clear() + assert len(collection) == 0 + + +def test_iterate_collection(): + collection = KernelPluginCollection() + plugins = [KernelPlugin(name=f"Plugin_{ascii_uppercase[i]}") for i in range(3)] + collection.add_list_of_plugins(plugins) + + for i, plugin in enumerate(collection.plugins.values()): + assert plugin.name == f"Plugin_{ascii_uppercase[i]}" + + +def test_get_plugin(): + collection = KernelPluginCollection() + plugin = KernelPlugin(name="TestPlugin") + collection.add(plugin) + retrieved_plugin = collection["TestPlugin"] + assert retrieved_plugin == plugin + + +def test_get_plugin_not_found_raises_keyerror(): + collection = KernelPluginCollection() + with pytest.raises(KeyError): + _ = collection["NonExistentPlugin"] + + +def test_get_plugin_succeeds(): + collection = KernelPluginCollection() + plugin = KernelPlugin(name="TestPlugin") + collection.add(plugin) + found_plugin = collection["TestPlugin"] + assert found_plugin == plugin + with pytest.raises(KeyError): + collection["NonExistentPlugin"] is None + + +def test_configure_plugins_on_object_creation(): + plugin = KernelPlugin(name="TestPlugin") + collection = KernelPluginCollection(plugins=[plugin]) + assert len(collection) == 1 diff --git a/python/tests/unit/plugin_definition/test_kernel_plugins.py b/python/tests/unit/plugin_definition/test_kernel_plugins.py new file mode 100644 index 000000000000..39f98a0f9a26 --- /dev/null +++ b/python/tests/unit/plugin_definition/test_kernel_plugins.py @@ -0,0 +1,208 @@ +# Copyright (c) Microsoft. All rights reserved. + +from typing import TYPE_CHECKING + +import pytest + +from semantic_kernel.orchestration.kernel_function import KernelFunction +from semantic_kernel.plugin_definition.kernel_plugin import KernelPlugin +from semantic_kernel.semantic_functions.chat_prompt_template import ChatPromptTemplate +from semantic_kernel.semantic_functions.prompt_template_config import PromptTemplateConfig +from semantic_kernel.semantic_functions.semantic_function_config import SemanticFunctionConfig +from semantic_kernel.template_engine.prompt_template_engine import PromptTemplateEngine + +if TYPE_CHECKING: + from semantic_kernel.orchestration.kernel_context import KernelContext + + +def test_throws_for_missing_name(): + with pytest.raises(TypeError): + KernelPlugin(description="A unit test plugin") + + +def test_default_kernel_plugin_construction_with_no_functions(): + expected_plugin_name = "test_plugin" + expected_plugin_description = "A unit test plugin" + plugin = KernelPlugin(name=expected_plugin_name, description=expected_plugin_description) + assert plugin.name == expected_plugin_name + assert plugin.description == expected_plugin_description + + +def test_default_kernel_plugin_construction_with_native_functions(): + expected_plugin_name = "test_plugin" + expected_plugin_description = "A unit test plugin" + + def mock_function(input: str, context: "KernelContext") -> None: + pass + + mock_function.__kernel_function__ = True + mock_function.__kernel_function_name__ = "mock_function" + mock_function.__kernel_function_description__ = "Mock description" + mock_function.__kernel_function_input_description__ = "Mock input description" + mock_function.__kernel_function_input_default_value__ = "default_input_value" + mock_function.__kernel_function_context_parameters__ = [ + { + "name": "param1", + "description": "Param 1 description", + "default_value": "default_param1_value", + } + ] + + mock_method = mock_function + + native_function = KernelFunction.from_native_method(mock_method, "MockPlugin") + + plugin = KernelPlugin( + name=expected_plugin_name, description=expected_plugin_description, functions=[native_function] + ) + assert plugin.name == expected_plugin_name + assert plugin.description == expected_plugin_description + assert len(plugin.functions) == 1 + assert plugin["mock_function"] == native_function + + +def test_default_kernel_plugin_exposes_the_native_function_it_contains(): + expected_plugin_name = "test_plugin" + expected_plugin_description = "A unit test plugin" + + def mock_function(input: str, context: "KernelContext") -> None: + pass + + mock_function.__kernel_function__ = True + mock_function.__kernel_function_name__ = "mock_function" + mock_function.__kernel_function_description__ = "Mock description" + mock_function.__kernel_function_input_description__ = "Mock input description" + mock_function.__kernel_function_input_default_value__ = "default_input_value" + mock_function.__kernel_function_context_parameters__ = [ + { + "name": "param1", + "description": "Param 1 description", + "default_value": "default_param1_value", + } + ] + + mock_method = mock_function + + native_function = KernelFunction.from_native_method(mock_method, "MockPlugin") + + plugin = KernelPlugin( + name=expected_plugin_name, description=expected_plugin_description, functions=[native_function] + ) + assert plugin.name == expected_plugin_name + assert plugin.description == expected_plugin_description + assert len(plugin.functions) == 1 + assert plugin["mock_function"] == native_function + + for func in [native_function]: + assert func.name in plugin + assert plugin[func.name] == func + + +def test_default_kernel_plugin_construction_with_semantic_function(): + prompt_config = PromptTemplateConfig.from_execution_settings(max_tokens=2000, temperature=0.7, top_p=0.8) + prompt_template = ChatPromptTemplate("{{$user_input}}", PromptTemplateEngine(), prompt_config) + function_config = SemanticFunctionConfig(prompt_config, prompt_template) + + expected_plugin_name = "test_plugin" + expected_function_name = "mock_function" + semantic_function = KernelFunction.from_semantic_config( + plugin_name=expected_plugin_name, function_name=expected_function_name, function_config=function_config + ) + + expected_plugin_description = "A unit test plugin" + + plugin = KernelPlugin( + name=expected_plugin_name, description=expected_plugin_description, functions=[semantic_function] + ) + + assert plugin.name == expected_plugin_name + assert plugin.description == expected_plugin_description + assert len(plugin.functions) == 1 + assert plugin["mock_function"] == semantic_function + + +def test_default_kernel_plugin_construction_with_both_function_types(): + # Construct a semantic function + prompt_config = PromptTemplateConfig.from_execution_settings(max_tokens=2000, temperature=0.7, top_p=0.8) + prompt_template = ChatPromptTemplate("{{$user_input}}", PromptTemplateEngine(), prompt_config) + function_config = SemanticFunctionConfig(prompt_config, prompt_template) + + expected_plugin_name = "test_plugin" + expected_function_name = "mock_semantic_function" + semantic_function = KernelFunction.from_semantic_config( + plugin_name=expected_plugin_name, function_name=expected_function_name, function_config=function_config + ) + + # Construct a nativate function + def mock_function(input: str, context: "KernelContext") -> None: + pass + + mock_function.__kernel_function__ = True + mock_function.__kernel_function_name__ = "mock_native_function" + mock_function.__kernel_function_description__ = "Mock description" + mock_function.__kernel_function_input_description__ = "Mock input description" + mock_function.__kernel_function_input_default_value__ = "default_input_value" + mock_function.__kernel_function_context_parameters__ = [ + { + "name": "param1", + "description": "Param 1 description", + "default_value": "default_param1_value", + } + ] + + mock_method = mock_function + + native_function = KernelFunction.from_native_method(mock_method, "MockPlugin") + + # Add both types to the default kernel plugin + expected_plugin_description = "A unit test plugin" + + plugin = KernelPlugin( + name=expected_plugin_name, + description=expected_plugin_description, + functions=[semantic_function, native_function], + ) + + assert plugin.name == expected_plugin_name + assert plugin.description == expected_plugin_description + assert len(plugin.functions) == 2 + + for func in [semantic_function, native_function]: + assert func.name in plugin + assert plugin[func.name] == func + + +def test_default_kernel_plugin_construction_with_same_function_names_throws(): + # Construct a semantic function + prompt_config = PromptTemplateConfig.from_execution_settings(max_tokens=2000, temperature=0.7, top_p=0.8) + prompt_template = ChatPromptTemplate("{{$user_input}}", PromptTemplateEngine(), prompt_config) + function_config = SemanticFunctionConfig(prompt_config, prompt_template) + + expected_plugin_name = "test_plugin" + expected_function_name = "mock_function" + semantic_function = KernelFunction.from_semantic_config( + plugin_name=expected_plugin_name, function_name=expected_function_name, function_config=function_config + ) + + # Construct a nativate function + def mock_function(input: str, context: "KernelContext") -> None: + pass + + mock_function.__kernel_function__ = True + mock_function.__kernel_function_name__ = expected_function_name + mock_function.__kernel_function_description__ = "Mock description" + mock_function.__kernel_function_input_description__ = "Mock input description" + mock_function.__kernel_function_input_default_value__ = "default_input_value" + mock_function.__kernel_function_context_parameters__ = [ + { + "name": "param1", + "description": "Param 1 description", + "default_value": "default_param1_value", + } + ] + + mock_method = mock_function + native_function = KernelFunction.from_native_method(mock_method, "MockPlugin") + + with pytest.raises(ValueError): + KernelPlugin(name=expected_plugin_name, functions=[semantic_function, native_function]) diff --git a/python/tests/unit/template_engine/blocks/test_code_block.py b/python/tests/unit/template_engine/blocks/test_code_block.py index 0ef92605f049..a380a6ac9240 100644 --- a/python/tests/unit/template_engine/blocks/test_code_block.py +++ b/python/tests/unit/template_engine/blocks/test_code_block.py @@ -7,8 +7,9 @@ from semantic_kernel.orchestration.delegate_types import DelegateTypes from semantic_kernel.orchestration.kernel_context import KernelContext from semantic_kernel.orchestration.kernel_function import KernelFunction -from semantic_kernel.plugin_definition.read_only_plugin_collection_base import ( - ReadOnlyPluginCollectionBase, +from semantic_kernel.plugin_definition.kernel_plugin import KernelPlugin +from semantic_kernel.plugin_definition.kernel_plugin_collection import ( + KernelPluginCollection, ) from semantic_kernel.template_engine.blocks.block_types import BlockTypes from semantic_kernel.template_engine.blocks.code_block import CodeBlock @@ -19,17 +20,16 @@ class TestCodeBlock: def setup_method(self): - self.plugins = Mock(spec=ReadOnlyPluginCollectionBase) + self.plugins = Mock(spec=KernelPluginCollection) @mark.asyncio async def test_it_throws_if_a_function_doesnt_exist(self): context = KernelContext.model_construct( variables=ContextVariables(), memory=NullMemory(), - plugin_collection=self.plugins, + plugins=KernelPluginCollection(), ) - # Make it so our self.plugins mock's `has_function` method returns False - self.plugins.has_function.return_value = False + target = CodeBlock( content="functionName", ) @@ -39,12 +39,6 @@ async def test_it_throws_if_a_function_doesnt_exist(self): @mark.asyncio async def test_it_throws_if_a_function_call_throws(self): - context = KernelContext.model_construct( - variables=ContextVariables(), - memory=NullMemory(), - plugin_collection=self.plugins, - ) - def invoke(_): raise Exception("error") @@ -58,8 +52,16 @@ def invoke(_): is_semantic=False, ) - self.plugins.has_function.return_value = True - self.plugins.get_function.return_value = function + dkp = KernelPlugin(name="test", functions=[function]) + plugins = KernelPluginCollection() + plugins.add(dkp) + + # Create a context with the variables, memory, and plugin collection + context = KernelContext.model_construct( + variables=ContextVariables(), + memory=NullMemory(), + plugins=plugins, + ) target = CodeBlock( content="functionName", @@ -148,7 +150,7 @@ async def test_it_renders_code_block_consisting_of_just_a_var_block1(self): context = KernelContext.model_construct( variables=variables, memory=NullMemory(), - plugin_collection=None, + plugins=None, ) code_block = CodeBlock( @@ -166,7 +168,7 @@ async def test_it_renders_code_block_consisting_of_just_a_var_block2(self): context = KernelContext.model_construct( variables=variables, memory=NullMemory(), - plugin_collection=None, + plugins=None, ) code_block = CodeBlock( @@ -182,7 +184,7 @@ async def test_it_renders_code_block_consisting_of_just_a_val_block1(self): context = KernelContext.model_construct( variables=ContextVariables(), memory=NullMemory(), - plugin_collection=None, + plugins=None, ) code_block = CodeBlock( @@ -197,7 +199,7 @@ async def test_it_renders_code_block_consisting_of_just_a_val_block2(self): context = KernelContext.model_construct( variables=ContextVariables(), memory=NullMemory(), - plugin_collection=None, + plugins=None, ) code_block = CodeBlock( @@ -216,13 +218,6 @@ async def test_it_invokes_function_cloning_all_variables(self): variables["var1"] = "uno" variables["var2"] = "due" - # Create a context with the variables, memory, and plugin collection - context = KernelContext.model_construct( - variables=variables, - memory=NullMemory(), - plugin_collection=self.plugins, - ) - # Create a FunctionIdBlock with the function name func_id = FunctionIdBlock(content="funcName") @@ -252,9 +247,16 @@ def invoke(ctx): is_semantic=False, ) - # Mock the plugin collection's function retrieval - self.plugins.has_function.return_value = True - self.plugins.get_function.return_value = function + dkp = KernelPlugin(name="test", functions=[function]) + plugins = KernelPluginCollection() + plugins.add(dkp) + + # Create a context with the variables, memory, and plugin collection + context = KernelContext.model_construct( + variables=variables, + memory=NullMemory(), + plugins=plugins, + ) # Create a CodeBlock with the FunctionIdBlock and render it with the context code_block = CodeBlock( @@ -283,13 +285,6 @@ async def test_it_invokes_function_with_custom_variable(self): variables = ContextVariables() variables[VAR_NAME] = VAR_VALUE - # Create a context with the variables, memory, and plugin collection - context = KernelContext.model_construct( - variables=variables, - memory=NullMemory(), - plugin_collection=self.plugins, - ) - # Create a FunctionIdBlock with the function name and a # VarBlock with the custom variable func_id = FunctionIdBlock(content="funcName") @@ -314,9 +309,16 @@ def invoke(ctx): is_semantic=False, ) - # Mock the plugin collection's function retrieval - self.plugins.has_function.return_value = True - self.plugins.get_function.return_value = function + dkp = KernelPlugin(name="test", functions=[function]) + plugins = KernelPluginCollection() + plugins.add(dkp) + + # Create a context with the variables, memory, and plugin collection + context = KernelContext.model_construct( + variables=variables, + memory=NullMemory(), + plugins=plugins, + ) # Create a CodeBlock with the FunctionIdBlock and VarBlock, # and render it with the context @@ -336,13 +338,6 @@ async def test_it_invokes_function_with_custom_value(self): # Define a value to be used in the test VALUE = "value" - # Create a context with empty variables, memory, and plugin collection - context = KernelContext.model_construct( - variables=ContextVariables(), - memory=NullMemory(), - plugin_collection=self.plugins, - ) - # Create a FunctionIdBlock with the function name and a ValBlock with the value func_id = FunctionIdBlock(content="funcName") val_block = ValBlock(content=f"'{VALUE}'") @@ -366,9 +361,16 @@ def invoke(ctx): is_semantic=False, ) - # Mock the plugin collection's function retrieval - self.plugins.has_function.return_value = True - self.plugins.get_function.return_value = function + dkp = KernelPlugin(name="test", functions=[function]) + plugins = KernelPluginCollection() + plugins.add(dkp) + + # Create a context with empty variables, memory, and plugin collection + context = KernelContext.model_construct( + variables=ContextVariables(), + memory=NullMemory(), + plugins=plugins, + ) # Create a CodeBlock with the FunctionIdBlock and ValBlock, # and render it with the context diff --git a/python/tests/unit/template_engine/test_prompt_template_engine.py b/python/tests/unit/template_engine/test_prompt_template_engine.py index e9cf92b73029..d1bd4a9e98a5 100644 --- a/python/tests/unit/template_engine/test_prompt_template_engine.py +++ b/python/tests/unit/template_engine/test_prompt_template_engine.py @@ -9,8 +9,8 @@ from semantic_kernel.orchestration.kernel_context import KernelContext from semantic_kernel.orchestration.kernel_function import KernelFunction from semantic_kernel.plugin_definition import kernel_function -from semantic_kernel.plugin_definition.read_only_plugin_collection import ( - ReadOnlyPluginCollection, +from semantic_kernel.plugin_definition.kernel_plugin_collection import ( + KernelPluginCollection, ) from semantic_kernel.template_engine.blocks.block_types import BlockTypes from semantic_kernel.template_engine.prompt_template_engine import PromptTemplateEngine @@ -28,12 +28,12 @@ def variables(): @fixture def plugins(): - return Mock(spec=ReadOnlyPluginCollection) + return Mock(spec=KernelPluginCollection) @fixture def context(variables, plugins): - return KernelContext(variables, NullMemory(), plugins) + return KernelContext(variables=variables, memory=NullMemory(), plugins=plugins) def test_it_renders_variables(target: PromptTemplateEngine, variables: ContextVariables): diff --git a/python/tests/unit/test_kernel.py b/python/tests/unit/test_kernel.py index 9f27d37f4ba6..165e20202469 100644 --- a/python/tests/unit/test_kernel.py +++ b/python/tests/unit/test_kernel.py @@ -7,6 +7,7 @@ from semantic_kernel import Kernel from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase from semantic_kernel.plugin_definition.function_view import FunctionView +from semantic_kernel.plugin_definition.kernel_plugin import KernelPlugin def create_mock_function(name) -> KernelFunctionBase: @@ -28,7 +29,7 @@ async def test_run_async_handles_pre_invocation(pipeline_count): mock_function = create_mock_function("test_function") mock_function.invoke = AsyncMock(side_effect=lambda input, context: context) - kernel._plugin_collection.add_semantic_function(mock_function) + kernel.plugins.add(KernelPlugin(name="test", functions=[mock_function])) invoked = 0 diff --git a/python/tests/unit/test_serialization.py b/python/tests/unit/test_serialization.py index bfdbfd5acfc0..edba766972f4 100644 --- a/python/tests/unit/test_serialization.py +++ b/python/tests/unit/test_serialization.py @@ -27,17 +27,10 @@ from semantic_kernel.plugin_definition.function_view import FunctionView from semantic_kernel.plugin_definition.functions_view import FunctionsView from semantic_kernel.plugin_definition.kernel_function_decorator import kernel_function -from semantic_kernel.plugin_definition.parameter_view import ParameterView -from semantic_kernel.plugin_definition.plugin_collection import PluginCollection -from semantic_kernel.plugin_definition.plugin_collection_base import ( - PluginCollectionBase, -) -from semantic_kernel.plugin_definition.read_only_plugin_collection import ( - ReadOnlyPluginCollection, -) -from semantic_kernel.plugin_definition.read_only_plugin_collection_base import ( - ReadOnlyPluginCollectionBase, +from semantic_kernel.plugin_definition.kernel_plugin_collection import ( + KernelPluginCollection, ) +from semantic_kernel.plugin_definition.parameter_view import ParameterView from semantic_kernel.template_engine.blocks.block import Block from semantic_kernel.template_engine.blocks.block_types import BlockTypes from semantic_kernel.template_engine.blocks.code_block import CodeBlock @@ -115,10 +108,10 @@ def create_context_variables() -> ContextVariables: variables={"foo": "bar"}, ) - def create_plugin_collection() -> PluginCollection: + def create_plugin_collection() -> KernelPluginCollection: """Return a plugin collection.""" # TODO: Add a few plugins to this collection. - return PluginCollection() + return KernelPluginCollection() cls_obj_map = { Block: Block(content="foo"), @@ -146,16 +139,15 @@ def create_plugin_collection() -> PluginCollection: False, ), FunctionsView: create_functions_view(), - ReadOnlyPluginCollection: create_plugin_collection().read_only_plugin_collection, + KernelPluginCollection: create_plugin_collection(), DelegateHandlers: DelegateHandlers(), DelegateInference: DelegateInference(), ContextVariables: create_context_variables(), - PluginCollection: create_plugin_collection(), KernelContext[NullMemory]: KernelContext[NullMemory]( # TODO: Test serialization with different types of memories. variables=create_context_variables(), memory=NullMemory(), - plugin_collection=create_plugin_collection().read_only_plugin_collection, + plugins=create_plugin_collection(), ), NullMemory: NullMemory(), KernelFunction: create_kernel_function(), @@ -184,17 +176,10 @@ def constructor(cls: t.Type[_Serializable]) -> _Serializable: ] BASE_CLASSES = [ - ReadOnlyPluginCollectionBase, - PluginCollectionBase, SemanticTextMemoryBase, KernelFunctionBase, ] -# Classes that don't need serialization -UNSERIALIZED_CLASSES = [ - ReadOnlyPluginCollection, -] - STATELESS_CLASSES = [ CodeTokenizer, PromptTemplateEngine, @@ -219,8 +204,7 @@ def constructor(cls: t.Type[_Serializable]) -> _Serializable: ParameterView, FunctionView, FunctionsView, - ReadOnlyPluginCollection, - PluginCollection, + KernelPluginCollection, ContextVariables, KernelContext[NullMemory], pytest.param( @@ -233,7 +217,7 @@ def constructor(cls: t.Type[_Serializable]) -> _Serializable: class TestUsageInPydanticFields: @pytest.mark.parametrize( "kernel_type", - BASE_CLASSES + PROTOCOLS + ENUMS + PYDANTIC_MODELS + STATELESS_CLASSES + UNSERIALIZED_CLASSES, + BASE_CLASSES + PROTOCOLS + ENUMS + PYDANTIC_MODELS + STATELESS_CLASSES, ) def test_usage_as_optional_field( self,