From 6751f9d819bb2956bd72cadde84655cac43ae649 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Mon, 15 Apr 2024 15:51:59 +0200 Subject: [PATCH] Python: Updated plugins (#5827) ### Motivation and Context The loading of plugins and functions into Kernel was not fully consistent and clear, this is now simplified. Adding a plugin can now be done in two ways: - multiple: `kernel.add_plugins[plugin1, plugin2]` - single plugin: `kernel.add_plugin(plugin)` - if the plugin here is a class, with methods that have the kernel_function decorator then that is parsed into a plugin Adding a function can now be done in three ways: - multiple: `kernel.add_functions([func1, func2]` or `kernel.add_function({'func1': func1})` - single: `kernel.add_function('plugin_name', func1)` - from a prompt: `kernel.add_function(function_name='func1', prompt='test prompt', ...)` In other words, all the different methods that were available have been simplified, these methods also return the created plugin (or updated plugin for the `add_functions` method. The `add_function` (singular) method has a parameter `return_plugin` to control whether you get the created or updated plugin, instead of the created function. **An important callout:** One big internal change that this introduces is that a function that gets added to a plugin (new or existing) is automatically copied, the metadata is deep-copied and the plugin_name in the metadata is updated, so this sequence is valid now: ```python @kernel_function(name='test') def f(...): etc func = KernelFunctionFromMethod(method=f) func2 = kernel.add_function('plugin', func) assert func != func2 ``` Also closes: #5855 ### Description Removed KernelPluginCollection Updated KernelPlugin added from_... method to load from different sources Updated KernelFunctionFromPrompt added from_yaml and from_directory methods. Added and updated tests to 99% coverage of KernelPlugin and KernelFunctionFromPrompt ### Contribution Checklist - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [ ] I didn't break anyone :smile: --- .pre-commit-config.yaml | 2 +- python/README.md | 4 +- .../notebooks/03-prompt-function-inline.ipynb | 680 ++++---- .../notebooks/04-kernel-arguments-chat.ipynb | 26 +- python/notebooks/05-using-the-planner.ipynb | 1382 ++++++++--------- .../notebooks/06-memory-and-embeddings.ipynb | 1021 ++++++------ .../07-hugging-face-for-plugins.ipynb | 408 +++-- .../notebooks/08-native-function-inline.ipynb | 1355 ++++++++-------- .../weaviate-persistent-memory.ipynb | 1025 ++++++------ .../configuring_prompts.py | 4 +- .../functions_within_prompts.py | 4 +- .../samples/documentation_examples/plugin.py | 2 +- .../plugins/MathPlugin/native_function.py | 2 +- .../samples/documentation_examples/prompts.py | 28 +- .../serializing_prompts.py | 2 +- .../documentation_examples/templates.py | 2 +- .../using_the_kernel.py | 2 +- .../kernel-syntax-examples/action_planner.py | 6 +- .../azure_chat_gpt_api.py | 2 +- .../azure_chat_gpt_api_handlebars.py | 2 +- .../azure_chat_gpt_api_jinja2.py | 2 +- .../azure_chat_gpt_with_data_api.py | 2 +- ...chat_gpt_with_data_api_function_calling.py | 8 +- ...re_chat_gpt_with_data_api_vector_search.py | 6 +- .../azure_cognitive_search_memory.py | 2 +- ...penai_function_calling_stepwise_planner.py | 4 +- .../bing_plugin_examples.py | 4 +- .../bing_search_plugin.py | 6 +- python/samples/kernel-syntax-examples/chat.py | 2 +- .../kernel-syntax-examples/chat_gpt_api.py | 2 +- .../chat_gpt_api_function_calling.py | 16 +- .../configuring_prompts.py | 2 +- .../google_palm_chat_with_memory.py | 4 +- .../google_palm_chat_with_plugin.py | 2 +- .../google_search_plugin.py | 25 +- .../kernel-syntax-examples/grounded.py | 104 +- .../load_yaml_prompt.py | 6 +- .../samples/kernel-syntax-examples/memory.py | 8 +- ...penai_function_calling_stepwise_planner.py | 8 +- .../openai_logit_bias.py | 15 +- .../openai_plugin_azure_key_vault.py | 2 +- .../openai_plugin_klarna.py | 3 +- .../plugins_from_dir.py | 2 +- .../rag_with_text_memory_plugin.py | 2 +- .../resources/email_plugin/native_function.py | 2 +- .../self-critique_rag.py | 17 +- .../sequential_planner.py | 6 +- .../template_language.py | 8 +- .../connectors/ai/open_ai/utils.py | 7 +- .../connectors/openai_plugin/openai_utils.py | 3 +- .../connectors/openapi_plugin/__init__.py | 3 +- .../openapi_plugin/openapi_manager.py | 120 +- .../conversation_summary_plugin.py | 6 +- python/semantic_kernel/functions/__init__.py | 2 - .../functions/kernel_arguments.py | 11 +- .../functions/kernel_function.py | 17 + .../functions/kernel_function_from_prompt.py | 84 +- .../functions/kernel_plugin.py | 552 ++++++- .../functions/kernel_plugin_collection.py | 231 --- python/semantic_kernel/functions/types.py | 7 + python/semantic_kernel/kernel.py | 737 ++++----- .../planners/action_planner/action_planner.py | 8 +- .../semantic_kernel/planners/basic_planner.py | 8 +- .../function_calling_stepwise_planner.py | 9 +- .../planners/planner_extensions.py | 2 +- .../sequential_planner/sequential_planner.py | 2 +- .../sequential_planner_extensions.py | 2 +- .../sequential_planner_parser.py | 2 +- .../stepwise_planner/stepwise_planner.py | 9 +- .../handlebars_prompt_template.py | 6 +- .../prompt_template/jinja2_prompt_template.py | 6 +- .../prompt_template/kernel_prompt_template.py | 2 +- .../template_engine/blocks/code_block.py | 36 +- .../{native_function.py => custom_class.py} | 0 .../TestNativePluginArgs/class_args.py | 36 + .../bad.yaml} | 0 .../test_plugins/TestFunctionYaml/empty.yaml | 0 .../TestFunctionYaml/test_function.yaml | 0 .../test_function.yaml | 0 .../TestFunctionYamlJinja2/test_function.yaml | 0 .../TestMixedPlugin/TestFunction/config.json | 13 + .../TestMixedPlugin/TestFunction/skprompt.txt | 5 + .../TestMixedPlugin/native_function.py | 32 + .../TestMixedPlugin/test_function.yaml | 12 + .../TestNoFunction/something_else.txt | 0 .../TestOpenAIPlugin/akv-openai.json | 0 .../TestOpenAPIPlugin/akv-openapi.yaml | 0 .../TestFunctionConfigOnly/config.json | 13 + .../TestFunctionPromptOnly/skprompt.txt | 5 + python/tests/conftest.py | 2 + .../tests/integration/completions/conftest.py | 2 +- .../test_azure_oai_chat_service.py | 35 +- .../test_azure_oai_chat_service_extensions.py | 6 +- .../test_azure_oai_text_service.py | 4 +- .../test_conversation_summary_plugin.py | 4 +- .../completions/test_gp_chat_service.py | 2 +- .../completions/test_oai_chat_service.py | 26 +- .../completions/test_oai_text_service.py | 6 +- .../test_azure_oai_embedding_service.py | 4 +- .../embeddings/test_gp_embedding_service.py | 2 +- .../embeddings/test_hf_embedding_service.py | 2 +- .../embeddings/test_oai_embedding_service.py | 4 +- ...t_int_function_calling_stepwise_planner.py | 2 +- .../test_sequential_plan_parser.py | 6 +- .../test_sequential_planner.py | 14 +- .../stepwise_planner/test_stepwise_planner.py | 10 +- .../test_hf_local_text_completions.py | 8 +- .../unit/core_plugins/test_http_plugin.py | 2 +- .../unit/core_plugins/test_math_plugin.py | 2 +- .../unit/core_plugins/test_text_plugin.py | 4 +- .../unit/core_plugins/test_time_plugin.py | 2 +- .../test_kernel_function_from_prompt.py | 34 + .../test_kernel_plugin_collection.py | 103 -- .../unit/functions/test_kernel_plugins.py | 547 +++++-- python/tests/unit/kernel/test_kernel.py | 259 +-- .../unit/kernel/test_register_functions.py | 9 +- .../action_planner/test_action_planner.py | 312 ++-- .../test_sequential_planner.py | 75 +- .../test_sequential_planner_extensions.py | 51 +- .../test_sequential_planner_parser.py | 3 +- .../test_stepwise_planner_parse_result.py | 10 +- .../tests/unit/planners/test_plan_creation.py | 49 +- .../unit/planners/test_plan_execution.py | 59 +- .../test_handlebars_prompt_template.py | 4 +- .../test_handlebars_prompt_template_e2e.py | 8 +- .../test_jinja2_prompt_template.py | 4 +- .../test_jinja2_prompt_template_e2e.py | 8 +- .../test_kernel_prompt_template.py | 31 +- .../test_prompt_template_e2e.py | 35 +- .../template_engine/blocks/test_code_block.py | 68 +- python/tests/unit/test_serialization.py | 14 +- 131 files changed, 5127 insertions(+), 4922 deletions(-) delete mode 100644 python/semantic_kernel/functions/kernel_plugin_collection.py create mode 100644 python/semantic_kernel/functions/types.py rename python/tests/assets/test_native_plugins/TestNativePlugin/{native_function.py => custom_class.py} (100%) create mode 100644 python/tests/assets/test_native_plugins/TestNativePluginArgs/class_args.py rename python/tests/assets/test_plugins/{TestPlugin/TestNoFunction/something_else.txt => TestFunctionBadYaml/bad.yaml} (100%) create mode 100644 python/tests/assets/test_plugins/TestFunctionYaml/empty.yaml rename python/tests/assets/test_plugins/{TestPlugin => }/TestFunctionYaml/test_function.yaml (100%) rename python/tests/assets/test_plugins/{TestPlugin => }/TestFunctionYamlHandlebars/test_function.yaml (100%) rename python/tests/assets/test_plugins/{TestPlugin => }/TestFunctionYamlJinja2/test_function.yaml (100%) create mode 100644 python/tests/assets/test_plugins/TestMixedPlugin/TestFunction/config.json create mode 100644 python/tests/assets/test_plugins/TestMixedPlugin/TestFunction/skprompt.txt create mode 100644 python/tests/assets/test_plugins/TestMixedPlugin/native_function.py create mode 100644 python/tests/assets/test_plugins/TestMixedPlugin/test_function.yaml create mode 100644 python/tests/assets/test_plugins/TestNoFunction/something_else.txt rename python/tests/assets/test_plugins/{TestPlugin => }/TestOpenAIPlugin/akv-openai.json (100%) rename python/tests/assets/test_plugins/{TestPlugin => }/TestOpenAPIPlugin/akv-openapi.yaml (100%) create mode 100644 python/tests/assets/test_plugins/TestPlugin/TestFunctionConfigOnly/config.json create mode 100644 python/tests/assets/test_plugins/TestPlugin/TestFunctionPromptOnly/skprompt.txt delete mode 100644 python/tests/unit/functions/test_kernel_plugin_collection.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8ebce0cb85..127f3fdf3e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: - id: black files: \.py$ - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.4 + rev: v0.3.5 hooks: - id: ruff args: [ --fix, --exit-non-zero-on-fix ] diff --git a/python/README.md b/python/README.md index 2b20d86efb..582ccc1797 100644 --- a/python/README.md +++ b/python/README.md @@ -87,7 +87,7 @@ prompt_template_config = sk.PromptTemplateConfig( execution_settings=req_settings, ) -function = kernel.create_function_from_prompt( +function = kernel.add_function( function_name="tldr_function", plugin_name="tldr_plugin", prompt_template_config=prompt_template_config, @@ -107,7 +107,7 @@ if __name__ == "__main__": ```python # Create a reusable function summarize function -summarize = kernel.create_function_from_prompt( +summarize = kernel.add_function( function_name="tldr_function", plugin_name="tldr_plugin", prompt="{{$input}}\n\nOne line TLDR with the fewest words.", diff --git a/python/notebooks/03-prompt-function-inline.ipynb b/python/notebooks/03-prompt-function-inline.ipynb index aa41e9e90e..7d02a80f1d 100644 --- a/python/notebooks/03-prompt-function-inline.ipynb +++ b/python/notebooks/03-prompt-function-inline.ipynb @@ -1,344 +1,340 @@ { - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "id": "3c93ac5b", - "metadata": {}, - "source": [ - "# Running Prompt Functions Inline" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "40201641", - "metadata": {}, - "source": [ - "The [previous notebook](./02-running-prompts-from-file.ipynb)\n", - "showed how to define a semantic function using a prompt template stored on a file.\n", - "\n", - "In this notebook, we'll show how to use the Semantic Kernel to define functions inline with your python code. This can be useful in a few scenarios:\n", - "\n", - "* Dynamically generating the prompt using complex rules at runtime\n", - "* Writing prompts by editing Python code instead of TXT files.\n", - "* Easily creating demos, like this document\n", - "\n", - "Prompt templates are defined using the SK template language, which allows to reference variables and functions. Read [this doc](https://aka.ms/sk/howto/configurefunction) to learn more about the design decisions for prompt templating. \n", - "\n", - "For now we'll use only the `{{$input}}` variable, and see more complex templates later.\n", - "\n", - "Almost all semantic function prompts have a reference to `{{$input}}`, which is the default way\n", - "a user can import content from the context variables." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "d90b0c13", - "metadata": {}, - "source": [ - "Prepare a semantic kernel instance first, loading also the AI service settings defined in the [Setup notebook](00-getting-started.ipynb):" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1da651d4", - "metadata": {}, - "outputs": [], - "source": [ - "!python -m pip install semantic-kernel==0.9.5b1" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "68b770df", - "metadata": {}, - "outputs": [], - "source": [ - "from services import Service\n", - "\n", - "# Select a service to use for this notebook (available services: OpenAI, AzureOpenAI, HuggingFace)\n", - "selectedService = Service.OpenAI" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3712b7c3", - "metadata": {}, - "outputs": [], - "source": [ - "import semantic_kernel as sk\n", - "import semantic_kernel.connectors.ai.open_ai as sk_oai\n", - "from semantic_kernel.prompt_template.input_variable import InputVariable\n", - "\n", - "kernel = sk.Kernel()\n", - "\n", - "service_id = None\n", - "if selectedService == Service.OpenAI:\n", - " from semantic_kernel.connectors.ai.open_ai import OpenAITextCompletion\n", - "\n", - " api_key, org_id = sk.openai_settings_from_dot_env()\n", - " service_id = \"oai_text_completion\"\n", - " kernel.add_service(\n", - " OpenAITextCompletion(\n", - " service_id=service_id, ai_model_id=\"gpt-3.5-turbo-instruct\", api_key=api_key, org_id=org_id\n", - " ),\n", - " )\n", - "elif selectedService == Service.AzureOpenAI:\n", - " from semantic_kernel.connectors.ai.open_ai import AzureTextCompletion\n", - "\n", - " deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env()\n", - " service_id = \"aoai_text_completion\"\n", - " kernel.add_service(\n", - " AzureTextCompletion(service_id=service_id, deployment_name=deployment, endpoint=endpoint, api_key=api_key),\n", - " )" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "589733c5", - "metadata": {}, - "source": [ - "Let's use a prompt to create a semantic function used to summarize content, allowing for some creativity and a sufficient number of tokens.\n", - "\n", - "The function will take in input the text to summarize." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ae29c207", - "metadata": {}, - "outputs": [], - "source": [ - "prompt = \"\"\"{{$input}}\n", - "Summarize the content above.\n", - "\"\"\"\n", - "\n", - "if selectedService == Service.OpenAI:\n", - " execution_settings = sk_oai.OpenAITextPromptExecutionSettings(\n", - " service_id=service_id,\n", - " ai_model_id=\"gpt-3.5-turbo-instruct\",\n", - " max_tokens=2000,\n", - " temperature=0.7,\n", - " )\n", - "elif selectedService == Service.AzureOpenAI:\n", - " execution_settings = sk_oai.OpenAITextPromptExecutionSettings(\n", - " service_id=service_id,\n", - " ai_model_id=deployment,\n", - " max_tokens=2000,\n", - " temperature=0.7,\n", - " )\n", - "\n", - "prompt_template_config = sk.PromptTemplateConfig(\n", - " template=prompt,\n", - " name=\"summarize\",\n", - " template_format=\"semantic-kernel\",\n", - " input_variables=[\n", - " InputVariable(name=\"input\", description=\"The user input\", is_required=True),\n", - " ],\n", - " execution_settings=execution_settings,\n", - ")\n", - "\n", - "summarize = kernel.create_function_from_prompt(\n", - " function_name=\"summarizeFunc\",\n", - " plugin_name=\"summarizePlugin\",\n", - " prompt_template_config=prompt_template_config,\n", - ")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "f26b90c4", - "metadata": {}, - "source": [ - "Set up some content to summarize, here's an extract about Demo, an ancient Greek poet, taken from Wikipedia (https://en.wikipedia.org/wiki/Demo_(ancient_Greek_poet))." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "314557fb", - "metadata": {}, - "outputs": [], - "source": [ - "input_text = \"\"\"\n", - "Demo (ancient Greek poet)\n", - "From Wikipedia, the free encyclopedia\n", - "Demo or Damo (Greek: Δεμώ, Δαμώ; fl. c. AD 200) was a Greek woman of the Roman period, known for a single epigram, engraved upon the Colossus of Memnon, which bears her name. She speaks of herself therein as a lyric poetess dedicated to the Muses, but nothing is known of her life.[1]\n", - "Identity\n", - "Demo was evidently Greek, as her name, a traditional epithet of Demeter, signifies. The name was relatively common in the Hellenistic world, in Egypt and elsewhere, and she cannot be further identified. The date of her visit to the Colossus of Memnon cannot be established with certainty, but internal evidence on the left leg suggests her poem was inscribed there at some point in or after AD 196.[2]\n", - "Epigram\n", - "There are a number of graffiti inscriptions on the Colossus of Memnon. Following three epigrams by Julia Balbilla, a fourth epigram, in elegiac couplets, entitled and presumably authored by \"Demo\" or \"Damo\" (the Greek inscription is difficult to read), is a dedication to the Muses.[2] The poem is traditionally published with the works of Balbilla, though the internal evidence suggests a different author.[1]\n", - "In the poem, Demo explains that Memnon has shown her special respect. In return, Demo offers the gift for poetry, as a gift to the hero. At the end of this epigram, she addresses Memnon, highlighting his divine status by recalling his strength and holiness.[2]\n", - "Demo, like Julia Balbilla, writes in the artificial and poetic Aeolic dialect. The language indicates she was knowledgeable in Homeric poetry—'bearing a pleasant gift', for example, alludes to the use of that phrase throughout the Iliad and Odyssey.[a][2] \n", - "\"\"\"" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "bf0f2330", - "metadata": {}, - "source": [ - "...and run the summary function:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7b0e3b0c", - "metadata": {}, - "outputs": [], - "source": [ - "summary = await kernel.invoke(summarize, sk.KernelArguments(input=input_text))\n", - "\n", - "print(summary)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "1c2c1262", - "metadata": {}, - "source": [ - "# Using ChatCompletion for Semantic Plugins" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "29b59b28", - "metadata": {}, - "source": [ - "You can also use chat completion models (like `gpt-35-turbo` and `gpt4`) for creating plugins. Normally you would have to tweak the API to accommodate for a system and user role, but SK abstracts that away for you by using `kernel.add_service` and `AzureChatCompletion` or `OpenAIChatCompletion`" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "4777f447", - "metadata": {}, - "source": [ - "Here's one more example of how to write an inline Semantic Function that gives a TLDR for a piece of text using a ChatCompletion model\n", - "\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c5886aeb", - "metadata": {}, - "outputs": [], - "source": [ - "kernel = sk.Kernel()\n", - "\n", - "service_id = None\n", - "if selectedService == Service.OpenAI:\n", - " from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion\n", - "\n", - " api_key, org_id = sk.openai_settings_from_dot_env()\n", - " service_id = \"oai_chat_gpt\"\n", - " kernel.add_service(\n", - " OpenAIChatCompletion(service_id=service_id, ai_model_id=\"gpt-3.5-turbo-1106\", api_key=api_key, org_id=org_id),\n", - " )\n", - "elif selectedService == Service.AzureOpenAI:\n", - " from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion\n", - "\n", - " deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env()\n", - " service_id = \"aoai_chat_completion\"\n", - " kernel.add_service(\n", - " AzureChatCompletion(service_id=service_id, deployment_name=deployment, endpoint=endpoint, api_key=api_key),\n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ea8128c8", - "metadata": {}, - "outputs": [], - "source": [ - "prompt = \"\"\"\n", - "{{$input}}\n", - "\n", - "Give me the TLDR in 5 words or less.\n", - "\"\"\"\n", - "\n", - "text = \"\"\"\n", - " 1) A robot may not injure a human being or, through inaction,\n", - " allow a human being to come to harm.\n", - "\n", - " 2) A robot must obey orders given it by human beings except where\n", - " such orders would conflict with the First Law.\n", - "\n", - " 3) A robot must protect its own existence as long as such protection\n", - " does not conflict with the First or Second Law.\n", - "\"\"\"\n", - "\n", - "if selectedService == Service.OpenAI:\n", - " execution_settings = sk_oai.OpenAIChatPromptExecutionSettings(\n", - " service_id=service_id,\n", - " ai_model_id=\"gpt-3.5-turbo-1106\",\n", - " max_tokens=2000,\n", - " temperature=0.7,\n", - " )\n", - "elif selectedService == Service.AzureOpenAI:\n", - " execution_settings = sk_oai.OpenAIChatPromptExecutionSettings(\n", - " service_id=service_id,\n", - " ai_model_id=deployment,\n", - " max_tokens=2000,\n", - " temperature=0.7,\n", - " )\n", - "\n", - "prompt_template_config = sk.PromptTemplateConfig(\n", - " template=prompt,\n", - " name=\"tldr\",\n", - " template_format=\"semantic-kernel\",\n", - " input_variables=[\n", - " InputVariable(name=\"input\", description=\"The user input\", is_required=True),\n", - " ],\n", - " execution_settings=execution_settings,\n", - ")\n", - "\n", - "tldr_function = kernel.create_function_from_prompt(\n", - " function_name=\"tldrFunction\",\n", - " plugin_name=\"tldrPlugin\",\n", - " prompt_template_config=prompt_template_config,\n", - ")\n", - "\n", - "summary = await kernel.invoke(tldr_function, sk.KernelArguments(input=text))\n", - "\n", - "print(f\"Output: {summary}\")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.12" - } - }, - "nbformat": 4, - "nbformat_minor": 5 + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "id": "3c93ac5b", + "metadata": {}, + "source": [ + "# Running Prompt Functions Inline\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "40201641", + "metadata": {}, + "source": [ + "The [previous notebook](./02-running-prompts-from-file.ipynb)\n", + "showed how to define a semantic function using a prompt template stored on a file.\n", + "\n", + "In this notebook, we'll show how to use the Semantic Kernel to define functions inline with your python code. This can be useful in a few scenarios:\n", + "\n", + "- Dynamically generating the prompt using complex rules at runtime\n", + "- Writing prompts by editing Python code instead of TXT files.\n", + "- Easily creating demos, like this document\n", + "\n", + "Prompt templates are defined using the SK template language, which allows to reference variables and functions. Read [this doc](https://aka.ms/sk/howto/configurefunction) to learn more about the design decisions for prompt templating.\n", + "\n", + "For now we'll use only the `{{$input}}` variable, and see more complex templates later.\n", + "\n", + "Almost all semantic function prompts have a reference to `{{$input}}`, which is the default way\n", + "a user can import content from the context variables.\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "d90b0c13", + "metadata": {}, + "source": [ + "Prepare a semantic kernel instance first, loading also the AI service settings defined in the [Setup notebook](00-getting-started.ipynb):\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1da651d4", + "metadata": {}, + "outputs": [], + "source": [ + "!python -m pip install semantic-kernel==0.9.5b1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "68b770df", + "metadata": {}, + "outputs": [], + "source": [ + "from services import Service\n", + "\n", + "# Select a service to use for this notebook (available services: OpenAI, AzureOpenAI, HuggingFace)\n", + "selectedService = Service.OpenAI" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3712b7c3", + "metadata": {}, + "outputs": [], + "source": [ + "import semantic_kernel as sk\n", + "\n", + "kernel = sk.Kernel()\n", + "\n", + "service_id = None\n", + "if selectedService == Service.OpenAI:\n", + " from semantic_kernel.connectors.ai.open_ai import OpenAITextCompletion\n", + "\n", + " api_key, org_id = sk.openai_settings_from_dot_env()\n", + " service_id = \"oai_text_completion\"\n", + " kernel.add_service(\n", + " OpenAITextCompletion(\n", + " service_id=service_id, ai_model_id=\"gpt-3.5-turbo-instruct\", api_key=api_key, org_id=org_id\n", + " ),\n", + " )\n", + "elif selectedService == Service.AzureOpenAI:\n", + " from semantic_kernel.connectors.ai.open_ai import AzureTextCompletion\n", + "\n", + " deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env()\n", + " service_id = \"aoai_text_completion\"\n", + " kernel.add_service(\n", + " AzureTextCompletion(service_id=service_id, deployment_name=deployment, endpoint=endpoint, api_key=api_key),\n", + " )" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "589733c5", + "metadata": {}, + "source": [ + "Let's use a prompt to create a semantic function used to summarize content, allowing for some creativity and a sufficient number of tokens.\n", + "\n", + "The function will take in input the text to summarize.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ae29c207", + "metadata": {}, + "outputs": [], + "source": [ + "prompt = \"\"\"{{$input}}\n", + "Summarize the content above.\n", + "\"\"\"\n", + "\n", + "if selectedService == Service.OpenAI:\n", + " execution_settings = sk_oai.OpenAITextPromptExecutionSettings(\n", + " service_id=service_id,\n", + " ai_model_id=\"gpt-3.5-turbo-instruct\",\n", + " max_tokens=2000,\n", + " temperature=0.7,\n", + " )\n", + "elif selectedService == Service.AzureOpenAI:\n", + " execution_settings = sk_oai.OpenAITextPromptExecutionSettings(\n", + " service_id=service_id,\n", + " ai_model_id=deployment,\n", + " max_tokens=2000,\n", + " temperature=0.7,\n", + " )\n", + "\n", + "prompt_template_config = sk.PromptTemplateConfig(\n", + " template=prompt,\n", + " name=\"summarize\",\n", + " template_format=\"semantic-kernel\",\n", + " input_variables=[\n", + " InputVariable(name=\"input\", description=\"The user input\", is_required=True),\n", + " ],\n", + " execution_settings=execution_settings,\n", + ")\n", + "\n", + "summarize = kernel.add_function(\n", + " function_name=\"summarizeFunc\",\n", + " plugin_name=\"summarizePlugin\",\n", + " prompt_template_config=prompt_template_config,\n", + ")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "f26b90c4", + "metadata": {}, + "source": [ + "Set up some content to summarize, here's an extract about Demo, an ancient Greek poet, taken from Wikipedia (https://en.wikipedia.org/wiki/Demo_(ancient_Greek_poet)).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "314557fb", + "metadata": {}, + "outputs": [], + "source": [ + "input_text = \"\"\"\n", + "Demo (ancient Greek poet)\n", + "From Wikipedia, the free encyclopedia\n", + "Demo or Damo (Greek: Δεμώ, Δαμώ; fl. c. AD 200) was a Greek woman of the Roman period, known for a single epigram, engraved upon the Colossus of Memnon, which bears her name. She speaks of herself therein as a lyric poetess dedicated to the Muses, but nothing is known of her life.[1]\n", + "Identity\n", + "Demo was evidently Greek, as her name, a traditional epithet of Demeter, signifies. The name was relatively common in the Hellenistic world, in Egypt and elsewhere, and she cannot be further identified. The date of her visit to the Colossus of Memnon cannot be established with certainty, but internal evidence on the left leg suggests her poem was inscribed there at some point in or after AD 196.[2]\n", + "Epigram\n", + "There are a number of graffiti inscriptions on the Colossus of Memnon. Following three epigrams by Julia Balbilla, a fourth epigram, in elegiac couplets, entitled and presumably authored by \"Demo\" or \"Damo\" (the Greek inscription is difficult to read), is a dedication to the Muses.[2] The poem is traditionally published with the works of Balbilla, though the internal evidence suggests a different author.[1]\n", + "In the poem, Demo explains that Memnon has shown her special respect. In return, Demo offers the gift for poetry, as a gift to the hero. At the end of this epigram, she addresses Memnon, highlighting his divine status by recalling his strength and holiness.[2]\n", + "Demo, like Julia Balbilla, writes in the artificial and poetic Aeolic dialect. The language indicates she was knowledgeable in Homeric poetry—'bearing a pleasant gift', for example, alludes to the use of that phrase throughout the Iliad and Odyssey.[a][2] \n", + "\"\"\"" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "bf0f2330", + "metadata": {}, + "source": [ + "...and run the summary function:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7b0e3b0c", + "metadata": {}, + "outputs": [], + "source": [ + "summary = await kernel.invoke(summarize, sk.KernelArguments(input=input_text))\n", + "\n", + "print(summary)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "1c2c1262", + "metadata": {}, + "source": [ + "# Using ChatCompletion for Semantic Plugins\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "29b59b28", + "metadata": {}, + "source": [ + "You can also use chat completion models (like `gpt-35-turbo` and `gpt4`) for creating plugins. Normally you would have to tweak the API to accommodate for a system and user role, but SK abstracts that away for you by using `kernel.add_service` and `AzureChatCompletion` or `OpenAIChatCompletion`\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "4777f447", + "metadata": {}, + "source": [ + "Here's one more example of how to write an inline Semantic Function that gives a TLDR for a piece of text using a ChatCompletion model\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c5886aeb", + "metadata": {}, + "outputs": [], + "source": [ + "kernel = sk.Kernel()\n", + "\n", + "service_id = None\n", + "if selectedService == Service.OpenAI:\n", + " from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion\n", + "\n", + " api_key, org_id = sk.openai_settings_from_dot_env()\n", + " service_id = \"oai_chat_gpt\"\n", + " kernel.add_service(\n", + " OpenAIChatCompletion(service_id=service_id, ai_model_id=\"gpt-3.5-turbo-1106\", api_key=api_key, org_id=org_id),\n", + " )\n", + "elif selectedService == Service.AzureOpenAI:\n", + " from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion\n", + "\n", + " deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env()\n", + " service_id = \"aoai_chat_completion\"\n", + " kernel.add_service(\n", + " AzureChatCompletion(service_id=service_id, deployment_name=deployment, endpoint=endpoint, api_key=api_key),\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ea8128c8", + "metadata": {}, + "outputs": [], + "source": [ + "prompt = \"\"\"\n", + "{{$input}}\n", + "\n", + "Give me the TLDR in 5 words or less.\n", + "\"\"\"\n", + "\n", + "text = \"\"\"\n", + " 1) A robot may not injure a human being or, through inaction,\n", + " allow a human being to come to harm.\n", + "\n", + " 2) A robot must obey orders given it by human beings except where\n", + " such orders would conflict with the First Law.\n", + "\n", + " 3) A robot must protect its own existence as long as such protection\n", + " does not conflict with the First or Second Law.\n", + "\"\"\"\n", + "\n", + "if selectedService == Service.OpenAI:\n", + " execution_settings = sk_oai.OpenAIChatPromptExecutionSettings(\n", + " service_id=service_id,\n", + " ai_model_id=\"gpt-3.5-turbo-1106\",\n", + " max_tokens=2000,\n", + " temperature=0.7,\n", + " )\n", + "elif selectedService == Service.AzureOpenAI:\n", + " execution_settings = sk_oai.OpenAIChatPromptExecutionSettings(\n", + " service_id=service_id,\n", + " ai_model_id=deployment,\n", + " max_tokens=2000,\n", + " temperature=0.7,\n", + " )\n", + "\n", + "prompt_template_config = sk.PromptTemplateConfig(\n", + " template=prompt,\n", + " name=\"tldr\",\n", + " template_format=\"semantic-kernel\",\n", + " input_variables=[\n", + " InputVariable(name=\"input\", description=\"The user input\", is_required=True),\n", + " ],\n", + " execution_settings=execution_settings,\n", + ")\n", + "\n", + "tldr_function = kernel.add_function(\n", + " function_name=\"tldrFunction\",\n", + " plugin_name=\"tldrPlugin\",\n", + " prompt_template_config=prompt_template_config,\n", + ")\n", + "\n", + "summary = await kernel.invoke(tldr_function, sk.KernelArguments(input=text))\n", + "\n", + "print(f\"Output: {summary}\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 } diff --git a/python/notebooks/04-kernel-arguments-chat.ipynb b/python/notebooks/04-kernel-arguments-chat.ipynb index e5f2a96c1f..40165a1063 100644 --- a/python/notebooks/04-kernel-arguments-chat.ipynb +++ b/python/notebooks/04-kernel-arguments-chat.ipynb @@ -8,15 +8,15 @@ "source": [ "# Creating a basic chat experience with kernel arguments\n", "\n", - "In this example, we show how you can build a simple chat bot by sending and updating the kernel arguments with your requests. \n", + "In this example, we show how you can build a simple chat bot by sending and updating the kernel arguments with your requests.\n", "\n", "We introduce the Kernel Arguments object which in this demo functions similarly as a key-value store that you can use when running the kernel.\n", "\n", "The chat history is local (i.e. in your computer's RAM) and not persisted anywhere beyond the life of this Jupyter session.\n", "\n", - "In future examples, we will show how to persist the chat history on disk so that you can bring it into your applications. \n", + "In future examples, we will show how to persist the chat history on disk so that you can bring it into your applications.\n", "\n", - "In this chat scenario, as the user talks back and forth with the bot, the chat context gets populated with the history of the conversation. During each new run of the kernel, the kernel arguments and chat history can provide the AI with its variables' content. " + "In this chat scenario, as the user talks back and forth with the bot, the chat context gets populated with the history of the conversation. During each new run of the kernel, the kernel arguments and chat history can provide the AI with its variables' content.\n" ] }, { @@ -50,10 +50,6 @@ "outputs": [], "source": [ "import semantic_kernel as sk\n", - "import semantic_kernel.connectors.ai.open_ai as sk_oai\n", - "from semantic_kernel.prompt_template.input_variable import InputVariable\n", - "from semantic_kernel.contents.chat_history import ChatHistory\n", - "from semantic_kernel.functions.kernel_arguments import KernelArguments\n", "\n", "kernel = sk.Kernel()\n", "\n", @@ -82,7 +78,7 @@ "id": "7971783d", "metadata": {}, "source": [ - "Let's define a prompt outlining a dialogue chat bot." + "Let's define a prompt outlining a dialogue chat bot.\n" ] }, { @@ -107,7 +103,7 @@ "id": "61716b16", "metadata": {}, "source": [ - "Register your semantic function" + "Register your semantic function\n" ] }, { @@ -143,7 +139,7 @@ " execution_settings=execution_settings,\n", ")\n", "\n", - "chat_function = kernel.create_function_from_prompt(\n", + "chat_function = kernel.add_function(\n", " function_name=\"chat\",\n", " plugin_name=\"chatPlugin\",\n", " prompt_template_config=prompt_template_config,\n", @@ -167,7 +163,7 @@ "id": "6e8a676f", "metadata": {}, "source": [ - "Initialize the Kernel Arguments" + "Initialize the Kernel Arguments\n" ] }, { @@ -186,7 +182,7 @@ "id": "4ce7c497", "metadata": {}, "source": [ - "Chat with the Bot" + "Chat with the Bot\n" ] }, { @@ -206,7 +202,7 @@ "id": "a5b03748", "metadata": {}, "source": [ - "Update the history with the output" + "Update the history with the output\n" ] }, { @@ -225,7 +221,7 @@ "id": "23a2eb02", "metadata": {}, "source": [ - "Keep Chatting!" + "Keep Chatting!\n" ] }, { @@ -295,7 +291,7 @@ "id": "c30bac97", "metadata": {}, "source": [ - "After chatting for a while, we have built a growing history, which we are attaching to each prompt and which contains the full conversation. Let's take a look!" + "After chatting for a while, we have built a growing history, which we are attaching to each prompt and which contains the full conversation. Let's take a look!\n" ] }, { diff --git a/python/notebooks/05-using-the-planner.ipynb b/python/notebooks/05-using-the-planner.ipynb index 84ebe0878e..23f9d89374 100644 --- a/python/notebooks/05-using-the-planner.ipynb +++ b/python/notebooks/05-using-the-planner.ipynb @@ -1,693 +1,693 @@ { - "cells": [ - { - "cell_type": "markdown", - "id": "99a80181", - "metadata": {}, - "source": [ - "# Introduction to the Planner\n", - "\n", - "The Planner is one of the fundamental concepts of the Semantic Kernel.\n", - "\n", - "It makes use of the collection of native and semantic functions that have been registered to the kernel and using AI, will formulate a plan to execute the given ask.\n", - "\n", - "From our own testing, planner works best with more powerful models like `gpt4` but sometimes you might get working plans with cheaper models like `gpt-35-turbo`. We encourage you to implement your own versions of the planner and use different models that fit your user needs.\n", - "\n", - "Read more about planner [here](https://aka.ms/sk/concepts/planner)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "07eb35d2", - "metadata": {}, - "outputs": [], - "source": [ - "!python -m pip install -U semantic-kernel==0.9.5b1" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7d548e40", - "metadata": {}, - "outputs": [], - "source": [ - "from services import Service\n", - "\n", - "# Select a service to use for this notebook (available services: OpenAI, AzureOpenAI, HuggingFace)\n", - "selectedService = Service.AzureOpenAI" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3852961c", - "metadata": {}, - "outputs": [], - "source": [ - "from semantic_kernel.contents.chat_history import ChatHistory # noqa: F401\n", - "from semantic_kernel.functions.kernel_arguments import KernelArguments # noqa: F401\n", - "from semantic_kernel.prompt_template.input_variable import InputVariable # noqa: F401" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "11e59885", - "metadata": {}, - "outputs": [], - "source": [ - "import semantic_kernel as sk\n", - "import semantic_kernel.connectors.ai.open_ai as sk_oai # noqa: F401\n", - "\n", - "kernel = sk.Kernel()\n", - "\n", - "service_id = None\n", - "if selectedService == Service.OpenAI:\n", - " api_key, org_id = sk.openai_settings_from_dot_env()\n", - " service_id = \"default\"\n", - " kernel.add_service(\n", - " sk_oai.OpenAIChatCompletion(\n", - " service_id=service_id, ai_model_id=\"gpt-3.5-turbo-1106\", api_key=api_key, org_id=org_id\n", - " ),\n", - " )\n", - "elif selectedService == Service.AzureOpenAI:\n", - " deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env()\n", - " service_id = \"default\"\n", - " kernel.add_service(\n", - " sk_oai.AzureChatCompletion(\n", - " service_id=service_id, deployment_name=deployment, endpoint=endpoint, api_key=api_key\n", - " ),\n", - " )" - ] - }, - { - "cell_type": "markdown", - "id": "4ff28070", - "metadata": {}, - "source": [ - "## It all begins with an ask\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "93bc6103", - "metadata": {}, - "outputs": [], - "source": [ - "ask = \"\"\"\n", - "Tomorrow is Valentine's day. I need to come up with a few date ideas. She speaks French so write it in French.\n", - "Convert the text to uppercase\"\"\"" - ] - }, - { - "cell_type": "markdown", - "id": "a5d86739", - "metadata": {}, - "source": [ - "### Providing plugins to the planner\n", - "\n", - "The planner needs to know what plugins are available to it. Here we'll give it access to the `SummarizePlugin` and `WriterPlugin` we have defined on disk. This will include many semantic functions, of which the planner will intelligently choose a subset.\n", - "\n", - "You can also include native functions as well. Here we'll add the TextPlugin.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ca0e7604", - "metadata": {}, - "outputs": [], - "source": [ - "from semantic_kernel.core_plugins.text_plugin import TextPlugin\n", - "\n", - "plugins_directory = \"../../samples/plugins/\"\n", - "summarize_plugin = kernel.import_plugin_from_prompt_directory(plugins_directory, \"SummarizePlugin\")\n", - "writer_plugin = kernel.import_plugin_from_prompt_directory(plugins_directory, \"WriterPlugin\")\n", - "text_plugin = kernel.import_plugin_from_object(TextPlugin(), \"TextPlugin\")" - ] - }, - { - "cell_type": "markdown", - "id": "deff5675", - "metadata": {}, - "source": [ - "Define your ASK. What do you want the Kernel to do?\n" - ] - }, - { - "cell_type": "markdown", - "id": "eee6fe7b", - "metadata": {}, - "source": [ - "# Basic Planner\n" - ] - }, - { - "cell_type": "markdown", - "id": "590a22f2", - "metadata": {}, - "source": [ - "Let's start by taking a look at a basic planner. The `BasicPlanner` produces a JSON-based plan that aims to solve the provided ask sequentially and evaluated in order.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "20d35ed0", - "metadata": {}, - "outputs": [], - "source": [ - "from semantic_kernel.planners.basic_planner import BasicPlanner\n", - "\n", - "planner = BasicPlanner(service_id)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d5697c09", - "metadata": {}, - "outputs": [], - "source": [ - "basic_plan = await planner.create_plan(ask, kernel)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b425ba1e", - "metadata": {}, - "outputs": [], - "source": [ - "print(basic_plan.generated_plan)" - ] - }, - { - "cell_type": "markdown", - "id": "0f3a48f8", - "metadata": {}, - "source": [ - "You can see that the Planner took my ask and converted it into an JSON-based plan detailing how the AI would go about solving this task, making use of the plugins that the Kernel has available to it.\n", - "\n", - "As you can see in the above plan, the AI has determined which functions to call in order to fulfill the user ask. The output of each step of the plan becomes the input to the next function.\n" - ] - }, - { - "cell_type": "markdown", - "id": "cd4df0c2", - "metadata": {}, - "source": [ - "Let's also define an inline plugin and have it be available to the Planner. Be sure to give it a function name and plugin name.\n" - ] - }, - { - "cell_type": "markdown", - "id": "5057cf9b", - "metadata": {}, - "source": [ - "Let's update our ask using this new plugin\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a3161dcf", - "metadata": {}, - "outputs": [], - "source": [ - "from semantic_kernel.functions.kernel_function_from_prompt import KernelFunctionFromPrompt\n", - "\n", - "kernel = sk.Kernel()\n", - "service_id = \"default\"\n", - "if selectedService == Service.OpenAI:\n", - " api_key, org_id = sk.openai_settings_from_dot_env()\n", - " kernel.add_service(\n", - " sk_oai.OpenAIChatCompletion(\n", - " service_id=service_id, ai_model_id=\"gpt-3.5-turbo-1106\", api_key=api_key, org_id=org_id\n", - " ),\n", - " )\n", - "elif selectedService == Service.AzureOpenAI:\n", - " deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env()\n", - " kernel.add_service(\n", - " sk_oai.AzureChatCompletion(\n", - " service_id=service_id, deployment_name=deployment, endpoint=endpoint, api_key=api_key\n", - " ),\n", - " )\n", - "\n", - "plugins_directory = \"../../samples/plugins/\"\n", - "summarize_plugin = kernel.import_plugin_from_prompt_directory(plugins_directory, \"SummarizePlugin\")\n", - "writer_plugin = kernel.import_plugin_from_prompt_directory(plugins_directory, \"WriterPlugin\")\n", - "text_plugin = kernel.import_plugin_from_object(TextPlugin(), \"TextPlugin\")\n", - "\n", - "shakespeare_func = KernelFunctionFromPrompt(\n", - " function_name=\"Shakespeare\",\n", - " plugin_name=\"WriterPlugin\",\n", - " prompt=\"\"\"\n", - "{{$input}}\n", - "\n", - "Rewrite the above in the style of Shakespeare.\n", - "\"\"\",\n", - " prompt_execution_settings=sk_oai.OpenAIChatPromptExecutionSettings(\n", - " service_id=service_id,\n", - " max_tokens=2000,\n", - " temperature=0.8,\n", - " ),\n", - ")\n", - "kernel.plugins.add_functions_to_plugin([shakespeare_func], \"WriterPlugin\")\n", - "\n", - "for plugin in kernel.plugins:\n", - " for function in plugin.functions.values():\n", - " print(f\"Plugin: {plugin.name}, Function: {function.name}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "25abac0d", - "metadata": {}, - "outputs": [], - "source": [ - "planner = BasicPlanner(service_id)\n", - "\n", - "ask = \"\"\"\n", - "Tomorrow is Valentine's day. I need to come up with a few short poems.\n", - "She likes Shakespeare so write using his style. She speaks French so write it in French.\n", - "Convert the text to uppercase.\"\"\"\n", - "\n", - "new_plan = await planner.create_plan(goal=ask, kernel=kernel)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "997462e8", - "metadata": {}, - "outputs": [], - "source": [ - "print(new_plan.generated_plan)" - ] - }, - { - "cell_type": "markdown", - "id": "b67a052e", - "metadata": {}, - "source": [ - "### Executing the plan\n" - ] - }, - { - "cell_type": "markdown", - "id": "3b839c90", - "metadata": {}, - "source": [ - "Now that we have a plan, let's try to execute it! The Planner has a function called `execute_plan`.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9384831a", - "metadata": {}, - "outputs": [], - "source": [ - "results = await planner.execute_plan(new_plan, kernel)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9192b186", - "metadata": {}, - "outputs": [], - "source": [ - "print(results)" - ] - }, - { - "cell_type": "markdown", - "id": "e8a9b6b7", - "metadata": {}, - "source": [ - "# The Plan Object Model\n" - ] - }, - { - "cell_type": "markdown", - "id": "e50f8859", - "metadata": {}, - "source": [ - "To build more advanced planners, we need to introduce a proper Plan object that can contain all the necessary state and information needed for high quality plans.\n", - "\n", - "To see what that object model is, look at (https://github.com/microsoft/semantic-kernel/blob/main/python/semantic_kernel/planners/plan.py)\n" - ] - }, - { - "cell_type": "markdown", - "id": "0a0cb2a2", - "metadata": {}, - "source": [ - "# Sequential Planner\n" - ] - }, - { - "cell_type": "markdown", - "id": "a1c66d83", - "metadata": {}, - "source": [ - "The sequential planner is an XML-based step-by-step planner. You can see the prompt used for it here (https://github.com/microsoft/semantic-kernel/blob/main/python/semantic_kernel/planners/sequential_planner/Plugins/SequentialPlanning/skprompt.txt)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e2e90624", - "metadata": {}, - "outputs": [], - "source": [ - "from semantic_kernel.planners import SequentialPlanner\n", - "\n", - "planner = SequentialPlanner(kernel, service_id)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0d537981", - "metadata": {}, - "outputs": [], - "source": [ - "sequential_plan = await planner.create_plan(goal=ask)" - ] - }, - { - "cell_type": "markdown", - "id": "ee2f462b", - "metadata": {}, - "source": [ - "To see the steps that the Sequential Planner will take, we can iterate over them and print their descriptions\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e7007418", - "metadata": {}, - "outputs": [], - "source": [ - "for step in sequential_plan._steps:\n", - " print(step.description, \":\", step._state.__dict__)" - ] - }, - { - "cell_type": "markdown", - "id": "4db5f844", - "metadata": {}, - "source": [ - "Let's ask the sequential planner to execute the plan.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "88411884", - "metadata": {}, - "outputs": [], - "source": [ - "result = await sequential_plan.invoke(kernel)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "36d27aa0", - "metadata": {}, - "outputs": [], - "source": [ - "print(result)" - ] - }, - { - "cell_type": "markdown", - "id": "d6487c75", - "metadata": {}, - "source": [ - "# Action Planner\n" - ] - }, - { - "cell_type": "markdown", - "id": "b045e26b", - "metadata": {}, - "source": [ - "The action planner takes in a list of functions and the goal, and outputs a **single** function to use that is appropriate to meet that goal.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5bfc0b9f", - "metadata": {}, - "outputs": [], - "source": [ - "from semantic_kernel.planners import ActionPlanner\n", - "\n", - "planner = ActionPlanner(kernel, service_id)" - ] - }, - { - "cell_type": "markdown", - "id": "53b1f296", - "metadata": {}, - "source": [ - "Let's add more plugins to the kernel\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cc12642a", - "metadata": {}, - "outputs": [], - "source": [ - "from semantic_kernel.core_plugins import MathPlugin, TextPlugin, TimePlugin\n", - "\n", - "kernel.import_plugin_from_object(MathPlugin(), \"math\")\n", - "kernel.import_plugin_from_object(TimePlugin(), \"time\")\n", - "kernel.import_plugin_from_object(TextPlugin(), \"text\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b938dc0e", - "metadata": {}, - "outputs": [], - "source": [ - "ask = \"What is the sum of 110 and 990?\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3aafd268", - "metadata": {}, - "outputs": [], - "source": [ - "plan = await planner.create_plan(goal=ask)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "42589835", - "metadata": {}, - "outputs": [], - "source": [ - "result = await plan.invoke(kernel)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dc75e7a9", - "metadata": {}, - "outputs": [], - "source": [ - "print(result)" - ] - }, - { - "cell_type": "markdown", - "id": "789b651a", - "metadata": {}, - "source": [ - "# Stepwise Planner\n" - ] - }, - { - "cell_type": "markdown", - "id": "8a4bbcc3", - "metadata": {}, - "source": [ - "Stepwise Planner is based off the paper from MRKL (Modular Reasoning, Knowledge and Language) and is similar to other papers like ReACT (Reasoning and Acting in Language Models). At the core, the stepwise planner allows for the AI to form \"thoughts\" and \"observations\" and execute actions based off those to achieve a user's goal. This continues until all required functions are complete and a final output is generated.\n", - "\n", - "See a video walkthrough of Stepwise Planner [here.](https://youtu.be/DG_Ge1v0c4Q?si=T1CHaAm1vV0mWRHu)\n" - ] - }, - { - "cell_type": "markdown", - "id": "e0a00bde", - "metadata": {}, - "source": [ - "Let's create a Bing Search native plugin that we can pass in to the Kernel.\n", - "\n", - "Make sure you have a Bing Search API key in your `.env` file\n", - "\n", - "(https://www.microsoft.com/en-us/bing/apis/bing-web-search-api)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "415f7876", - "metadata": {}, - "outputs": [], - "source": [ - "from semantic_kernel.connectors.search_engine import BingConnector\n", - "from semantic_kernel.core_plugins import WebSearchEnginePlugin\n", - "\n", - "BING_API_KEY = sk.bing_search_settings_from_dot_env()\n", - "connector = BingConnector(BING_API_KEY)\n", - "kernel.import_plugin_from_object(WebSearchEnginePlugin(connector), plugin_name=\"WebSearch\")" - ] - }, - { - "cell_type": "markdown", - "id": "effdf3ab", - "metadata": {}, - "source": [ - "Let's also add a couple more plugins\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "abe150e0", - "metadata": {}, - "outputs": [], - "source": [ - "from semantic_kernel.core_plugins.math_plugin import MathPlugin\n", - "from semantic_kernel.core_plugins.time_plugin import TimePlugin\n", - "\n", - "kernel.import_plugin_from_object(TimePlugin(), \"time\")\n", - "kernel.import_plugin_from_object(MathPlugin(), \"math\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "06d08549", - "metadata": {}, - "outputs": [], - "source": [ - "from semantic_kernel.planners.stepwise_planner import StepwisePlanner, StepwisePlannerConfig\n", - "\n", - "planner = StepwisePlanner(kernel, StepwisePlannerConfig(max_iterations=10, min_iteration_time_ms=1000))" - ] - }, - { - "cell_type": "markdown", - "id": "50699ec3", - "metadata": {}, - "source": [ - "Now let's do a more complicated ask that will require planner to make a call to Bing to get the latest information.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "596ade21", - "metadata": {}, - "outputs": [], - "source": [ - "ask = \"\"\"How many total championships combined do the top 5 teams in the NBA have? And which teams are they?\"\"\"\n", - "\n", - "plan = planner.create_plan(goal=ask)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "176988ac", - "metadata": {}, - "outputs": [], - "source": [ - "result = await plan.invoke(kernel)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d00c6f71", - "metadata": {}, - "outputs": [], - "source": [ - "print(result)" - ] - }, - { - "cell_type": "markdown", - "id": "cb40370d", - "metadata": {}, - "source": [ - "Let's see the steps that the AI took to get to the answer.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7159ca1b", - "metadata": {}, - "outputs": [], - "source": [ - "for index, step in enumerate(plan._steps):\n", - " print(\"Step:\", index)\n", - " print(\"Description:\", step.description)\n", - " print(\"Function:\", step.plugin_name + \".\" + step._function.name)\n", - " print(f\" Output: {','.join(str(res) for res in result.metadata['results'])}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "82a52451", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.12" - } - }, - "nbformat": 4, - "nbformat_minor": 5 + "cells": [ + { + "cell_type": "markdown", + "id": "99a80181", + "metadata": {}, + "source": [ + "# Introduction to the Planner\n", + "\n", + "The Planner is one of the fundamental concepts of the Semantic Kernel.\n", + "\n", + "It makes use of the collection of native and semantic functions that have been registered to the kernel and using AI, will formulate a plan to execute the given ask.\n", + "\n", + "From our own testing, planner works best with more powerful models like `gpt4` but sometimes you might get working plans with cheaper models like `gpt-35-turbo`. We encourage you to implement your own versions of the planner and use different models that fit your user needs.\n", + "\n", + "Read more about planner [here](https://aka.ms/sk/concepts/planner)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "07eb35d2", + "metadata": {}, + "outputs": [], + "source": [ + "!python -m pip install -U semantic-kernel==0.9.5b1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7d548e40", + "metadata": {}, + "outputs": [], + "source": [ + "from services import Service\n", + "\n", + "# Select a service to use for this notebook (available services: OpenAI, AzureOpenAI, HuggingFace)\n", + "selectedService = Service.AzureOpenAI" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3852961c", + "metadata": {}, + "outputs": [], + "source": [ + "from semantic_kernel.contents.chat_history import ChatHistory # noqa: F401\n", + "from semantic_kernel.functions.kernel_arguments import KernelArguments # noqa: F401\n", + "from semantic_kernel.prompt_template.input_variable import InputVariable # noqa: F401" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11e59885", + "metadata": {}, + "outputs": [], + "source": [ + "import semantic_kernel as sk\n", + "import semantic_kernel.connectors.ai.open_ai as sk_oai # noqa: F401\n", + "\n", + "kernel = sk.Kernel()\n", + "\n", + "service_id = None\n", + "if selectedService == Service.OpenAI:\n", + " api_key, org_id = sk.openai_settings_from_dot_env()\n", + " service_id = \"default\"\n", + " kernel.add_service(\n", + " sk_oai.OpenAIChatCompletion(\n", + " service_id=service_id, ai_model_id=\"gpt-3.5-turbo-1106\", api_key=api_key, org_id=org_id\n", + " ),\n", + " )\n", + "elif selectedService == Service.AzureOpenAI:\n", + " deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env()\n", + " service_id = \"default\"\n", + " kernel.add_service(\n", + " sk_oai.AzureChatCompletion(\n", + " service_id=service_id, deployment_name=deployment, endpoint=endpoint, api_key=api_key\n", + " ),\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "4ff28070", + "metadata": {}, + "source": [ + "## It all begins with an ask\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "93bc6103", + "metadata": {}, + "outputs": [], + "source": [ + "ask = \"\"\"\n", + "Tomorrow is Valentine's day. I need to come up with a few date ideas. She speaks French so write it in French.\n", + "Convert the text to uppercase\"\"\"" + ] + }, + { + "cell_type": "markdown", + "id": "a5d86739", + "metadata": {}, + "source": [ + "### Providing plugins to the planner\n", + "\n", + "The planner needs to know what plugins are available to it. Here we'll give it access to the `SummarizePlugin` and `WriterPlugin` we have defined on disk. This will include many semantic functions, of which the planner will intelligently choose a subset.\n", + "\n", + "You can also include native functions as well. Here we'll add the TextPlugin.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ca0e7604", + "metadata": {}, + "outputs": [], + "source": [ + "from semantic_kernel.core_plugins.text_plugin import TextPlugin\n", + "\n", + "plugins_directory = \"../../samples/plugins/\"\n", + "summarize_plugin = kernel.import_plugin_from_prompt_directory(plugins_directory, \"SummarizePlugin\")\n", + "writer_plugin = kernel.import_plugin_from_prompt_directory(plugins_directory, \"WriterPlugin\")\n", + "text_plugin = kernel.add_plugin(TextPlugin(), \"TextPlugin\")" + ] + }, + { + "cell_type": "markdown", + "id": "deff5675", + "metadata": {}, + "source": [ + "Define your ASK. What do you want the Kernel to do?\n" + ] + }, + { + "cell_type": "markdown", + "id": "eee6fe7b", + "metadata": {}, + "source": [ + "# Basic Planner\n" + ] + }, + { + "cell_type": "markdown", + "id": "590a22f2", + "metadata": {}, + "source": [ + "Let's start by taking a look at a basic planner. The `BasicPlanner` produces a JSON-based plan that aims to solve the provided ask sequentially and evaluated in order.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20d35ed0", + "metadata": {}, + "outputs": [], + "source": [ + "from semantic_kernel.planners.basic_planner import BasicPlanner\n", + "\n", + "planner = BasicPlanner(service_id)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d5697c09", + "metadata": {}, + "outputs": [], + "source": [ + "basic_plan = await planner.create_plan(ask, kernel)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b425ba1e", + "metadata": {}, + "outputs": [], + "source": [ + "print(basic_plan.generated_plan)" + ] + }, + { + "cell_type": "markdown", + "id": "0f3a48f8", + "metadata": {}, + "source": [ + "You can see that the Planner took my ask and converted it into an JSON-based plan detailing how the AI would go about solving this task, making use of the plugins that the Kernel has available to it.\n", + "\n", + "As you can see in the above plan, the AI has determined which functions to call in order to fulfill the user ask. The output of each step of the plan becomes the input to the next function.\n" + ] + }, + { + "cell_type": "markdown", + "id": "cd4df0c2", + "metadata": {}, + "source": [ + "Let's also define an inline plugin and have it be available to the Planner. Be sure to give it a function name and plugin name.\n" + ] + }, + { + "cell_type": "markdown", + "id": "5057cf9b", + "metadata": {}, + "source": [ + "Let's update our ask using this new plugin\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a3161dcf", + "metadata": {}, + "outputs": [], + "source": [ + "from semantic_kernel.functions.kernel_function_from_prompt import KernelFunctionFromPrompt\n", + "\n", + "kernel = sk.Kernel()\n", + "service_id = \"default\"\n", + "if selectedService == Service.OpenAI:\n", + " api_key, org_id = sk.openai_settings_from_dot_env()\n", + " kernel.add_service(\n", + " sk_oai.OpenAIChatCompletion(\n", + " service_id=service_id, ai_model_id=\"gpt-3.5-turbo-1106\", api_key=api_key, org_id=org_id\n", + " ),\n", + " )\n", + "elif selectedService == Service.AzureOpenAI:\n", + " deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env()\n", + " kernel.add_service(\n", + " sk_oai.AzureChatCompletion(\n", + " service_id=service_id, deployment_name=deployment, endpoint=endpoint, api_key=api_key\n", + " ),\n", + " )\n", + "\n", + "plugins_directory = \"../../samples/plugins/\"\n", + "summarize_plugin = kernel.import_plugin_from_prompt_directory(plugins_directory, \"SummarizePlugin\")\n", + "writer_plugin = kernel.import_plugin_from_prompt_directory(plugins_directory, \"WriterPlugin\")\n", + "text_plugin = kernel.add_plugin(TextPlugin(), \"TextPlugin\")\n", + "\n", + "shakespeare_func = KernelFunctionFromPrompt(\n", + " function_name=\"Shakespeare\",\n", + " plugin_name=\"WriterPlugin\",\n", + " prompt=\"\"\"\n", + "{{$input}}\n", + "\n", + "Rewrite the above in the style of Shakespeare.\n", + "\"\"\",\n", + " prompt_execution_settings=sk_oai.OpenAIChatPromptExecutionSettings(\n", + " service_id=service_id,\n", + " max_tokens=2000,\n", + " temperature=0.8,\n", + " ),\n", + ")\n", + "kernel.add_function(\"WriterPlugin\", shakespeare_func)\n", + "\n", + "for plugin in kernel.plugins.values():\n", + " for function in plugin:\n", + " print(f\"Plugin: {plugin.name}, Function: {function.name}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25abac0d", + "metadata": {}, + "outputs": [], + "source": [ + "planner = BasicPlanner(service_id)\n", + "\n", + "ask = \"\"\"\n", + "Tomorrow is Valentine's day. I need to come up with a few short poems.\n", + "She likes Shakespeare so write using his style. She speaks French so write it in French.\n", + "Convert the text to uppercase.\"\"\"\n", + "\n", + "new_plan = await planner.create_plan(goal=ask, kernel=kernel)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "997462e8", + "metadata": {}, + "outputs": [], + "source": [ + "print(new_plan.generated_plan)" + ] + }, + { + "cell_type": "markdown", + "id": "b67a052e", + "metadata": {}, + "source": [ + "### Executing the plan\n" + ] + }, + { + "cell_type": "markdown", + "id": "3b839c90", + "metadata": {}, + "source": [ + "Now that we have a plan, let's try to execute it! The Planner has a function called `execute_plan`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9384831a", + "metadata": {}, + "outputs": [], + "source": [ + "results = await planner.execute_plan(new_plan, kernel)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9192b186", + "metadata": {}, + "outputs": [], + "source": [ + "print(results)" + ] + }, + { + "cell_type": "markdown", + "id": "e8a9b6b7", + "metadata": {}, + "source": [ + "# The Plan Object Model\n" + ] + }, + { + "cell_type": "markdown", + "id": "e50f8859", + "metadata": {}, + "source": [ + "To build more advanced planners, we need to introduce a proper Plan object that can contain all the necessary state and information needed for high quality plans.\n", + "\n", + "To see what that object model is, look at (https://github.com/microsoft/semantic-kernel/blob/main/python/semantic_kernel/planners/plan.py)\n" + ] + }, + { + "cell_type": "markdown", + "id": "0a0cb2a2", + "metadata": {}, + "source": [ + "# Sequential Planner\n" + ] + }, + { + "cell_type": "markdown", + "id": "a1c66d83", + "metadata": {}, + "source": [ + "The sequential planner is an XML-based step-by-step planner. You can see the prompt used for it here (https://github.com/microsoft/semantic-kernel/blob/main/python/semantic_kernel/planners/sequential_planner/Plugins/SequentialPlanning/skprompt.txt)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e2e90624", + "metadata": {}, + "outputs": [], + "source": [ + "from semantic_kernel.planners import SequentialPlanner\n", + "\n", + "planner = SequentialPlanner(kernel, service_id)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0d537981", + "metadata": {}, + "outputs": [], + "source": [ + "sequential_plan = await planner.create_plan(goal=ask)" + ] + }, + { + "cell_type": "markdown", + "id": "ee2f462b", + "metadata": {}, + "source": [ + "To see the steps that the Sequential Planner will take, we can iterate over them and print their descriptions\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e7007418", + "metadata": {}, + "outputs": [], + "source": [ + "for step in sequential_plan._steps:\n", + " print(step.description, \":\", step._state.__dict__)" + ] + }, + { + "cell_type": "markdown", + "id": "4db5f844", + "metadata": {}, + "source": [ + "Let's ask the sequential planner to execute the plan.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "88411884", + "metadata": {}, + "outputs": [], + "source": [ + "result = await sequential_plan.invoke(kernel)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "36d27aa0", + "metadata": {}, + "outputs": [], + "source": [ + "print(result)" + ] + }, + { + "cell_type": "markdown", + "id": "d6487c75", + "metadata": {}, + "source": [ + "# Action Planner\n" + ] + }, + { + "cell_type": "markdown", + "id": "b045e26b", + "metadata": {}, + "source": [ + "The action planner takes in a list of functions and the goal, and outputs a **single** function to use that is appropriate to meet that goal.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5bfc0b9f", + "metadata": {}, + "outputs": [], + "source": [ + "from semantic_kernel.planners import ActionPlanner\n", + "\n", + "planner = ActionPlanner(kernel, service_id)" + ] + }, + { + "cell_type": "markdown", + "id": "53b1f296", + "metadata": {}, + "source": [ + "Let's add more plugins to the kernel\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cc12642a", + "metadata": {}, + "outputs": [], + "source": [ + "from semantic_kernel.core_plugins import MathPlugin, TextPlugin, TimePlugin\n", + "\n", + "kernel.add_plugin(MathPlugin(), \"math\")\n", + "kernel.add_plugin(TimePlugin(), \"time\")\n", + "kernel.add_plugin(TextPlugin(), \"text\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b938dc0e", + "metadata": {}, + "outputs": [], + "source": [ + "ask = \"What is the sum of 110 and 990?\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3aafd268", + "metadata": {}, + "outputs": [], + "source": [ + "plan = await planner.create_plan(goal=ask)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "42589835", + "metadata": {}, + "outputs": [], + "source": [ + "result = await plan.invoke(kernel)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dc75e7a9", + "metadata": {}, + "outputs": [], + "source": [ + "print(result)" + ] + }, + { + "cell_type": "markdown", + "id": "789b651a", + "metadata": {}, + "source": [ + "# Stepwise Planner\n" + ] + }, + { + "cell_type": "markdown", + "id": "8a4bbcc3", + "metadata": {}, + "source": [ + "Stepwise Planner is based off the paper from MRKL (Modular Reasoning, Knowledge and Language) and is similar to other papers like ReACT (Reasoning and Acting in Language Models). At the core, the stepwise planner allows for the AI to form \"thoughts\" and \"observations\" and execute actions based off those to achieve a user's goal. This continues until all required functions are complete and a final output is generated.\n", + "\n", + "See a video walkthrough of Stepwise Planner [here.](https://youtu.be/DG_Ge1v0c4Q?si=T1CHaAm1vV0mWRHu)\n" + ] + }, + { + "cell_type": "markdown", + "id": "e0a00bde", + "metadata": {}, + "source": [ + "Let's create a Bing Search native plugin that we can pass in to the Kernel.\n", + "\n", + "Make sure you have a Bing Search API key in your `.env` file\n", + "\n", + "(https://www.microsoft.com/en-us/bing/apis/bing-web-search-api)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "415f7876", + "metadata": {}, + "outputs": [], + "source": [ + "from semantic_kernel.connectors.search_engine import BingConnector\n", + "from semantic_kernel.core_plugins import WebSearchEnginePlugin\n", + "\n", + "BING_API_KEY = sk.bing_search_settings_from_dot_env()\n", + "connector = BingConnector(BING_API_KEY)\n", + "kernel.add_plugin(WebSearchEnginePlugin(connector), plugin_name=\"WebSearch\")" + ] + }, + { + "cell_type": "markdown", + "id": "effdf3ab", + "metadata": {}, + "source": [ + "Let's also add a couple more plugins\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "abe150e0", + "metadata": {}, + "outputs": [], + "source": [ + "from semantic_kernel.core_plugins.math_plugin import MathPlugin\n", + "from semantic_kernel.core_plugins.time_plugin import TimePlugin\n", + "\n", + "kernel.add_plugin(TimePlugin(), \"time\")\n", + "kernel.add_plugin(MathPlugin(), \"math\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "06d08549", + "metadata": {}, + "outputs": [], + "source": [ + "from semantic_kernel.planners.stepwise_planner import StepwisePlanner, StepwisePlannerConfig\n", + "\n", + "planner = StepwisePlanner(kernel, StepwisePlannerConfig(max_iterations=10, min_iteration_time_ms=1000))" + ] + }, + { + "cell_type": "markdown", + "id": "50699ec3", + "metadata": {}, + "source": [ + "Now let's do a more complicated ask that will require planner to make a call to Bing to get the latest information.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "596ade21", + "metadata": {}, + "outputs": [], + "source": [ + "ask = \"\"\"How many total championships combined do the top 5 teams in the NBA have? And which teams are they?\"\"\"\n", + "\n", + "plan = planner.create_plan(goal=ask)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "176988ac", + "metadata": {}, + "outputs": [], + "source": [ + "result = await plan.invoke(kernel)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d00c6f71", + "metadata": {}, + "outputs": [], + "source": [ + "print(result)" + ] + }, + { + "cell_type": "markdown", + "id": "cb40370d", + "metadata": {}, + "source": [ + "Let's see the steps that the AI took to get to the answer.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7159ca1b", + "metadata": {}, + "outputs": [], + "source": [ + "for index, step in enumerate(plan._steps):\n", + " print(\"Step:\", index)\n", + " print(\"Description:\", step.description)\n", + " print(\"Function:\", step.plugin_name + \".\" + step._function.name)\n", + " print(f\" Output: {','.join(str(res) for res in result.metadata['results'])}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "82a52451", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 } diff --git a/python/notebooks/06-memory-and-embeddings.ipynb b/python/notebooks/06-memory-and-embeddings.ipynb index 0e88a2a46a..e776c90d9e 100644 --- a/python/notebooks/06-memory-and-embeddings.ipynb +++ b/python/notebooks/06-memory-and-embeddings.ipynb @@ -1,519 +1,506 @@ { - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "id": "68e1c158", - "metadata": {}, - "source": [ - "# Building Semantic Memory with Embeddings\n", - "\n", - "So far, we've mostly been treating the kernel as a stateless orchestration engine.\n", - "We send text into a model API and receive text out.\n", - "\n", - "In a [previous notebook](04-kernel-arguments-chat.ipynb), we used `kernel arguments` to pass in additional\n", - "text into prompts to enrich them with more data. This allowed us to create a basic chat experience.\n", - "\n", - "However, if you solely relied on kernel arguments, you would quickly realize that eventually your prompt\n", - "would grow so large that you would run into the model's token limit. What we need is a way to persist state\n", - "and build both short-term and long-term memory to empower even more intelligent applications.\n", - "\n", - "To do this, we dive into the key concept of `Semantic Memory` in the Semantic Kernel.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a77bdf89", - "metadata": {}, - "outputs": [], - "source": [ - "!python -m pip install semantic-kernel==0.9.5b1" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "508ad44f", - "metadata": {}, - "outputs": [], - "source": [ - "import semantic_kernel as sk\n", - "from semantic_kernel.connectors.ai.open_ai import (\n", - " OpenAIChatCompletion,\n", - " OpenAITextEmbedding,\n", - " AzureChatCompletion,\n", - " AzureTextEmbedding,\n", - ")\n", - "from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig\n", - "from semantic_kernel.memory.semantic_text_memory import SemanticTextMemory\n", - "from semantic_kernel.core_plugins.text_memory_plugin import TextMemoryPlugin" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1b95af24", - "metadata": {}, - "outputs": [], - "source": [ - "from services import Service\n", - "\n", - "# Select a service to use for this notebook (available services: OpenAI, AzureOpenAI, HuggingFace)\n", - "selectedService = Service.OpenAI" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "d8ddffc1", - "metadata": {}, - "source": [ - "In order to use memory, we need to instantiate the Kernel with a Memory Storage\n", - "and an Embedding service. In this example, we make use of the `VolatileMemoryStore` which can be thought of as a temporary in-memory storage. This memory is not written to disk and is only available during the app session.\n", - "\n", - "When developing your app you will have the option to plug in persistent storage like Azure AI Search, Azure Cosmos Db, PostgreSQL, SQLite, etc. Semantic Memory allows also to index external data sources, without duplicating all the information as you will see further down in this notebook.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8f8dcbc6", - "metadata": {}, - "outputs": [], - "source": [ - "kernel = sk.Kernel()\n", - "\n", - "chat_service_id = \"chat\"\n", - "\n", - "# Configure AI service used by the kernel\n", - "if selectedService == Service.AzureOpenAI:\n", - " deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env()\n", - " # next line assumes chat deployment name is \"turbo\", adjust the deployment name to the value of your chat model if needed\n", - " azure_chat_service = AzureChatCompletion(\n", - " service_id=chat_service_id, deployment_name=\"turbo\", endpoint=endpoint, api_key=api_key\n", - " )\n", - " # next line assumes embeddings deployment name is \"text-embedding\", adjust the deployment name to the value of your chat model if needed\n", - " embedding_gen = AzureTextEmbedding(deployment_name=\"text-embedding\", endpoint=endpoint, api_key=api_key)\n", - " kernel.add_service(azure_chat_service)\n", - " kernel.add_service(embedding_gen)\n", - "elif selectedService == Service.OpenAI:\n", - " api_key, org_id = sk.openai_settings_from_dot_env()\n", - " oai_chat_service = OpenAIChatCompletion(\n", - " service_id=chat_service_id, ai_model_id=\"gpt-3.5-turbo\", api_key=api_key, org_id=org_id\n", - " )\n", - " embedding_gen = OpenAITextEmbedding(ai_model_id=\"text-embedding-ada-002\", api_key=api_key, org_id=org_id)\n", - " kernel.add_service(oai_chat_service)\n", - " kernel.add_service(embedding_gen)\n", - "\n", - "memory = SemanticTextMemory(storage=sk.memory.VolatileMemoryStore(), embeddings_generator=embedding_gen)\n", - "kernel.import_plugin_from_object(TextMemoryPlugin(memory), \"TextMemoryPlugin\")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "e7fefb6a", - "metadata": {}, - "source": [ - "At its core, Semantic Memory is a set of data structures that allow you to store the meaning of text that come from different data sources, and optionally to store the source text too. These texts can be from the web, e-mail providers, chats, a database, or from your local directory, and are hooked up to the Semantic Kernel through data source connectors.\n", - "\n", - "The texts are embedded or compressed into a vector of floats representing mathematically the texts' contents and meaning. You can read more about embeddings [here](https://aka.ms/sk/embeddings).\n" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "2a7e7ca4", - "metadata": {}, - "source": [ - "### Manually adding memories\n", - "\n", - "Let's create some initial memories \"About Me\". We can add memories to our `VolatileMemoryStore` by using `SaveInformationAsync`\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d096504c", - "metadata": {}, - "outputs": [], - "source": [ - "collection_id = \"generic\"\n", - "\n", - "\n", - "async def populate_memory(memory: SemanticTextMemory) -> None:\n", - " # Add some documents to the semantic memory\n", - " await memory.save_information(collection=collection_id, id=\"info1\", text=\"Your budget for 2024 is $100,000\")\n", - " await memory.save_information(collection=collection_id, id=\"info2\", text=\"Your savings from 2023 are $50,000\")\n", - " await memory.save_information(collection=collection_id, id=\"info3\", text=\"Your investments are $80,000\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5338d3ac", - "metadata": {}, - "outputs": [], - "source": [ - "await populate_memory(memory)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "2calf857", - "metadata": {}, - "source": [ - "Let's try searching the memory:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "628c843e", - "metadata": {}, - "outputs": [], - "source": [ - "async def search_memory_examples(memory: SemanticTextMemory) -> None:\n", - " questions = [\"What is my budget for 2024?\", \"What are my savings from 2023?\", \"What are my investments?\"]\n", - "\n", - " for question in questions:\n", - " print(f\"Question: {question}\")\n", - " result = await memory.search(collection_id, question)\n", - " print(f\"Answer: {result[0].text}\\n\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "24764c48", - "metadata": {}, - "outputs": [], - "source": [ - "await search_memory_examples(memory)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "e70c2b22", - "metadata": {}, - "source": [ - "Let's now revisit the our chat sample from the [previous notebook](04-kernel-arguments-chat.ipynb).\n", - "If you remember, we used kernel arguments to fill the prompt with a `history` that continuously got populated as we chatted with the bot. Let's add also memory to it!\n" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "1ed54a32", - "metadata": {}, - "source": [ - "This is done by using the `TextMemoryPlugin` which exposes the `recall` native function.\n", - "\n", - "`recall` takes an input ask and performs a similarity search on the contents that have\n", - "been embedded in the Memory Store and returns the most relevant memory.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fb8549b2", - "metadata": {}, - "outputs": [], - "source": [ - "async def setup_chat_with_memory(\n", - " kernel: sk.Kernel,\n", - " service_id: str,\n", - ") -> sk.KernelFunction:\n", - " prompt = \"\"\"\n", - " ChatBot can have a conversation with you about any topic.\n", - " It can give explicit instructions or say 'I don't know' if\n", - " it does not have an answer.\n", - "\n", - " Information about me, from previous conversations:\n", - " - {{recall 'budget by year'}} What is my budget for 2024?\n", - " - {{recall 'savings from previous year'}} What are my savings from 2023?\n", - " - {{recall 'investments'}} What are my investments?\n", - "\n", - " {{$request}}\n", - " \"\"\".strip()\n", - "\n", - " prompt_template_config = PromptTemplateConfig(\n", - " template=prompt,\n", - " execution_settings={\n", - " service_id: kernel.get_service(service_id).get_prompt_execution_settings_class()(service_id=service_id)\n", - " },\n", - " )\n", - "\n", - " chat_func = kernel.create_function_from_prompt(\n", - " function_name=\"chat_with_memory\",\n", - " plugin_name=\"chat\",\n", - " prompt_template_config=prompt_template_config,\n", - " )\n", - "\n", - " return chat_func" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "1ac62457", - "metadata": {}, - "source": [ - "The `RelevanceParam` is used in memory search and is a measure of the relevance score from 0.0 to 1.0, where 1.0 means a perfect match. We encourage users to experiment with different values.\n" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "645b55a1", - "metadata": {}, - "source": [ - "Now that we've included our memories, let's chat!\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "75267a2f", - "metadata": {}, - "outputs": [], - "source": [ - "async def chat(kernel: sk.Kernel, chat_func: sk.KernelFunction) -> bool:\n", - " try:\n", - " user_input = input(\"User:> \")\n", - " except KeyboardInterrupt:\n", - " print(\"\\n\\nExiting chat...\")\n", - " return False\n", - " except EOFError:\n", - " print(\"\\n\\nExiting chat...\")\n", - " return False\n", - "\n", - " if user_input == \"exit\":\n", - " print(\"\\n\\nExiting chat...\")\n", - " return False\n", - "\n", - " answer = await kernel.invoke(chat_func, request=user_input)\n", - "\n", - " print(f\"ChatBot:> {answer}\")\n", - " return True" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e3875a34", - "metadata": {}, - "outputs": [], - "source": [ - "print(\"Populating memory...\")\n", - "await populate_memory(memory)\n", - "\n", - "print(\"Asking questions... (manually)\")\n", - "await search_memory_examples(memory)\n", - "\n", - "print(\"Setting up a chat (with memory!)\")\n", - "chat_func = await setup_chat_with_memory(kernel, chat_service_id)\n", - "\n", - "print(\"Begin chatting (type 'exit' to exit):\\n\")\n", - "print(\n", - " \"Welcome to the chat bot!\\\n", - " \\n Type 'exit' to exit.\\\n", - " \\n Try asking a question about your finances (i.e. \\\"talk to me about my finances\\\").\"\n", - ")\n", - "chatting = True\n", - "while chatting:\n", - " chatting = await chat(kernel, chat_func)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "0a51542b", - "metadata": {}, - "source": [ - "### Adding documents to your memory\n", - "\n", - "Many times in your applications you'll want to bring in external documents into your memory. Let's see how we can do this using our VolatileMemoryStore.\n", - "\n", - "Let's first get some data using some of the links in the Semantic Kernel repo.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c3d5a1b9", - "metadata": {}, - "outputs": [], - "source": [ - "github_files = {}\n", - "github_files[\"https://github.com/microsoft/semantic-kernel/blob/main/README.md\"] = (\n", - " \"README: Installation, getting started, and how to contribute\"\n", - ")\n", - "github_files[\n", - " \"https://github.com/microsoft/semantic-kernel/blob/main/dotnet/notebooks/02-running-prompts-from-file.ipynb\"\n", - "] = \"Jupyter notebook describing how to pass prompts from a file to a semantic plugin or function\"\n", - "github_files[\"https://github.com/microsoft/semantic-kernel/blob/main/dotnet/notebooks/00-getting-started.ipynb\"] = (\n", - " \"Jupyter notebook describing how to get started with the Semantic Kernel\"\n", - ")\n", - "github_files[\"https://github.com/microsoft/semantic-kernel/tree/main/samples/plugins/ChatPlugin/ChatGPT\"] = (\n", - " \"Sample demonstrating how to create a chat plugin interfacing with ChatGPT\"\n", - ")\n", - "github_files[\n", - " \"https://github.com/microsoft/semantic-kernel/blob/main/dotnet/src/SemanticKernel/Memory/Volatile/VolatileMemoryStore.cs\"\n", - "] = \"C# class that defines a volatile embedding store\"" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "75f3ea5e", - "metadata": {}, - "source": [ - "Now let's add these files to our VolatileMemoryStore using `SaveReferenceAsync`. We'll separate these memories from the chat memories by putting them in a different collection.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "170e7142", - "metadata": {}, - "outputs": [], - "source": [ - "memory_collection_name = \"SKGitHub\"\n", - "print(\"Adding some GitHub file URLs and their descriptions to a volatile Semantic Memory.\")\n", - "i = 0\n", - "for entry, value in github_files.items():\n", - " await memory.save_reference(\n", - " collection=memory_collection_name,\n", - " description=value,\n", - " text=value,\n", - " external_id=entry,\n", - " external_source_name=\"GitHub\",\n", - " )\n", - " i += 1\n", - " print(\" URL {} saved\".format(i))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "143911c3", - "metadata": {}, - "outputs": [], - "source": [ - "ask = \"I love Jupyter notebooks, how should I get started?\"\n", - "print(\"===========================\\n\" + \"Query: \" + ask + \"\\n\")\n", - "\n", - "memories = await memory.search(memory_collection_name, ask, limit=5, min_relevance_score=0.77)\n", - "\n", - "i = 0\n", - "for memory in memories:\n", - " i += 1\n", - " print(f\"Result {i}:\")\n", - " print(\" URL: : \" + memory.id)\n", - " print(\" Title : \" + memory.description)\n", - " print(\" Relevance: \" + str(memory.relevance))\n", - " print()" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "59294dac", - "metadata": {}, - "source": [ - "Now you might be wondering what happens if you have so much data that it doesn't fit into your RAM? That's where you want to make use of an external Vector Database made specifically for storing and retrieving embeddings. Fortunately, semantic kernel makes this easy thanks to an extensive list of available connectors. In the following section, we will connect to an existing Azure AI Search service that we will use as an external Vector Database to store and retrieve embeddings.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "77fdfa86", - "metadata": {}, - "outputs": [], - "source": [ - "from semantic_kernel.connectors.memory.azure_cognitive_search import (\n", - " AzureCognitiveSearchMemoryStore,\n", - ")\n", - "\n", - "azure_ai_search_api_key, azure_ai_search_url = sk.azure_aisearch_settings_from_dot_env()\n", - "\n", - "acs_memory_store = AzureCognitiveSearchMemoryStore(\n", - " vector_size=1536,\n", - " search_endpoint=azure_ai_search_url,\n", - " admin_key=azure_ai_search_api_key,\n", - ")\n", - "\n", - "memory = SemanticTextMemory(storage=acs_memory_store, embeddings_generator=embedding_gen)\n", - "kernel.import_plugin_from_object(TextMemoryPlugin(memory), \"TextMemoryPluginACS\")" - ] - }, - { - "cell_type": "markdown", - "id": "94f9e83b", - "metadata": {}, - "source": [ - "The implementation of Semantic Kernel allows to easily swap memory store for another. Here, we will re-use the functions we initially created for `VolatileMemoryStore` with our new external Vector Store leveraging Azure AI Search\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fc3da7e1", - "metadata": {}, - "outputs": [], - "source": [ - "await populate_memory(memory)" - ] - }, - { - "cell_type": "markdown", - "id": "b0bbe830", - "metadata": {}, - "source": [ - "Let's now try to query from Azure AI Search!\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1a09d0ca", - "metadata": {}, - "outputs": [], - "source": [ - "await search_memory_examples(memory)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We have laid the foundation which will allow us to store an arbitrary amount of data in an external Vector Store above and beyond what could fit in memory at the expense of a little more latency.\n" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.12" - } - }, - "nbformat": 4, - "nbformat_minor": 5 + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "id": "68e1c158", + "metadata": {}, + "source": [ + "# Building Semantic Memory with Embeddings\n", + "\n", + "So far, we've mostly been treating the kernel as a stateless orchestration engine.\n", + "We send text into a model API and receive text out.\n", + "\n", + "In a [previous notebook](04-kernel-arguments-chat.ipynb), we used `kernel arguments` to pass in additional\n", + "text into prompts to enrich them with more data. This allowed us to create a basic chat experience.\n", + "\n", + "However, if you solely relied on kernel arguments, you would quickly realize that eventually your prompt\n", + "would grow so large that you would run into the model's token limit. What we need is a way to persist state\n", + "and build both short-term and long-term memory to empower even more intelligent applications.\n", + "\n", + "To do this, we dive into the key concept of `Semantic Memory` in the Semantic Kernel.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a77bdf89", + "metadata": {}, + "outputs": [], + "source": [ + "!python -m pip install semantic-kernel==0.9.5b1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "508ad44f", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1b95af24", + "metadata": {}, + "outputs": [], + "source": [ + "from services import Service\n", + "\n", + "# Select a service to use for this notebook (available services: OpenAI, AzureOpenAI, HuggingFace)\n", + "selectedService = Service.OpenAI" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "d8ddffc1", + "metadata": {}, + "source": [ + "In order to use memory, we need to instantiate the Kernel with a Memory Storage\n", + "and an Embedding service. In this example, we make use of the `VolatileMemoryStore` which can be thought of as a temporary in-memory storage. This memory is not written to disk and is only available during the app session.\n", + "\n", + "When developing your app you will have the option to plug in persistent storage like Azure AI Search, Azure Cosmos Db, PostgreSQL, SQLite, etc. Semantic Memory allows also to index external data sources, without duplicating all the information as you will see further down in this notebook.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8f8dcbc6", + "metadata": {}, + "outputs": [], + "source": [ + "kernel = sk.Kernel()\n", + "\n", + "chat_service_id = \"chat\"\n", + "\n", + "# Configure AI service used by the kernel\n", + "if selectedService == Service.AzureOpenAI:\n", + " deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env()\n", + " # next line assumes chat deployment name is \"turbo\", adjust the deployment name to the value of your chat model if needed\n", + " azure_chat_service = AzureChatCompletion(\n", + " service_id=chat_service_id, deployment_name=\"turbo\", endpoint=endpoint, api_key=api_key\n", + " )\n", + " # next line assumes embeddings deployment name is \"text-embedding\", adjust the deployment name to the value of your chat model if needed\n", + " embedding_gen = AzureTextEmbedding(deployment_name=\"text-embedding\", endpoint=endpoint, api_key=api_key)\n", + " kernel.add_service(azure_chat_service)\n", + " kernel.add_service(embedding_gen)\n", + "elif selectedService == Service.OpenAI:\n", + " api_key, org_id = sk.openai_settings_from_dot_env()\n", + " oai_chat_service = OpenAIChatCompletion(\n", + " service_id=chat_service_id, ai_model_id=\"gpt-3.5-turbo\", api_key=api_key, org_id=org_id\n", + " )\n", + " embedding_gen = OpenAITextEmbedding(ai_model_id=\"text-embedding-ada-002\", api_key=api_key, org_id=org_id)\n", + " kernel.add_service(oai_chat_service)\n", + " kernel.add_service(embedding_gen)\n", + "\n", + "memory = SemanticTextMemory(storage=sk.memory.VolatileMemoryStore(), embeddings_generator=embedding_gen)\n", + "kernel.add_plugin(TextMemoryPlugin(memory), \"TextMemoryPlugin\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "e7fefb6a", + "metadata": {}, + "source": [ + "At its core, Semantic Memory is a set of data structures that allow you to store the meaning of text that come from different data sources, and optionally to store the source text too. These texts can be from the web, e-mail providers, chats, a database, or from your local directory, and are hooked up to the Semantic Kernel through data source connectors.\n", + "\n", + "The texts are embedded or compressed into a vector of floats representing mathematically the texts' contents and meaning. You can read more about embeddings [here](https://aka.ms/sk/embeddings).\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "2a7e7ca4", + "metadata": {}, + "source": [ + "### Manually adding memories\n", + "\n", + "Let's create some initial memories \"About Me\". We can add memories to our `VolatileMemoryStore` by using `SaveInformationAsync`\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d096504c", + "metadata": {}, + "outputs": [], + "source": [ + "collection_id = \"generic\"\n", + "\n", + "\n", + "async def populate_memory(memory: SemanticTextMemory) -> None:\n", + " # Add some documents to the semantic memory\n", + " await memory.save_information(collection=collection_id, id=\"info1\", text=\"Your budget for 2024 is $100,000\")\n", + " await memory.save_information(collection=collection_id, id=\"info2\", text=\"Your savings from 2023 are $50,000\")\n", + " await memory.save_information(collection=collection_id, id=\"info3\", text=\"Your investments are $80,000\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5338d3ac", + "metadata": {}, + "outputs": [], + "source": [ + "await populate_memory(memory)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "2calf857", + "metadata": {}, + "source": [ + "Let's try searching the memory:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "628c843e", + "metadata": {}, + "outputs": [], + "source": [ + "async def search_memory_examples(memory: SemanticTextMemory) -> None:\n", + " questions = [\"What is my budget for 2024?\", \"What are my savings from 2023?\", \"What are my investments?\"]\n", + "\n", + " for question in questions:\n", + " print(f\"Question: {question}\")\n", + " result = await memory.search(collection_id, question)\n", + " print(f\"Answer: {result[0].text}\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24764c48", + "metadata": {}, + "outputs": [], + "source": [ + "await search_memory_examples(memory)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "e70c2b22", + "metadata": {}, + "source": [ + "Let's now revisit the our chat sample from the [previous notebook](04-kernel-arguments-chat.ipynb).\n", + "If you remember, we used kernel arguments to fill the prompt with a `history` that continuously got populated as we chatted with the bot. Let's add also memory to it!\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "1ed54a32", + "metadata": {}, + "source": [ + "This is done by using the `TextMemoryPlugin` which exposes the `recall` native function.\n", + "\n", + "`recall` takes an input ask and performs a similarity search on the contents that have\n", + "been embedded in the Memory Store and returns the most relevant memory.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fb8549b2", + "metadata": {}, + "outputs": [], + "source": [ + "async def setup_chat_with_memory(\n", + " kernel: sk.Kernel,\n", + " service_id: str,\n", + ") -> sk.KernelFunction:\n", + " prompt = \"\"\"\n", + " ChatBot can have a conversation with you about any topic.\n", + " It can give explicit instructions or say 'I don't know' if\n", + " it does not have an answer.\n", + "\n", + " Information about me, from previous conversations:\n", + " - {{recall 'budget by year'}} What is my budget for 2024?\n", + " - {{recall 'savings from previous year'}} What are my savings from 2023?\n", + " - {{recall 'investments'}} What are my investments?\n", + "\n", + " {{$request}}\n", + " \"\"\".strip()\n", + "\n", + " prompt_template_config = PromptTemplateConfig(\n", + " template=prompt,\n", + " execution_settings={\n", + " service_id: kernel.get_service(service_id).get_prompt_execution_settings_class()(service_id=service_id)\n", + " },\n", + " )\n", + "\n", + " chat_func = kernel.add_function(\n", + " function_name=\"chat_with_memory\",\n", + " plugin_name=\"chat\",\n", + " prompt_template_config=prompt_template_config,\n", + " )\n", + "\n", + " return chat_func" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "1ac62457", + "metadata": {}, + "source": [ + "The `RelevanceParam` is used in memory search and is a measure of the relevance score from 0.0 to 1.0, where 1.0 means a perfect match. We encourage users to experiment with different values.\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "645b55a1", + "metadata": {}, + "source": [ + "Now that we've included our memories, let's chat!\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "75267a2f", + "metadata": {}, + "outputs": [], + "source": [ + "async def chat(kernel: sk.Kernel, chat_func: sk.KernelFunction) -> bool:\n", + " try:\n", + " user_input = input(\"User:> \")\n", + " except KeyboardInterrupt:\n", + " print(\"\\n\\nExiting chat...\")\n", + " return False\n", + " except EOFError:\n", + " print(\"\\n\\nExiting chat...\")\n", + " return False\n", + "\n", + " if user_input == \"exit\":\n", + " print(\"\\n\\nExiting chat...\")\n", + " return False\n", + "\n", + " answer = await kernel.invoke(chat_func, request=user_input)\n", + "\n", + " print(f\"ChatBot:> {answer}\")\n", + " return True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e3875a34", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Populating memory...\")\n", + "await populate_memory(memory)\n", + "\n", + "print(\"Asking questions... (manually)\")\n", + "await search_memory_examples(memory)\n", + "\n", + "print(\"Setting up a chat (with memory!)\")\n", + "chat_func = await setup_chat_with_memory(kernel, chat_service_id)\n", + "\n", + "print(\"Begin chatting (type 'exit' to exit):\\n\")\n", + "print(\n", + " \"Welcome to the chat bot!\\\n", + " \\n Type 'exit' to exit.\\\n", + " \\n Try asking a question about your finances (i.e. \\\"talk to me about my finances\\\").\"\n", + ")\n", + "chatting = True\n", + "while chatting:\n", + " chatting = await chat(kernel, chat_func)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "0a51542b", + "metadata": {}, + "source": [ + "### Adding documents to your memory\n", + "\n", + "Many times in your applications you'll want to bring in external documents into your memory. Let's see how we can do this using our VolatileMemoryStore.\n", + "\n", + "Let's first get some data using some of the links in the Semantic Kernel repo.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c3d5a1b9", + "metadata": {}, + "outputs": [], + "source": [ + "github_files = {}\n", + "github_files[\"https://github.com/microsoft/semantic-kernel/blob/main/README.md\"] = (\n", + " \"README: Installation, getting started, and how to contribute\"\n", + ")\n", + "github_files[\n", + " \"https://github.com/microsoft/semantic-kernel/blob/main/dotnet/notebooks/02-running-prompts-from-file.ipynb\"\n", + "] = \"Jupyter notebook describing how to pass prompts from a file to a semantic plugin or function\"\n", + "github_files[\"https://github.com/microsoft/semantic-kernel/blob/main/dotnet/notebooks/00-getting-started.ipynb\"] = (\n", + " \"Jupyter notebook describing how to get started with the Semantic Kernel\"\n", + ")\n", + "github_files[\"https://github.com/microsoft/semantic-kernel/tree/main/samples/plugins/ChatPlugin/ChatGPT\"] = (\n", + " \"Sample demonstrating how to create a chat plugin interfacing with ChatGPT\"\n", + ")\n", + "github_files[\n", + " \"https://github.com/microsoft/semantic-kernel/blob/main/dotnet/src/SemanticKernel/Memory/Volatile/VolatileMemoryStore.cs\"\n", + "] = \"C# class that defines a volatile embedding store\"" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "75f3ea5e", + "metadata": {}, + "source": [ + "Now let's add these files to our VolatileMemoryStore using `SaveReferenceAsync`. We'll separate these memories from the chat memories by putting them in a different collection.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "170e7142", + "metadata": {}, + "outputs": [], + "source": [ + "memory_collection_name = \"SKGitHub\"\n", + "print(\"Adding some GitHub file URLs and their descriptions to a volatile Semantic Memory.\")\n", + "i = 0\n", + "for entry, value in github_files.items():\n", + " await memory.save_reference(\n", + " collection=memory_collection_name,\n", + " description=value,\n", + " text=value,\n", + " external_id=entry,\n", + " external_source_name=\"GitHub\",\n", + " )\n", + " i += 1\n", + " print(\" URL {} saved\".format(i))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "143911c3", + "metadata": {}, + "outputs": [], + "source": [ + "ask = \"I love Jupyter notebooks, how should I get started?\"\n", + "print(\"===========================\\n\" + \"Query: \" + ask + \"\\n\")\n", + "\n", + "memories = await memory.search(memory_collection_name, ask, limit=5, min_relevance_score=0.77)\n", + "\n", + "i = 0\n", + "for memory in memories:\n", + " i += 1\n", + " print(f\"Result {i}:\")\n", + " print(\" URL: : \" + memory.id)\n", + " print(\" Title : \" + memory.description)\n", + " print(\" Relevance: \" + str(memory.relevance))\n", + " print()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "59294dac", + "metadata": {}, + "source": [ + "Now you might be wondering what happens if you have so much data that it doesn't fit into your RAM? That's where you want to make use of an external Vector Database made specifically for storing and retrieving embeddings. Fortunately, semantic kernel makes this easy thanks to an extensive list of available connectors. In the following section, we will connect to an existing Azure AI Search service that we will use as an external Vector Database to store and retrieve embeddings.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "77fdfa86", + "metadata": {}, + "outputs": [], + "source": [ + "from semantic_kernel.connectors.memory.azure_cognitive_search import AzureCognitiveSearchMemoryStore\n", + "\n", + "azure_ai_search_api_key, azure_ai_search_url = sk.azure_aisearch_settings_from_dot_env()\n", + "\n", + "acs_memory_store = AzureCognitiveSearchMemoryStore(\n", + " vector_size=1536,\n", + " search_endpoint=azure_ai_search_url,\n", + " admin_key=azure_ai_search_api_key,\n", + ")\n", + "\n", + "memory = SemanticTextMemory(storage=acs_memory_store, embeddings_generator=embedding_gen)\n", + "kernel.add_plugin(TextMemoryPlugin(memory), \"TextMemoryPluginACS\")" + ] + }, + { + "cell_type": "markdown", + "id": "94f9e83b", + "metadata": {}, + "source": [ + "The implementation of Semantic Kernel allows to easily swap memory store for another. Here, we will re-use the functions we initially created for `VolatileMemoryStore` with our new external Vector Store leveraging Azure AI Search\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fc3da7e1", + "metadata": {}, + "outputs": [], + "source": [ + "await populate_memory(memory)" + ] + }, + { + "cell_type": "markdown", + "id": "b0bbe830", + "metadata": {}, + "source": [ + "Let's now try to query from Azure AI Search!\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1a09d0ca", + "metadata": {}, + "outputs": [], + "source": [ + "await search_memory_examples(memory)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We have laid the foundation which will allow us to store an arbitrary amount of data in an external Vector Store above and beyond what could fit in memory at the expense of a little more latency.\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 } diff --git a/python/notebooks/07-hugging-face-for-plugins.ipynb b/python/notebooks/07-hugging-face-for-plugins.ipynb index 57fc243f0a..a397819546 100644 --- a/python/notebooks/07-hugging-face-for-plugins.ipynb +++ b/python/notebooks/07-hugging-face-for-plugins.ipynb @@ -1,208 +1,204 @@ { - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "id": "68e1c158", - "metadata": {}, - "source": [ - "# Using Hugging Face With Plugins\n", - "\n", - "In this notebook, we demonstrate using Hugging Face models for Plugins using both SemanticMemory and text completions. \n", - "\n", - "SK supports downloading models from the Hugging Face that can perform the following tasks: text-generation, text2text-generation, summarization, and sentence-similarity. You can search for models by task at https://huggingface.co/models." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a77bdf89", - "metadata": {}, - "outputs": [], - "source": [ - "!python -m pip install semantic-kernel==0.9.5b1\n", - "\n", - "# Note that additional dependencies are required for the Hugging Face connectors:\n", - "!python -m pip install torch==2.0.0\n", - "!python -m pip install transformers==^4.28.1\n", - "!python -m pip install sentence-transformers==^2.2.2" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "508ad44f", - "metadata": {}, - "outputs": [], - "source": [ - "import semantic_kernel as sk\n", - "import semantic_kernel.connectors.ai.hugging_face as sk_hf\n", - "from semantic_kernel.memory.semantic_text_memory import SemanticTextMemory" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "753ab756", - "metadata": {}, - "outputs": [], - "source": [ - "from services import Service\n", - "\n", - "# Select a service to use for this notebook (available services: OpenAI, AzureOpenAI, HuggingFace)\n", - "selectedService = Service.HuggingFace" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "d8ddffc1", - "metadata": {}, - "source": [ - "First, we will create a kernel and add both text completion and embedding services. \n", - "\n", - "For text completion, we are choosing GPT2. This is a text-generation model. (Note: text-generation will repeat the input in the output, text2text-generation will not.)\n", - "For embeddings, we are using sentence-transformers/all-MiniLM-L6-v2. Vectors generated for this model are of length 384 (compared to a length of 1536 from OpenAI ADA).\n", - "\n", - "The following step may take a few minutes when run for the first time as the models will be downloaded to your local machine." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8f8dcbc6", - "metadata": {}, - "outputs": [], - "source": [ - "kernel = sk.Kernel()\n", - "\n", - "# Configure LLM service\n", - "if selectedService == Service.HuggingFace:\n", - " # Feel free to update this model to any other model available on Hugging Face\n", - " text_service_id = \"HuggingFaceM4/tiny-random-LlamaForCausalLM\"\n", - " kernel.add_service(\n", - " service=sk_hf.HuggingFaceTextCompletion(\n", - " service_id=text_service_id, ai_model_id=text_service_id, task=\"text-generation\"\n", - " ),\n", - " )\n", - " embed_service_id = \"sentence-transformers/all-MiniLM-L6-v2\"\n", - " embedding_svc = sk_hf.HuggingFaceTextEmbedding(service_id=embed_service_id, ai_model_id=embed_service_id)\n", - " kernel.add_service(\n", - " service=embedding_svc,\n", - " )\n", - " memory = SemanticTextMemory(storage=sk.memory.VolatileMemoryStore(), embeddings_generator=embedding_svc)\n", - " kernel.import_plugin_from_object(sk.core_plugins.TextMemoryPlugin(memory), \"TextMemoryPlugin\")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "2a7e7ca4", - "metadata": {}, - "source": [ - "### Add Memories and Define a plugin to use them\n", - "\n", - "Most models available on huggingface.co are not as powerful as OpenAI GPT-3+. Your plugins will likely need to be simpler to accommodate this." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d096504c", - "metadata": {}, - "outputs": [], - "source": [ - "collection_id = \"generic\"\n", - "\n", - "await memory.save_information(collection=collection_id, id=\"info1\", text=\"Sharks are fish.\")\n", - "await memory.save_information(collection=collection_id, id=\"info2\", text=\"Whales are mammals.\")\n", - "await memory.save_information(collection=collection_id, id=\"info3\", text=\"Penguins are birds.\")\n", - "await memory.save_information(collection=collection_id, id=\"info4\", text=\"Dolphins are mammals.\")\n", - "await memory.save_information(collection=collection_id, id=\"info5\", text=\"Flies are insects.\")\n", - "\n", - "# Define prompt function using SK prompt template language\n", - "my_prompt = \"\"\"I know these animal facts: \n", - "- {{recall 'fact about sharks'}}\n", - "- {{recall 'fact about whales'}} \n", - "- {{recall 'fact about penguins'}} \n", - "- {{recall 'fact about dolphins'}} \n", - "- {{recall 'fact about flies'}}\n", - "Now, tell me something about: {{$request}}\"\"\"\n", - "\n", - "execution_settings = sk_hf.HuggingFacePromptExecutionSettings(\n", - " service_id=text_service_id,\n", - " ai_model_id=text_service_id,\n", - " max_tokens=45,\n", - " temperature=0.5,\n", - " top_p=0.5,\n", - ")\n", - "\n", - "prompt_template_config = sk.PromptTemplateConfig(\n", - " template=my_prompt,\n", - " name=\"text_complete\",\n", - " template_format=\"semantic-kernel\",\n", - " execution_settings=execution_settings,\n", - ")\n", - "\n", - "my_function = kernel.create_function_from_prompt(\n", - " function_name=\"text_complete\",\n", - " plugin_name=\"TextCompletionPlugin\",\n", - " prompt_template_config=prompt_template_config,\n", - ")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "2calf857", - "metadata": {}, - "source": [ - "Let's now see what the completion looks like! Remember, \"gpt2\" is nowhere near as large as ChatGPT, so expect a much simpler answer." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "628c843e", - "metadata": {}, - "outputs": [], - "source": [ - "output = await kernel.invoke(\n", - " my_function,\n", - " request=\"What are whales?\",\n", - ")\n", - "\n", - "output = str(output).strip()\n", - "\n", - "query_result1 = await memory.search(\n", - " collection=collection_id, query=\"What are sharks?\", limit=1, min_relevance_score=0.3\n", - ")\n", - "\n", - "print(f\"The queried result for 'What are sharks?' is {query_result1[0].text}\")\n", - "\n", - "print(f\"{text_service_id} completed prompt with: '{output}'\")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.12" - } - }, - "nbformat": 4, - "nbformat_minor": 5 + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "id": "68e1c158", + "metadata": {}, + "source": [ + "# Using Hugging Face With Plugins\n", + "\n", + "In this notebook, we demonstrate using Hugging Face models for Plugins using both SemanticMemory and text completions.\n", + "\n", + "SK supports downloading models from the Hugging Face that can perform the following tasks: text-generation, text2text-generation, summarization, and sentence-similarity. You can search for models by task at https://huggingface.co/models.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a77bdf89", + "metadata": {}, + "outputs": [], + "source": [ + "!python -m pip install semantic-kernel==0.9.5b1\n", + "\n", + "# Note that additional dependencies are required for the Hugging Face connectors:\n", + "!python -m pip install torch==2.0.0\n", + "!python -m pip install transformers==^4.28.1\n", + "!python -m pip install sentence-transformers==^2.2.2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "508ad44f", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "753ab756", + "metadata": {}, + "outputs": [], + "source": [ + "from services import Service\n", + "\n", + "# Select a service to use for this notebook (available services: OpenAI, AzureOpenAI, HuggingFace)\n", + "selectedService = Service.HuggingFace" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "d8ddffc1", + "metadata": {}, + "source": [ + "First, we will create a kernel and add both text completion and embedding services.\n", + "\n", + "For text completion, we are choosing GPT2. This is a text-generation model. (Note: text-generation will repeat the input in the output, text2text-generation will not.)\n", + "For embeddings, we are using sentence-transformers/all-MiniLM-L6-v2. Vectors generated for this model are of length 384 (compared to a length of 1536 from OpenAI ADA).\n", + "\n", + "The following step may take a few minutes when run for the first time as the models will be downloaded to your local machine.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8f8dcbc6", + "metadata": {}, + "outputs": [], + "source": [ + "kernel = sk.Kernel()\n", + "\n", + "# Configure LLM service\n", + "if selectedService == Service.HuggingFace:\n", + " # Feel free to update this model to any other model available on Hugging Face\n", + " text_service_id = \"HuggingFaceM4/tiny-random-LlamaForCausalLM\"\n", + " kernel.add_service(\n", + " service=sk_hf.HuggingFaceTextCompletion(\n", + " service_id=text_service_id, ai_model_id=text_service_id, task=\"text-generation\"\n", + " ),\n", + " )\n", + " embed_service_id = \"sentence-transformers/all-MiniLM-L6-v2\"\n", + " embedding_svc = sk_hf.HuggingFaceTextEmbedding(service_id=embed_service_id, ai_model_id=embed_service_id)\n", + " kernel.add_service(\n", + " service=embedding_svc,\n", + " )\n", + " memory = SemanticTextMemory(storage=sk.memory.VolatileMemoryStore(), embeddings_generator=embedding_svc)\n", + " kernel.add_plugin(sk.core_plugins.TextMemoryPlugin(memory), \"TextMemoryPlugin\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "2a7e7ca4", + "metadata": {}, + "source": [ + "### Add Memories and Define a plugin to use them\n", + "\n", + "Most models available on huggingface.co are not as powerful as OpenAI GPT-3+. Your plugins will likely need to be simpler to accommodate this.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d096504c", + "metadata": {}, + "outputs": [], + "source": [ + "collection_id = \"generic\"\n", + "\n", + "await memory.save_information(collection=collection_id, id=\"info1\", text=\"Sharks are fish.\")\n", + "await memory.save_information(collection=collection_id, id=\"info2\", text=\"Whales are mammals.\")\n", + "await memory.save_information(collection=collection_id, id=\"info3\", text=\"Penguins are birds.\")\n", + "await memory.save_information(collection=collection_id, id=\"info4\", text=\"Dolphins are mammals.\")\n", + "await memory.save_information(collection=collection_id, id=\"info5\", text=\"Flies are insects.\")\n", + "\n", + "# Define prompt function using SK prompt template language\n", + "my_prompt = \"\"\"I know these animal facts: \n", + "- {{recall 'fact about sharks'}}\n", + "- {{recall 'fact about whales'}} \n", + "- {{recall 'fact about penguins'}} \n", + "- {{recall 'fact about dolphins'}} \n", + "- {{recall 'fact about flies'}}\n", + "Now, tell me something about: {{$request}}\"\"\"\n", + "\n", + "execution_settings = sk_hf.HuggingFacePromptExecutionSettings(\n", + " service_id=text_service_id,\n", + " ai_model_id=text_service_id,\n", + " max_tokens=45,\n", + " temperature=0.5,\n", + " top_p=0.5,\n", + ")\n", + "\n", + "prompt_template_config = sk.PromptTemplateConfig(\n", + " template=my_prompt,\n", + " name=\"text_complete\",\n", + " template_format=\"semantic-kernel\",\n", + " execution_settings=execution_settings,\n", + ")\n", + "\n", + "my_function = kernel.add_function(\n", + " function_name=\"text_complete\",\n", + " plugin_name=\"TextCompletionPlugin\",\n", + " prompt_template_config=prompt_template_config,\n", + ")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "2calf857", + "metadata": {}, + "source": [ + "Let's now see what the completion looks like! Remember, \"gpt2\" is nowhere near as large as ChatGPT, so expect a much simpler answer.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "628c843e", + "metadata": {}, + "outputs": [], + "source": [ + "output = await kernel.invoke(\n", + " my_function,\n", + " request=\"What are whales?\",\n", + ")\n", + "\n", + "output = str(output).strip()\n", + "\n", + "query_result1 = await memory.search(\n", + " collection=collection_id, query=\"What are sharks?\", limit=1, min_relevance_score=0.3\n", + ")\n", + "\n", + "print(f\"The queried result for 'What are sharks?' is {query_result1[0].text}\")\n", + "\n", + "print(f\"{text_service_id} completed prompt with: '{output}'\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 } diff --git a/python/notebooks/08-native-function-inline.ipynb b/python/notebooks/08-native-function-inline.ipynb index f59aab271f..c6479b1b77 100644 --- a/python/notebooks/08-native-function-inline.ipynb +++ b/python/notebooks/08-native-function-inline.ipynb @@ -1,680 +1,679 @@ { - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "id": "3c93ac5b", - "metadata": {}, - "source": [ - "# Running Native Functions\n" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "40201641", - "metadata": {}, - "source": [ - "Two of the previous notebooks showed how to [execute semantic functions inline](./03-semantic-function-inline.ipynb) and how to [run prompts from a file](./02-running-prompts-from-file.ipynb).\n", - "\n", - "In this notebook, we'll show how to use native functions from a file. We will also show how to call semantic functions from native functions.\n", - "\n", - "This can be useful in a few scenarios:\n", - "\n", - "- Writing logic around how to run a prompt that changes the prompt's outcome.\n", - "- Using external data sources to gather data to concatenate into your prompt.\n", - "- Validating user input data prior to sending it to the LLM prompt.\n", - "\n", - "Native functions are defined using standard Python code. The structure is simple, but not well documented at this point.\n", - "\n", - "The following examples are intended to help guide new users towards successful native & semantic function use with the SK Python framework.\n" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "d90b0c13", - "metadata": {}, - "source": [ - "Prepare a semantic kernel instance first, loading also the AI service settings defined in the [Setup notebook](00-getting-started.ipynb):\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1da651d4", - "metadata": {}, - "outputs": [], - "source": [ - "!python -m pip install semantic-kernel==0.9.5b1" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fddb5403", - "metadata": {}, - "outputs": [], - "source": [ - "from services import Service\n", - "\n", - "# Select a service to use for this notebook (available services: OpenAI, AzureOpenAI, HuggingFace)\n", - "selectedService = Service.OpenAI" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dd150646", - "metadata": {}, - "outputs": [], - "source": [ - "import semantic_kernel as sk\n", - "import semantic_kernel.connectors.ai.open_ai as sk_oai\n", - "from semantic_kernel.prompt_template.input_variable import InputVariable\n", - "\n", - "kernel = sk.Kernel()\n", - "\n", - "if selectedService == Service.AzureOpenAI:\n", - " from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion\n", - "\n", - " deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env()\n", - " service_id = \"aoai_chat\" # used later in the notebook\n", - " azure_chat_service = AzureChatCompletion(\n", - " service_id=service_id, deployment_name=\"gpt-35-turbo\", endpoint=endpoint, api_key=api_key\n", - " ) # set the deployment name to the value of your chat model\n", - " kernel.add_service(azure_chat_service)\n", - "\n", - "# Configure OpenAI service\n", - "if selectedService == Service.OpenAI:\n", - " from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion\n", - "\n", - " api_key, org_id = sk.openai_settings_from_dot_env()\n", - " service_id = \"oai_chat\" # used later in the notebook\n", - " oai_chat_service = OpenAIChatCompletion(\n", - " service_id=service_id, ai_model_id=\"gpt-4-turbo-1106\", api_key=api_key, org_id=org_id\n", - " )\n", - " kernel.add_service(oai_chat_service)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "186767f8", - "metadata": {}, - "source": [ - "Let's create a **native** function that gives us a random number between 3 and a user input as the upper limit. We'll use this number to create 3-x paragraphs of text when passed to a semantic function.\n" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "589733c5", - "metadata": {}, - "source": [ - "First, let's create our native function.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ae29c207", - "metadata": {}, - "outputs": [], - "source": [ - "import random\n", - "from semantic_kernel.functions import kernel_function\n", - "\n", - "\n", - "class GenerateNumberPlugin:\n", - " \"\"\"\n", - " Description: Generate a number between 3-x.\n", - " \"\"\"\n", - "\n", - " @kernel_function(\n", - " description=\"Generate a random number between 3-x\",\n", - " name=\"GenerateNumberThreeOrHigher\",\n", - " )\n", - " def generate_number_three_or_higher(self, input: str) -> str:\n", - " \"\"\"\n", - " Generate a number between 3-\n", - " Example:\n", - " \"8\" => rand(3,8)\n", - " Args:\n", - " input -- The upper limit for the random number generation\n", - " Returns:\n", - " int value\n", - " \"\"\"\n", - " try:\n", - " return str(random.randint(3, int(input)))\n", - " except ValueError as e:\n", - " print(f\"Invalid input {input}\")\n", - " raise e" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "f26b90c4", - "metadata": {}, - "source": [ - "Next, let's create a semantic function that accepts a number as `{{$input}}` and generates that number of paragraphs about two Corgis on an adventure. `$input` is a default variable semantic functions can use.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7890943f", - "metadata": {}, - "outputs": [], - "source": [ - "prompt = \"\"\"\n", - "Write a short story about two Corgis on an adventure.\n", - "The story must be:\n", - "- G rated\n", - "- Have a positive message\n", - "- No sexism, racism or other bias/bigotry\n", - "- Be exactly {{$input}} paragraphs long. It must be this length.\n", - "\"\"\"\n", - "\n", - "if selectedService == Service.OpenAI:\n", - " execution_settings = sk_oai.OpenAIChatPromptExecutionSettings(\n", - " service_id=service_id,\n", - " ai_model_id=\"gpt-3.5-turbo-1106\",\n", - " max_tokens=2000,\n", - " temperature=0.7,\n", - " )\n", - "elif selectedService == Service.AzureOpenAI:\n", - " execution_settings = sk_oai.OpenAIChatPromptExecutionSettings(\n", - " service_id=service_id,\n", - " ai_model_id=deployment,\n", - " max_tokens=2000,\n", - " temperature=0.7,\n", - " )\n", - "\n", - "prompt_template_config = sk.PromptTemplateConfig(\n", - " template=prompt,\n", - " name=\"story\",\n", - " template_format=\"semantic-kernel\",\n", - " input_variables=[\n", - " InputVariable(name=\"input\", description=\"The user input\", is_required=True),\n", - " ],\n", - " execution_settings=execution_settings,\n", - ")\n", - "\n", - "corgi_story = kernel.create_function_from_prompt(\n", - " function_name=\"CorgiStory\",\n", - " plugin_name=\"CorgiPlugin\",\n", - " prompt_template_config=prompt_template_config,\n", - ")\n", - "\n", - "generate_number_plugin = kernel.import_plugin_from_object(GenerateNumberPlugin(), \"GenerateNumberPlugin\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2471c2ab", - "metadata": {}, - "outputs": [], - "source": [ - "# Run the number generator\n", - "generate_number_three_or_higher = generate_number_plugin[\"GenerateNumberThreeOrHigher\"]\n", - "number_result = await generate_number_three_or_higher(kernel, input=6)\n", - "print(number_result)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f043a299", - "metadata": {}, - "outputs": [], - "source": [ - "story = await corgi_story.invoke(kernel, input=number_result.value)" - ] - }, - { - "cell_type": "markdown", - "id": "7245e7a2", - "metadata": {}, - "source": [ - "_Note: depending on which model you're using, it may not respond with the proper number of paragraphs._" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "59a60e2a", - "metadata": {}, - "outputs": [], - "source": [ - "print(f\"Generating a corgi story exactly {number_result.value} paragraphs long.\")\n", - "print(\"=====================================================\")\n", - "print(story)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "8ef29d16", - "metadata": {}, - "source": [ - "## Kernel Functions with Annotated Parameters\n", - "\n", - "That works! But let's expand on our example to make it more generic.\n", - "\n", - "For the native function, we'll introduce the lower limit variable. This means that a user will input two numbers and the number generator function will pick a number between the first and second input.\n", - "\n", - "We'll make use of the Python's `Annotated` class to hold these variables.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d54983d8", - "metadata": {}, - "outputs": [], - "source": [ - "import sys\n", - "import semantic_kernel as sk\n", - "from semantic_kernel.connectors.ai.open_ai import (\n", - " AzureChatCompletion,\n", - " OpenAIChatCompletion,\n", - ")\n", - "\n", - "if sys.version_info >= (3, 9):\n", - " from typing import Annotated\n", - "else:\n", - " from typing_extensions import Annotated\n", - "\n", - "kernel = sk.Kernel()\n", - "\n", - "if selectedService == Service.AzureOpenAI:\n", - " from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion\n", - "\n", - " deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env()\n", - " service_id = \"aoai_chat\" # used later in the notebook\n", - " azure_chat_service = AzureChatCompletion(\n", - " service_id=service_id, deployment_name=deployment, endpoint=endpoint, api_key=api_key\n", - " ) # set the deployment name to the value of your chat model\n", - " kernel.add_service(azure_chat_service)\n", - "\n", - "# Configure OpenAI service\n", - "if selectedService == Service.OpenAI:\n", - " from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion\n", - "\n", - " api_key, org_id = sk.openai_settings_from_dot_env()\n", - " service_id = \"oai_chat\" # used later in the notebook\n", - " oai_chat_service = OpenAIChatCompletion(\n", - " service_id=service_id, ai_model_id=\"gpt-4-turbo-1106\", api_key=api_key, org_id=org_id\n", - " )\n", - " kernel.add_service(oai_chat_service)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "091f45e4", - "metadata": {}, - "source": [ - "Let's start with the native function. Notice that we're add the `@kernel_function` decorator that holds the name of the function as well as an optional description. The input parameters are configured as part of the function's signature, and we use the `Annotated` type to specify the required input arguments.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4ea462c2", - "metadata": {}, - "outputs": [], - "source": [ - "import random\n", - "from semantic_kernel.functions import kernel_function\n", - "\n", - "\n", - "class GenerateNumberPlugin:\n", - " \"\"\"\n", - " Description: Generate a number between a min and a max.\n", - " \"\"\"\n", - "\n", - " @kernel_function(\n", - " name=\"GenerateNumber\",\n", - " description=\"Generate a random number between min and max\",\n", - " )\n", - " def generate_number(\n", - " self,\n", - " min: Annotated[int, \"the minimum number of paragraphs\"],\n", - " max: Annotated[int, \"the maximum number of paragraphs\"] = 10,\n", - " ) -> Annotated[int, \"the output is a number\"]:\n", - " \"\"\"\n", - " Generate a number between min-max\n", - " Example:\n", - " min=\"4\" max=\"10\" => rand(4,8)\n", - " Args:\n", - " min -- The lower limit for the random number generation\n", - " max -- The upper limit for the random number generation\n", - " Returns:\n", - " int value\n", - " \"\"\"\n", - " try:\n", - " return str(random.randint(min, max))\n", - " except ValueError as e:\n", - " print(f\"Invalid input {min} and {max}\")\n", - " raise e" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "48bcdf9e", - "metadata": {}, - "outputs": [], - "source": [ - "generate_number_plugin = kernel.import_plugin_from_object(GenerateNumberPlugin(), \"GenerateNumberPlugin\")\n", - "generate_number = generate_number_plugin[\"GenerateNumber\"]" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "6ad068d6", - "metadata": {}, - "source": [ - "Now let's also allow the semantic function to take in additional arguments. In this case, we're going to allow the our CorgiStory function to be written in a specified language. We'll need to provide a `paragraph_count` and a `language`.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8b8286fb", - "metadata": {}, - "outputs": [], - "source": [ - "prompt = \"\"\"\n", - "Write a short story about two Corgis on an adventure.\n", - "The story must be:\n", - "- G rated\n", - "- Have a positive message\n", - "- No sexism, racism or other bias/bigotry\n", - "- Be exactly {{$paragraph_count}} paragraphs long\n", - "- Be written in this language: {{$language}}\n", - "\"\"\"\n", - "\n", - "if selectedService == Service.OpenAI:\n", - " execution_settings = sk_oai.OpenAIChatPromptExecutionSettings(\n", - " service_id=service_id,\n", - " ai_model_id=\"gpt-3.5-turbo-1106\",\n", - " max_tokens=2000,\n", - " temperature=0.7,\n", - " )\n", - "elif selectedService == Service.AzureOpenAI:\n", - " execution_settings = sk_oai.OpenAIChatPromptExecutionSettings(\n", - " service_id=service_id,\n", - " ai_model_id=deployment,\n", - " max_tokens=2000,\n", - " temperature=0.7,\n", - " )\n", - "\n", - "prompt_template_config = sk.PromptTemplateConfig(\n", - " template=prompt,\n", - " name=\"summarize\",\n", - " template_format=\"semantic-kernel\",\n", - " input_variables=[\n", - " InputVariable(name=\"paragraph_count\", description=\"The number of paragraphs\", is_required=True),\n", - " InputVariable(name=\"language\", description=\"The language of the story\", is_required=True),\n", - " ],\n", - " execution_settings=execution_settings,\n", - ")\n", - "\n", - "corgi_story = kernel.create_function_from_prompt(\n", - " function_name=\"CorgiStory\",\n", - " plugin_name=\"CorgiPlugin\",\n", - " prompt_template_config=prompt_template_config,\n", - ")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "c8778bad", - "metadata": {}, - "source": [ - "Let's generate a paragraph count.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "28820d9d", - "metadata": {}, - "outputs": [], - "source": [ - "result = await generate_number.invoke(kernel, min=1, max=5)\n", - "num_paragraphs = result.value\n", - "print(f\"Generating a corgi story {num_paragraphs} paragraphs long.\")" - ] - }, - { - "cell_type": "markdown", - "id": "225a9147", - "metadata": {}, - "source": [ - "We can now invoke our corgi_story function using the `kernel` and the keyword arguments `paragraph_count` and `language`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dbe07c4d", - "metadata": {}, - "outputs": [], - "source": [ - "# Pass the output to the semantic story function\n", - "desired_language = \"Spanish\"\n", - "story = await corgi_story.invoke(kernel, paragraph_count=num_paragraphs, language=desired_language)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6732a30b", - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "print(f\"Generating a corgi story {num_paragraphs} paragraphs long in {desired_language}.\")\n", - "print(\"=====================================================\")\n", - "print(story)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "fb786c54", - "metadata": {}, - "source": [ - "## Calling Native Functions within a Semantic Function\n", - "\n", - "One neat thing about the Semantic Kernel is that you can also call native functions from within Prompt Functions!\n", - "\n", - "We will make our CorgiStory semantic function call a native function `GenerateNames` which will return names for our Corgi characters.\n", - "\n", - "We do this using the syntax `{{plugin_name.function_name}}`. You can read more about our prompte templating syntax [here](../../../docs/PROMPT_TEMPLATE_LANGUAGE.md).\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d84c7d84", - "metadata": {}, - "outputs": [], - "source": [ - "import random\n", - "from semantic_kernel.functions import kernel_function\n", - "\n", - "\n", - "class GenerateNamesPlugin:\n", - " \"\"\"\n", - " Description: Generate character names.\n", - " \"\"\"\n", - "\n", - " # The default function name will be the name of the function itself, however you can override this\n", - " # by setting the name= in the @kernel_function decorator. In this case, we're using\n", - " # the same name as the function name for simplicity.\n", - " @kernel_function(description=\"Generate character names\", name=\"generate_names\")\n", - " def generate_names(self) -> str:\n", - " \"\"\"\n", - " Generate two names.\n", - " Returns:\n", - " str\n", - " \"\"\"\n", - " names = {\"Hoagie\", \"Hamilton\", \"Bacon\", \"Pizza\", \"Boots\", \"Shorts\", \"Tuna\"}\n", - " first_name = random.choice(list(names))\n", - " names.remove(first_name)\n", - " second_name = random.choice(list(names))\n", - " return f\"{first_name}, {second_name}\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2ab7d65f", - "metadata": {}, - "outputs": [], - "source": [ - "generate_names_plugin = kernel.import_plugin_from_object(GenerateNamesPlugin(), plugin_name=\"GenerateNames\")\n", - "generate_names = generate_names_plugin[\"generate_names\"]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "94decd3e", - "metadata": {}, - "outputs": [], - "source": [ - "prompt = \"\"\"\n", - "Write a short story about two Corgis on an adventure.\n", - "The story must be:\n", - "- G rated\n", - "- Have a positive message\n", - "- No sexism, racism or other bias/bigotry\n", - "- Be exactly {{$paragraph_count}} paragraphs long\n", - "- Be written in this language: {{$language}}\n", - "- The two names of the corgis are {{GenerateNames.generate_names}}\n", - "\"\"\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "be72a503", - "metadata": {}, - "outputs": [], - "source": [ - "if selectedService == Service.OpenAI:\n", - " execution_settings = sk_oai.OpenAIChatPromptExecutionSettings(\n", - " service_id=service_id,\n", - " ai_model_id=\"gpt-3.5-turbo-1106\",\n", - " max_tokens=2000,\n", - " temperature=0.7,\n", - " )\n", - "elif selectedService == Service.AzureOpenAI:\n", - " execution_settings = sk_oai.OpenAIChatPromptExecutionSettings(\n", - " service_id=service_id,\n", - " ai_model_id=deployment,\n", - " max_tokens=2000,\n", - " temperature=0.7,\n", - " )\n", - "\n", - "prompt_template_config = sk.PromptTemplateConfig(\n", - " template=prompt,\n", - " name=\"corgi-new\",\n", - " template_format=\"semantic-kernel\",\n", - " input_variables=[\n", - " InputVariable(name=\"paragraph_count\", description=\"The number of paragraphs\", is_required=True),\n", - " InputVariable(name=\"language\", description=\"The language of the story\", is_required=True),\n", - " ],\n", - " execution_settings=execution_settings,\n", - ")\n", - "\n", - "corgi_story = kernel.create_function_from_prompt(\n", - " function_name=\"CorgiStoryUpdated\",\n", - " plugin_name=\"CorgiPluginUpdated\",\n", - " prompt_template_config=prompt_template_config,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "56e6cf0f", - "metadata": {}, - "outputs": [], - "source": [ - "result = await generate_number.invoke(kernel, min=1, max=5)\n", - "num_paragraphs = result.value" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7e980348", - "metadata": {}, - "outputs": [], - "source": [ - "desired_language = \"French\"\n", - "story = await corgi_story.invoke(kernel, paragraph_count=num_paragraphs, language=desired_language)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c4ade048", - "metadata": {}, - "outputs": [], - "source": [ - "print(f\"Generating a corgi story {num_paragraphs} paragraphs long in {desired_language}.\")\n", - "print(\"=====================================================\")\n", - "print(story)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "42f0c472", - "metadata": {}, - "source": [ - "### Recap\n", - "\n", - "A quick review of what we've learned here:\n", - "\n", - "- We've learned how to create native and prompt functions and register them to the kernel\n", - "- We've seen how we can use Kernel Arguments to pass in more custom variables into our prompt\n", - "- We've seen how we can call native functions within a prompt.\n" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.8" - } - }, - "nbformat": 4, - "nbformat_minor": 5 + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "id": "3c93ac5b", + "metadata": {}, + "source": [ + "# Running Native Functions\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "40201641", + "metadata": {}, + "source": [ + "Two of the previous notebooks showed how to [execute semantic functions inline](./03-semantic-function-inline.ipynb) and how to [run prompts from a file](./02-running-prompts-from-file.ipynb).\n", + "\n", + "In this notebook, we'll show how to use native functions from a file. We will also show how to call semantic functions from native functions.\n", + "\n", + "This can be useful in a few scenarios:\n", + "\n", + "- Writing logic around how to run a prompt that changes the prompt's outcome.\n", + "- Using external data sources to gather data to concatenate into your prompt.\n", + "- Validating user input data prior to sending it to the LLM prompt.\n", + "\n", + "Native functions are defined using standard Python code. The structure is simple, but not well documented at this point.\n", + "\n", + "The following examples are intended to help guide new users towards successful native & semantic function use with the SK Python framework.\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "d90b0c13", + "metadata": {}, + "source": [ + "Prepare a semantic kernel instance first, loading also the AI service settings defined in the [Setup notebook](00-getting-started.ipynb):\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1da651d4", + "metadata": {}, + "outputs": [], + "source": [ + "!python -m pip install semantic-kernel==0.9.5b1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fddb5403", + "metadata": {}, + "outputs": [], + "source": [ + "from services import Service\n", + "\n", + "# Select a service to use for this notebook (available services: OpenAI, AzureOpenAI, HuggingFace)\n", + "selectedService = Service.OpenAI" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dd150646", + "metadata": {}, + "outputs": [], + "source": [ + "import semantic_kernel as sk\n", + "\n", + "kernel = sk.Kernel()\n", + "\n", + "if selectedService == Service.AzureOpenAI:\n", + " from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion\n", + "\n", + " deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env()\n", + " service_id = \"aoai_chat\" # used later in the notebook\n", + " azure_chat_service = AzureChatCompletion(\n", + " service_id=service_id, deployment_name=\"gpt-35-turbo\", endpoint=endpoint, api_key=api_key\n", + " ) # set the deployment name to the value of your chat model\n", + " kernel.add_service(azure_chat_service)\n", + "\n", + "# Configure OpenAI service\n", + "if selectedService == Service.OpenAI:\n", + " from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion\n", + "\n", + " api_key, org_id = sk.openai_settings_from_dot_env()\n", + " service_id = \"oai_chat\" # used later in the notebook\n", + " oai_chat_service = OpenAIChatCompletion(\n", + " service_id=service_id, ai_model_id=\"gpt-4-turbo-1106\", api_key=api_key, org_id=org_id\n", + " )\n", + " kernel.add_service(oai_chat_service)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "186767f8", + "metadata": {}, + "source": [ + "Let's create a **native** function that gives us a random number between 3 and a user input as the upper limit. We'll use this number to create 3-x paragraphs of text when passed to a semantic function.\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "589733c5", + "metadata": {}, + "source": [ + "First, let's create our native function.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ae29c207", + "metadata": {}, + "outputs": [], + "source": [ + "import random\n", + "\n", + "from semantic_kernel.functions import kernel_function\n", + "\n", + "\n", + "class GenerateNumberPlugin:\n", + " \"\"\"\n", + " Description: Generate a number between 3-x.\n", + " \"\"\"\n", + "\n", + " @kernel_function(\n", + " description=\"Generate a random number between 3-x\",\n", + " name=\"GenerateNumberThreeOrHigher\",\n", + " )\n", + " def generate_number_three_or_higher(self, input: str) -> str:\n", + " \"\"\"\n", + " Generate a number between 3-\n", + " Example:\n", + " \"8\" => rand(3,8)\n", + " Args:\n", + " input -- The upper limit for the random number generation\n", + " Returns:\n", + " int value\n", + " \"\"\"\n", + " try:\n", + " return str(random.randint(3, int(input)))\n", + " except ValueError as e:\n", + " print(f\"Invalid input {input}\")\n", + " raise e" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "f26b90c4", + "metadata": {}, + "source": [ + "Next, let's create a semantic function that accepts a number as `{{$input}}` and generates that number of paragraphs about two Corgis on an adventure. `$input` is a default variable semantic functions can use.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7890943f", + "metadata": {}, + "outputs": [], + "source": [ + "prompt = \"\"\"\n", + "Write a short story about two Corgis on an adventure.\n", + "The story must be:\n", + "- G rated\n", + "- Have a positive message\n", + "- No sexism, racism or other bias/bigotry\n", + "- Be exactly {{$input}} paragraphs long. It must be this length.\n", + "\"\"\"\n", + "\n", + "if selectedService == Service.OpenAI:\n", + " execution_settings = sk_oai.OpenAIChatPromptExecutionSettings(\n", + " service_id=service_id,\n", + " ai_model_id=\"gpt-3.5-turbo-1106\",\n", + " max_tokens=2000,\n", + " temperature=0.7,\n", + " )\n", + "elif selectedService == Service.AzureOpenAI:\n", + " execution_settings = sk_oai.OpenAIChatPromptExecutionSettings(\n", + " service_id=service_id,\n", + " ai_model_id=deployment,\n", + " max_tokens=2000,\n", + " temperature=0.7,\n", + " )\n", + "\n", + "prompt_template_config = sk.PromptTemplateConfig(\n", + " template=prompt,\n", + " name=\"story\",\n", + " template_format=\"semantic-kernel\",\n", + " input_variables=[\n", + " InputVariable(name=\"input\", description=\"The user input\", is_required=True),\n", + " ],\n", + " execution_settings=execution_settings,\n", + ")\n", + "\n", + "corgi_story = kernel.add_function(\n", + " function_name=\"CorgiStory\",\n", + " plugin_name=\"CorgiPlugin\",\n", + " prompt_template_config=prompt_template_config,\n", + ")\n", + "\n", + "generate_number_plugin = kernel.add_plugin(GenerateNumberPlugin(), \"GenerateNumberPlugin\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2471c2ab", + "metadata": {}, + "outputs": [], + "source": [ + "# Run the number generator\n", + "generate_number_three_or_higher = generate_number_plugin[\"GenerateNumberThreeOrHigher\"]\n", + "number_result = await generate_number_three_or_higher(kernel, input=6)\n", + "print(number_result)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f043a299", + "metadata": {}, + "outputs": [], + "source": [ + "story = await corgi_story.invoke(kernel, input=number_result.value)" + ] + }, + { + "cell_type": "markdown", + "id": "7245e7a2", + "metadata": {}, + "source": [ + "_Note: depending on which model you're using, it may not respond with the proper number of paragraphs._\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "59a60e2a", + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"Generating a corgi story exactly {number_result.value} paragraphs long.\")\n", + "print(\"=====================================================\")\n", + "print(story)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "8ef29d16", + "metadata": {}, + "source": [ + "## Kernel Functions with Annotated Parameters\n", + "\n", + "That works! But let's expand on our example to make it more generic.\n", + "\n", + "For the native function, we'll introduce the lower limit variable. This means that a user will input two numbers and the number generator function will pick a number between the first and second input.\n", + "\n", + "We'll make use of the Python's `Annotated` class to hold these variables.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d54983d8", + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "import semantic_kernel as sk\n", + "from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion, OpenAIChatCompletion\n", + "\n", + "if sys.version_info >= (3, 9):\n", + " pass\n", + "else:\n", + " pass\n", + "\n", + "kernel = sk.Kernel()\n", + "\n", + "if selectedService == Service.AzureOpenAI:\n", + " from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion\n", + "\n", + " deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env()\n", + " service_id = \"aoai_chat\" # used later in the notebook\n", + " azure_chat_service = AzureChatCompletion(\n", + " service_id=service_id, deployment_name=deployment, endpoint=endpoint, api_key=api_key\n", + " ) # set the deployment name to the value of your chat model\n", + " kernel.add_service(azure_chat_service)\n", + "\n", + "# Configure OpenAI service\n", + "if selectedService == Service.OpenAI:\n", + " from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion\n", + "\n", + " api_key, org_id = sk.openai_settings_from_dot_env()\n", + " service_id = \"oai_chat\" # used later in the notebook\n", + " oai_chat_service = OpenAIChatCompletion(\n", + " service_id=service_id, ai_model_id=\"gpt-4-turbo-1106\", api_key=api_key, org_id=org_id\n", + " )\n", + " kernel.add_service(oai_chat_service)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "091f45e4", + "metadata": {}, + "source": [ + "Let's start with the native function. Notice that we're add the `@kernel_function` decorator that holds the name of the function as well as an optional description. The input parameters are configured as part of the function's signature, and we use the `Annotated` type to specify the required input arguments.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4ea462c2", + "metadata": {}, + "outputs": [], + "source": [ + "import random\n", + "\n", + "from semantic_kernel.functions import kernel_function\n", + "\n", + "\n", + "class GenerateNumberPlugin:\n", + " \"\"\"\n", + " Description: Generate a number between a min and a max.\n", + " \"\"\"\n", + "\n", + " @kernel_function(\n", + " name=\"GenerateNumber\",\n", + " description=\"Generate a random number between min and max\",\n", + " )\n", + " def generate_number(\n", + " self,\n", + " min: Annotated[int, \"the minimum number of paragraphs\"],\n", + " max: Annotated[int, \"the maximum number of paragraphs\"] = 10,\n", + " ) -> Annotated[int, \"the output is a number\"]:\n", + " \"\"\"\n", + " Generate a number between min-max\n", + " Example:\n", + " min=\"4\" max=\"10\" => rand(4,8)\n", + " Args:\n", + " min -- The lower limit for the random number generation\n", + " max -- The upper limit for the random number generation\n", + " Returns:\n", + " int value\n", + " \"\"\"\n", + " try:\n", + " return str(random.randint(min, max))\n", + " except ValueError as e:\n", + " print(f\"Invalid input {min} and {max}\")\n", + " raise e" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48bcdf9e", + "metadata": {}, + "outputs": [], + "source": [ + "generate_number_plugin = kernel.add_plugin(GenerateNumberPlugin(), \"GenerateNumberPlugin\")\n", + "generate_number = generate_number_plugin[\"GenerateNumber\"]" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "6ad068d6", + "metadata": {}, + "source": [ + "Now let's also allow the semantic function to take in additional arguments. In this case, we're going to allow the our CorgiStory function to be written in a specified language. We'll need to provide a `paragraph_count` and a `language`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8b8286fb", + "metadata": {}, + "outputs": [], + "source": [ + "prompt = \"\"\"\n", + "Write a short story about two Corgis on an adventure.\n", + "The story must be:\n", + "- G rated\n", + "- Have a positive message\n", + "- No sexism, racism or other bias/bigotry\n", + "- Be exactly {{$paragraph_count}} paragraphs long\n", + "- Be written in this language: {{$language}}\n", + "\"\"\"\n", + "\n", + "if selectedService == Service.OpenAI:\n", + " execution_settings = sk_oai.OpenAIChatPromptExecutionSettings(\n", + " service_id=service_id,\n", + " ai_model_id=\"gpt-3.5-turbo-1106\",\n", + " max_tokens=2000,\n", + " temperature=0.7,\n", + " )\n", + "elif selectedService == Service.AzureOpenAI:\n", + " execution_settings = sk_oai.OpenAIChatPromptExecutionSettings(\n", + " service_id=service_id,\n", + " ai_model_id=deployment,\n", + " max_tokens=2000,\n", + " temperature=0.7,\n", + " )\n", + "\n", + "prompt_template_config = sk.PromptTemplateConfig(\n", + " template=prompt,\n", + " name=\"summarize\",\n", + " template_format=\"semantic-kernel\",\n", + " input_variables=[\n", + " InputVariable(name=\"paragraph_count\", description=\"The number of paragraphs\", is_required=True),\n", + " InputVariable(name=\"language\", description=\"The language of the story\", is_required=True),\n", + " ],\n", + " execution_settings=execution_settings,\n", + ")\n", + "\n", + "corgi_story = kernel.add_function(\n", + " function_name=\"CorgiStory\",\n", + " plugin_name=\"CorgiPlugin\",\n", + " prompt_template_config=prompt_template_config,\n", + ")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "c8778bad", + "metadata": {}, + "source": [ + "Let's generate a paragraph count.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28820d9d", + "metadata": {}, + "outputs": [], + "source": [ + "result = await generate_number.invoke(kernel, min=1, max=5)\n", + "num_paragraphs = result.value\n", + "print(f\"Generating a corgi story {num_paragraphs} paragraphs long.\")" + ] + }, + { + "cell_type": "markdown", + "id": "225a9147", + "metadata": {}, + "source": [ + "We can now invoke our corgi_story function using the `kernel` and the keyword arguments `paragraph_count` and `language`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dbe07c4d", + "metadata": {}, + "outputs": [], + "source": [ + "# Pass the output to the semantic story function\n", + "desired_language = \"Spanish\"\n", + "story = await corgi_story.invoke(kernel, paragraph_count=num_paragraphs, language=desired_language)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6732a30b", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "print(f\"Generating a corgi story {num_paragraphs} paragraphs long in {desired_language}.\")\n", + "print(\"=====================================================\")\n", + "print(story)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "fb786c54", + "metadata": {}, + "source": [ + "## Calling Native Functions within a Semantic Function\n", + "\n", + "One neat thing about the Semantic Kernel is that you can also call native functions from within Prompt Functions!\n", + "\n", + "We will make our CorgiStory semantic function call a native function `GenerateNames` which will return names for our Corgi characters.\n", + "\n", + "We do this using the syntax `{{plugin_name.function_name}}`. You can read more about our prompte templating syntax [here](../../../docs/PROMPT_TEMPLATE_LANGUAGE.md).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d84c7d84", + "metadata": {}, + "outputs": [], + "source": [ + "import random\n", + "\n", + "from semantic_kernel.functions import kernel_function\n", + "\n", + "\n", + "class GenerateNamesPlugin:\n", + " \"\"\"\n", + " Description: Generate character names.\n", + " \"\"\"\n", + "\n", + " # The default function name will be the name of the function itself, however you can override this\n", + " # by setting the name= in the @kernel_function decorator. In this case, we're using\n", + " # the same name as the function name for simplicity.\n", + " @kernel_function(description=\"Generate character names\", name=\"generate_names\")\n", + " def generate_names(self) -> str:\n", + " \"\"\"\n", + " Generate two names.\n", + " Returns:\n", + " str\n", + " \"\"\"\n", + " names = {\"Hoagie\", \"Hamilton\", \"Bacon\", \"Pizza\", \"Boots\", \"Shorts\", \"Tuna\"}\n", + " first_name = random.choice(list(names))\n", + " names.remove(first_name)\n", + " second_name = random.choice(list(names))\n", + " return f\"{first_name}, {second_name}\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2ab7d65f", + "metadata": {}, + "outputs": [], + "source": [ + "generate_names_plugin = kernel.add_plugin(GenerateNamesPlugin(), plugin_name=\"GenerateNames\")\n", + "generate_names = generate_names_plugin[\"generate_names\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "94decd3e", + "metadata": {}, + "outputs": [], + "source": [ + "prompt = \"\"\"\n", + "Write a short story about two Corgis on an adventure.\n", + "The story must be:\n", + "- G rated\n", + "- Have a positive message\n", + "- No sexism, racism or other bias/bigotry\n", + "- Be exactly {{$paragraph_count}} paragraphs long\n", + "- Be written in this language: {{$language}}\n", + "- The two names of the corgis are {{GenerateNames.generate_names}}\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "be72a503", + "metadata": {}, + "outputs": [], + "source": [ + "if selectedService == Service.OpenAI:\n", + " execution_settings = sk_oai.OpenAIChatPromptExecutionSettings(\n", + " service_id=service_id,\n", + " ai_model_id=\"gpt-3.5-turbo-1106\",\n", + " max_tokens=2000,\n", + " temperature=0.7,\n", + " )\n", + "elif selectedService == Service.AzureOpenAI:\n", + " execution_settings = sk_oai.OpenAIChatPromptExecutionSettings(\n", + " service_id=service_id,\n", + " ai_model_id=deployment,\n", + " max_tokens=2000,\n", + " temperature=0.7,\n", + " )\n", + "\n", + "prompt_template_config = sk.PromptTemplateConfig(\n", + " template=prompt,\n", + " name=\"corgi-new\",\n", + " template_format=\"semantic-kernel\",\n", + " input_variables=[\n", + " InputVariable(name=\"paragraph_count\", description=\"The number of paragraphs\", is_required=True),\n", + " InputVariable(name=\"language\", description=\"The language of the story\", is_required=True),\n", + " ],\n", + " execution_settings=execution_settings,\n", + ")\n", + "\n", + "corgi_story = kernel.add_function(\n", + " function_name=\"CorgiStoryUpdated\",\n", + " plugin_name=\"CorgiPluginUpdated\",\n", + " prompt_template_config=prompt_template_config,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "56e6cf0f", + "metadata": {}, + "outputs": [], + "source": [ + "result = await generate_number.invoke(kernel, min=1, max=5)\n", + "num_paragraphs = result.value" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7e980348", + "metadata": {}, + "outputs": [], + "source": [ + "desired_language = \"French\"\n", + "story = await corgi_story.invoke(kernel, paragraph_count=num_paragraphs, language=desired_language)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c4ade048", + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"Generating a corgi story {num_paragraphs} paragraphs long in {desired_language}.\")\n", + "print(\"=====================================================\")\n", + "print(story)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "42f0c472", + "metadata": {}, + "source": [ + "### Recap\n", + "\n", + "A quick review of what we've learned here:\n", + "\n", + "- We've learned how to create native and prompt functions and register them to the kernel\n", + "- We've seen how we can use Kernel Arguments to pass in more custom variables into our prompt\n", + "- We've seen how we can call native functions within a prompt.\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.8" + } + }, + "nbformat": 4, + "nbformat_minor": 5 } diff --git a/python/notebooks/third_party/weaviate-persistent-memory.ipynb b/python/notebooks/third_party/weaviate-persistent-memory.ipynb index de7a3cfa8e..0e17ef91a7 100644 --- a/python/notebooks/third_party/weaviate-persistent-memory.ipynb +++ b/python/notebooks/third_party/weaviate-persistent-memory.ipynb @@ -1,521 +1,508 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Introduction\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This notebook shows how to replace the `VolatileMemoryStore` memory storage used in a [previous notebook](./06-memory-and-embeddings.ipynb) with a `WeaviateMemoryStore`.\n", - "\n", - "`WeaviateMemoryStore` is an example of a persistent (i.e. long-term) memory store backed by the Weaviate vector database.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# About Weaviate\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "[Weaviate](https://weaviate.io/) is an open-source vector database designed to scale seamlessly into billions of data objects. This implementation supports hybrid search out-of-the-box (meaning it will perform better for keyword searches).\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can run Weaviate in 5 ways:\n", - "\n", - "- **SaaS** – with [Weaviate Cloud Services (WCS)](https://weaviate.io/pricing).\n", - "\n", - " WCS is a fully managed service that takes care of hosting, scaling, and updating your Weaviate instance. You can try it out for free with a sandbox that lasts for 14 days.\n", - "\n", - " To set up a SaaS Weaviate instance with WCS:\n", - "\n", - " 1. Navigate to [Weaviate Cloud Console](https://console.weaviate.cloud/).\n", - " 2. Register or sign in to your WCS account.\n", - " 3. Create a new cluster with the following settings:\n", - " - `Subscription Tier` – Free sandbox for a free trial, or contact [hello@weaviate.io](mailto:hello@weaviate.io) for other options.\n", - " - `Cluster name` – a unique name for your cluster. The name will become part of the URL used to access this instance.\n", - " - `Enable Authentication?` – Enabled by default. This will generate a static API key that you can use to authenticate.\n", - " 4. Wait for a few minutes until your cluster is ready. You will see a green tick ✔️ when it's done. Copy your cluster URL.\n", - "\n", - "- **Hybrid SaaS**\n", - "\n", - " > If you need to keep your data on-premise for security or compliance reasons, Weaviate also offers a Hybrid SaaS option: Weaviate runs within your cloud instances, but the cluster is managed remotely by Weaviate. This gives you the benefits of a managed service without sending data to an external party.\n", - "\n", - " The Weaviate Hybrid SaaS is a custom solution. If you are interested in this option, please reach out to [hello@weaviate.io](mailto:hello@weaviate.io).\n", - "\n", - "- **Self-hosted** – with a Docker container\n", - "\n", - " To set up a Weaviate instance with Docker:\n", - "\n", - " 1. [Install Docker](https://docs.docker.com/engine/install/) on your local machine if it is not already installed.\n", - " 2. [Install the Docker Compose Plugin](https://docs.docker.com/compose/install/)\n", - " 3. Download a `docker-compose.yml` file with this `curl` command:\n", - "\n", - " ```\n", - " curl -o docker-compose.yml \"https://configuration.weaviate.io/v2/docker-compose/docker-compose.yml?modules=standalone&runtime=docker-compose&weaviate_version=v1.19.6\"\n", - " ```\n", - "\n", - " Alternatively, you can use Weaviate's docker compose [configuration tool](https://weaviate.io/developers/weaviate/installation/docker-compose) to generate your own `docker-compose.yml` file.\n", - "\n", - " 4. Run `docker compose up -d` to spin up a Weaviate instance.\n", - "\n", - " > To shut it down, run `docker compose down`.\n", - "\n", - "- **Self-hosted** – with a Kubernetes cluster\n", - "\n", - " To configure a self-hosted instance with Kubernetes, follow Weaviate's [documentation](https://weaviate.io/developers/weaviate/installation/kubernetes).|\n", - "\n", - "- **Embedded** - start a weaviate instance right from your application code using the client library\n", - "\n", - " This code snippet shows how to instantiate an embedded weaviate instance and upload a document:\n", - "\n", - " ```python\n", - " import weaviate\n", - " from weaviate.embedded import EmbeddedOptions\n", - "\n", - " client = weaviate.Client(\n", - " embedded_options=EmbeddedOptions()\n", - " )\n", - "\n", - " data_obj = {\n", - " \"name\": \"Chardonnay\",\n", - " \"description\": \"Goes with fish\"\n", - " }\n", - "\n", - " client.data_object.create(data_obj, \"Wine\")\n", - " ```\n", - "\n", - " Refer to the [documentation](https://weaviate.io/developers/weaviate/installation/embedded) for more details about this deployment method.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Setup\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!pip install semantic-kernel==0.3.8.dev0\n", - "!pip install weaviate-client\n", - "!pip install python-dotenv" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## OS-specific notes:\n", - "\n", - "- if you run into SSL errors when connecting to OpenAI on macOS, see this issue for a [potential solution](https://github.com/microsoft/semantic-kernel/issues/627#issuecomment-1580912248)\n", - "- on Windows, you may need to run Docker Desktop as administrator\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from typing import Tuple\n", - "\n", - "import semantic_kernel as sk\n", - "from semantic_kernel.connectors.ai.open_ai import (\n", - " OpenAIChatCompletion,\n", - " OpenAITextEmbedding,\n", - ")\n", - "\n", - "import os\n", - "\n", - "from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig\n", - "from semantic_kernel.memory.semantic_text_memory import SemanticTextMemory\n", - "from semantic_kernel.core_plugins.text_memory_plugin import TextMemoryPlugin" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "First, we instantiate the Weaviate memory store. Uncomment ONE of the options below, depending on how you want to use Weaviate:\n", - "\n", - "- from a Docker instance\n", - "- from WCS\n", - "- directly from the client (embedded Weaviate), which works on Linux only at the moment\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from semantic_kernel.connectors.memory.weaviate import weaviate_memory_store\n", - "from dotenv import load_dotenv\n", - "\n", - "load_dotenv(override=True)\n", - "\n", - "# Using Docker\n", - "config = weaviate_memory_store.WeaviateConfig(url=\"http://localhost:8080\")\n", - "\n", - "# Using WCS. Make sure the environment variables `WEAVIATE_URL` and `WEAVIATE_API_KEY`\n", - "# were set in the `.env` file.\n", - "#\n", - "# weaviate_api, weaviate_url = sk.weaviate_settings_from_dot_env()\n", - "#\n", - "# config = weaviate_memory_store.WeaviateConfig(\n", - "# url=weaviate_url,\n", - "# api_key=weaviate_api\n", - "# )\n", - "\n", - "# Using Embedded Weaviate\n", - "# config = weaviate_memory_store.WeaviateConfig(use_embed=True)\n", - "\n", - "store = weaviate_memory_store.WeaviateMemoryStore(config=config)\n", - "store.client.schema.delete_all()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Then, we register the memory store to the kernel:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from services import Service\n", - "\n", - "# Select a service to use for this notebook (available services: OpenAI, AzureOpenAI, HuggingFace)\n", - "selectedService = Service.OpenAI" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kernel = sk.Kernel()\n", - "\n", - "chat_service_id = \"chat\"\n", - "if selectedService == Service.OpenAI:\n", - " api_key, org_id = sk.openai_settings_from_dot_env()\n", - " oai_chat_service = OpenAIChatCompletion(\n", - " service_id=chat_service_id, ai_model_id=\"gpt-3.5-turbo\", api_key=api_key, org_id=org_id\n", - " )\n", - " embedding_gen = OpenAITextEmbedding(ai_model_id=\"text-embedding-ada-002\", api_key=api_key, org_id=org_id)\n", - " kernel.add_service(oai_chat_service)\n", - " kernel.add_service(embedding_gen)\n", - "\n", - "memory = SemanticTextMemory(storage=sk.memory.VolatileMemoryStore(), embeddings_generator=embedding_gen)\n", - "kernel.import_plugin_from_object(TextMemoryPlugin(memory), \"TextMemoryPlugin\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Manually adding memories\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's create some initial memories \"About Me\". We can add memories to our weaviate memory store by using `save_information`\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "collection_id = \"generic\"\n", - "\n", - "\n", - "async def populate_memory(memory: SemanticTextMemory) -> None:\n", - " # Add some documents to the semantic memory\n", - " await memory.save_information(collection=collection_id, id=\"info1\", text=\"Your budget for 2024 is $100,000\")\n", - " await memory.save_information(collection=collection_id, id=\"info2\", text=\"Your savings from 2023 are $50,000\")\n", - " await memory.save_information(collection=collection_id, id=\"info3\", text=\"Your investments are $80,000\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "await populate_memory(memory)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Searching is done through `search`:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "async def search_memory_examples(memory: SemanticTextMemory) -> None:\n", - " questions = [\"What is my budget for 2024?\", \"What are my savings from 2023?\", \"What are my investments?\"]\n", - "\n", - " for question in questions:\n", - " print(f\"Question: {question}\")\n", - " result = await memory.search(collection_id, question)\n", - " print(f\"Answer: {result[0].text}\\n\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "await search_memory_examples(memory)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Here's how to use the weaviate memory store in a chat application:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "async def setup_chat_with_memory(\n", - " kernel: sk.Kernel,\n", - " service_id: str,\n", - ") -> sk.KernelFunction:\n", - " prompt = \"\"\"\n", - " ChatBot can have a conversation with you about any topic.\n", - " It can give explicit instructions or say 'I don't know' if\n", - " it does not have an answer.\n", - "\n", - " Information about me, from previous conversations:\n", - " - {{recall 'budget by year'}} What is my budget for 2024?\n", - " - {{recall 'savings from previous year'}} What are my savings from 2023?\n", - " - {{recall 'investments'}} What are my investments?\n", - "\n", - " {{$request}}\n", - " \"\"\".strip()\n", - "\n", - " prompt_template_config = PromptTemplateConfig(\n", - " template=prompt,\n", - " execution_settings={\n", - " service_id: kernel.get_service(service_id).get_prompt_execution_settings_class()(service_id=service_id)\n", - " },\n", - " )\n", - "\n", - " chat_func = kernel.create_function_from_prompt(\n", - " function_name=\"chat_with_memory\",\n", - " plugin_name=\"TextMemoryPlugin\",\n", - " prompt_template_config=prompt_template_config,\n", - " )\n", - "\n", - " return chat_func" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "async def chat(kernel: sk.Kernel, chat_func: sk.KernelFunction) -> bool:\n", - " try:\n", - " user_input = input(\"User:> \")\n", - " except KeyboardInterrupt:\n", - " print(\"\\n\\nExiting chat...\")\n", - " return False\n", - " except EOFError:\n", - " print(\"\\n\\nExiting chat...\")\n", - " return False\n", - "\n", - " if user_input == \"exit\":\n", - " print(\"\\n\\nExiting chat...\")\n", - " return False\n", - "\n", - " answer = await kernel.invoke(chat_func, request=user_input)\n", - "\n", - " print(f\"ChatBot:> {answer}\")\n", - " return True" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(\"Populating memory...\")\n", - "await populate_memory(memory)\n", - "\n", - "print(\"Asking questions... (manually)\")\n", - "await search_memory_examples(memory)\n", - "\n", - "print(\"Setting up a chat (with memory!)\")\n", - "chat_func = await setup_chat_with_memory(kernel, chat_service_id)\n", - "\n", - "print(\"Begin chatting (type 'exit' to exit):\\n\")\n", - "print(\n", - " \"Welcome to the chat bot!\\\n", - " \\n Type 'exit' to exit.\\\n", - " \\n Try asking a question about your finances (i.e. \\\"talk to me about my finances\\\").\"\n", - ")\n", - "chatting = True\n", - "while chatting:\n", - " chatting = await chat(kernel, chat_func)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Adding documents to your memory\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Create a dictionary to hold some files. The key is the hyperlink to the file and the value is the file's content:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "github_files = {}\n", - "github_files[\"https://github.com/microsoft/semantic-kernel/blob/main/README.md\"] = (\n", - " \"README: Installation, getting started, and how to contribute\"\n", - ")\n", - "github_files[\n", - " \"https://github.com/microsoft/semantic-kernel/blob/main/dotnet/notebooks/02-running-prompts-from-file.ipynb\"\n", - "] = \"Jupyter notebook describing how to pass prompts from a file to a semantic plugin or function\"\n", - "github_files[\"https://github.com/microsoft/semantic-kernel/blob/main/dotnet/notebooks/00-getting-started.ipynb\"] = (\n", - " \"Jupyter notebook describing how to get started with the Semantic Kernel\"\n", - ")\n", - "github_files[\"https://github.com/microsoft/semantic-kernel/tree/main/samples/plugins/ChatPlugin/ChatGPT\"] = (\n", - " \"Sample demonstrating how to create a chat plugin interfacing with ChatGPT\"\n", - ")\n", - "github_files[\n", - " \"https://github.com/microsoft/semantic-kernel/blob/main/dotnet/src/SemanticKernel/Memory/Volatile/VolatileMemoryStore.cs\"\n", - "] = \"C# class that defines a volatile embedding store\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Use `save_reference` to save the file:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "COLLECTION = \"SKGitHub\"\n", - "\n", - "print(\"Adding some GitHub file URLs and their descriptions to a volatile Semantic Memory.\")\n", - "i = 0\n", - "for entry, value in github_files.items():\n", - " await memory.save_reference(\n", - " collection=COLLECTION,\n", - " description=value,\n", - " text=value,\n", - " external_id=entry,\n", - " external_source_name=\"GitHub\",\n", - " )\n", - " i += 1\n", - " print(\" URL {} saved\".format(i))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Use `search` to ask a question:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "ask = \"I love Jupyter notebooks, how should I get started?\"\n", - "print(\"===========================\\n\" + \"Query: \" + ask + \"\\n\")\n", - "\n", - "memories = await memory.search(COLLECTION, ask, limit=5, min_relevance_score=0.77)\n", - "\n", - "i = 0\n", - "for memory in memories:\n", - " i += 1\n", - " print(f\"Result {i}:\")\n", - " print(\" URL: : \" + memory.id)\n", - " print(\" Title : \" + memory.description)\n", - " print(\" Relevance: \" + str(memory.relevance))\n", - " print()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.13" - } - }, - "nbformat": 4, - "nbformat_minor": 2 + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Introduction\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This notebook shows how to replace the `VolatileMemoryStore` memory storage used in a [previous notebook](./06-memory-and-embeddings.ipynb) with a `WeaviateMemoryStore`.\n", + "\n", + "`WeaviateMemoryStore` is an example of a persistent (i.e. long-term) memory store backed by the Weaviate vector database.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# About Weaviate\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[Weaviate](https://weaviate.io/) is an open-source vector database designed to scale seamlessly into billions of data objects. This implementation supports hybrid search out-of-the-box (meaning it will perform better for keyword searches).\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can run Weaviate in 5 ways:\n", + "\n", + "- **SaaS** – with [Weaviate Cloud Services (WCS)](https://weaviate.io/pricing).\n", + "\n", + " WCS is a fully managed service that takes care of hosting, scaling, and updating your Weaviate instance. You can try it out for free with a sandbox that lasts for 14 days.\n", + "\n", + " To set up a SaaS Weaviate instance with WCS:\n", + "\n", + " 1. Navigate to [Weaviate Cloud Console](https://console.weaviate.cloud/).\n", + " 2. Register or sign in to your WCS account.\n", + " 3. Create a new cluster with the following settings:\n", + " - `Subscription Tier` – Free sandbox for a free trial, or contact [hello@weaviate.io](mailto:hello@weaviate.io) for other options.\n", + " - `Cluster name` – a unique name for your cluster. The name will become part of the URL used to access this instance.\n", + " - `Enable Authentication?` – Enabled by default. This will generate a static API key that you can use to authenticate.\n", + " 4. Wait for a few minutes until your cluster is ready. You will see a green tick ✔️ when it's done. Copy your cluster URL.\n", + "\n", + "- **Hybrid SaaS**\n", + "\n", + " > If you need to keep your data on-premise for security or compliance reasons, Weaviate also offers a Hybrid SaaS option: Weaviate runs within your cloud instances, but the cluster is managed remotely by Weaviate. This gives you the benefits of a managed service without sending data to an external party.\n", + "\n", + " The Weaviate Hybrid SaaS is a custom solution. If you are interested in this option, please reach out to [hello@weaviate.io](mailto:hello@weaviate.io).\n", + "\n", + "- **Self-hosted** – with a Docker container\n", + "\n", + " To set up a Weaviate instance with Docker:\n", + "\n", + " 1. [Install Docker](https://docs.docker.com/engine/install/) on your local machine if it is not already installed.\n", + " 2. [Install the Docker Compose Plugin](https://docs.docker.com/compose/install/)\n", + " 3. Download a `docker-compose.yml` file with this `curl` command:\n", + "\n", + " ```\n", + " curl -o docker-compose.yml \"https://configuration.weaviate.io/v2/docker-compose/docker-compose.yml?modules=standalone&runtime=docker-compose&weaviate_version=v1.19.6\"\n", + " ```\n", + "\n", + " Alternatively, you can use Weaviate's docker compose [configuration tool](https://weaviate.io/developers/weaviate/installation/docker-compose) to generate your own `docker-compose.yml` file.\n", + "\n", + " 4. Run `docker compose up -d` to spin up a Weaviate instance.\n", + "\n", + " > To shut it down, run `docker compose down`.\n", + "\n", + "- **Self-hosted** – with a Kubernetes cluster\n", + "\n", + " To configure a self-hosted instance with Kubernetes, follow Weaviate's [documentation](https://weaviate.io/developers/weaviate/installation/kubernetes).|\n", + "\n", + "- **Embedded** - start a weaviate instance right from your application code using the client library\n", + "\n", + " This code snippet shows how to instantiate an embedded weaviate instance and upload a document:\n", + "\n", + " ```python\n", + " import weaviate\n", + " from weaviate.embedded import EmbeddedOptions\n", + "\n", + " client = weaviate.Client(\n", + " embedded_options=EmbeddedOptions()\n", + " )\n", + "\n", + " data_obj = {\n", + " \"name\": \"Chardonnay\",\n", + " \"description\": \"Goes with fish\"\n", + " }\n", + "\n", + " client.data_object.create(data_obj, \"Wine\")\n", + " ```\n", + "\n", + " Refer to the [documentation](https://weaviate.io/developers/weaviate/installation/embedded) for more details about this deployment method.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Setup\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip install semantic-kernel==0.3.8.dev0\n", + "!pip install weaviate-client\n", + "!pip install python-dotenv" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## OS-specific notes:\n", + "\n", + "- if you run into SSL errors when connecting to OpenAI on macOS, see this issue for a [potential solution](https://github.com/microsoft/semantic-kernel/issues/627#issuecomment-1580912248)\n", + "- on Windows, you may need to run Docker Desktop as administrator\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First, we instantiate the Weaviate memory store. Uncomment ONE of the options below, depending on how you want to use Weaviate:\n", + "\n", + "- from a Docker instance\n", + "- from WCS\n", + "- directly from the client (embedded Weaviate), which works on Linux only at the moment\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from dotenv import load_dotenv\n", + "\n", + "from semantic_kernel.connectors.memory.weaviate import weaviate_memory_store\n", + "\n", + "load_dotenv(override=True)\n", + "\n", + "# Using Docker\n", + "config = weaviate_memory_store.WeaviateConfig(url=\"http://localhost:8080\")\n", + "\n", + "# Using WCS. Make sure the environment variables `WEAVIATE_URL` and `WEAVIATE_API_KEY`\n", + "# were set in the `.env` file.\n", + "#\n", + "# weaviate_api, weaviate_url = sk.weaviate_settings_from_dot_env()\n", + "#\n", + "# config = weaviate_memory_store.WeaviateConfig(\n", + "# url=weaviate_url,\n", + "# api_key=weaviate_api\n", + "# )\n", + "\n", + "# Using Embedded Weaviate\n", + "# config = weaviate_memory_store.WeaviateConfig(use_embed=True)\n", + "\n", + "store = weaviate_memory_store.WeaviateMemoryStore(config=config)\n", + "store.client.schema.delete_all()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then, we register the memory store to the kernel:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from services import Service\n", + "\n", + "# Select a service to use for this notebook (available services: OpenAI, AzureOpenAI, HuggingFace)\n", + "selectedService = Service.OpenAI" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "kernel = sk.Kernel()\n", + "\n", + "chat_service_id = \"chat\"\n", + "if selectedService == Service.OpenAI:\n", + " api_key, org_id = sk.openai_settings_from_dot_env()\n", + " oai_chat_service = OpenAIChatCompletion(\n", + " service_id=chat_service_id, ai_model_id=\"gpt-3.5-turbo\", api_key=api_key, org_id=org_id\n", + " )\n", + " embedding_gen = OpenAITextEmbedding(ai_model_id=\"text-embedding-ada-002\", api_key=api_key, org_id=org_id)\n", + " kernel.add_service(oai_chat_service)\n", + " kernel.add_service(embedding_gen)\n", + "\n", + "memory = SemanticTextMemory(storage=sk.memory.VolatileMemoryStore(), embeddings_generator=embedding_gen)\n", + "kernel.add_plugin(TextMemoryPlugin(memory), \"TextMemoryPlugin\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Manually adding memories\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's create some initial memories \"About Me\". We can add memories to our weaviate memory store by using `save_information`\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "collection_id = \"generic\"\n", + "\n", + "\n", + "async def populate_memory(memory: SemanticTextMemory) -> None:\n", + " # Add some documents to the semantic memory\n", + " await memory.save_information(collection=collection_id, id=\"info1\", text=\"Your budget for 2024 is $100,000\")\n", + " await memory.save_information(collection=collection_id, id=\"info2\", text=\"Your savings from 2023 are $50,000\")\n", + " await memory.save_information(collection=collection_id, id=\"info3\", text=\"Your investments are $80,000\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "await populate_memory(memory)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Searching is done through `search`:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "async def search_memory_examples(memory: SemanticTextMemory) -> None:\n", + " questions = [\"What is my budget for 2024?\", \"What are my savings from 2023?\", \"What are my investments?\"]\n", + "\n", + " for question in questions:\n", + " print(f\"Question: {question}\")\n", + " result = await memory.search(collection_id, question)\n", + " print(f\"Answer: {result[0].text}\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "await search_memory_examples(memory)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here's how to use the weaviate memory store in a chat application:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "async def setup_chat_with_memory(\n", + " kernel: sk.Kernel,\n", + " service_id: str,\n", + ") -> sk.KernelFunction:\n", + " prompt = \"\"\"\n", + " ChatBot can have a conversation with you about any topic.\n", + " It can give explicit instructions or say 'I don't know' if\n", + " it does not have an answer.\n", + "\n", + " Information about me, from previous conversations:\n", + " - {{recall 'budget by year'}} What is my budget for 2024?\n", + " - {{recall 'savings from previous year'}} What are my savings from 2023?\n", + " - {{recall 'investments'}} What are my investments?\n", + "\n", + " {{$request}}\n", + " \"\"\".strip()\n", + "\n", + " prompt_template_config = PromptTemplateConfig(\n", + " template=prompt,\n", + " execution_settings={\n", + " service_id: kernel.get_service(service_id).get_prompt_execution_settings_class()(service_id=service_id)\n", + " },\n", + " )\n", + "\n", + " chat_func = kernel.add_function(\n", + " function_name=\"chat_with_memory\",\n", + " plugin_name=\"TextMemoryPlugin\",\n", + " prompt_template_config=prompt_template_config,\n", + " )\n", + "\n", + " return chat_func" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "async def chat(kernel: sk.Kernel, chat_func: sk.KernelFunction) -> bool:\n", + " try:\n", + " user_input = input(\"User:> \")\n", + " except KeyboardInterrupt:\n", + " print(\"\\n\\nExiting chat...\")\n", + " return False\n", + " except EOFError:\n", + " print(\"\\n\\nExiting chat...\")\n", + " return False\n", + "\n", + " if user_input == \"exit\":\n", + " print(\"\\n\\nExiting chat...\")\n", + " return False\n", + "\n", + " answer = await kernel.invoke(chat_func, request=user_input)\n", + "\n", + " print(f\"ChatBot:> {answer}\")\n", + " return True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Populating memory...\")\n", + "await populate_memory(memory)\n", + "\n", + "print(\"Asking questions... (manually)\")\n", + "await search_memory_examples(memory)\n", + "\n", + "print(\"Setting up a chat (with memory!)\")\n", + "chat_func = await setup_chat_with_memory(kernel, chat_service_id)\n", + "\n", + "print(\"Begin chatting (type 'exit' to exit):\\n\")\n", + "print(\n", + " \"Welcome to the chat bot!\\\n", + " \\n Type 'exit' to exit.\\\n", + " \\n Try asking a question about your finances (i.e. \\\"talk to me about my finances\\\").\"\n", + ")\n", + "chatting = True\n", + "while chatting:\n", + " chatting = await chat(kernel, chat_func)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Adding documents to your memory\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a dictionary to hold some files. The key is the hyperlink to the file and the value is the file's content:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "github_files = {}\n", + "github_files[\"https://github.com/microsoft/semantic-kernel/blob/main/README.md\"] = (\n", + " \"README: Installation, getting started, and how to contribute\"\n", + ")\n", + "github_files[\n", + " \"https://github.com/microsoft/semantic-kernel/blob/main/dotnet/notebooks/02-running-prompts-from-file.ipynb\"\n", + "] = \"Jupyter notebook describing how to pass prompts from a file to a semantic plugin or function\"\n", + "github_files[\"https://github.com/microsoft/semantic-kernel/blob/main/dotnet/notebooks/00-getting-started.ipynb\"] = (\n", + " \"Jupyter notebook describing how to get started with the Semantic Kernel\"\n", + ")\n", + "github_files[\"https://github.com/microsoft/semantic-kernel/tree/main/samples/plugins/ChatPlugin/ChatGPT\"] = (\n", + " \"Sample demonstrating how to create a chat plugin interfacing with ChatGPT\"\n", + ")\n", + "github_files[\n", + " \"https://github.com/microsoft/semantic-kernel/blob/main/dotnet/src/SemanticKernel/Memory/Volatile/VolatileMemoryStore.cs\"\n", + "] = \"C# class that defines a volatile embedding store\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Use `save_reference` to save the file:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "COLLECTION = \"SKGitHub\"\n", + "\n", + "print(\"Adding some GitHub file URLs and their descriptions to a volatile Semantic Memory.\")\n", + "i = 0\n", + "for entry, value in github_files.items():\n", + " await memory.save_reference(\n", + " collection=COLLECTION,\n", + " description=value,\n", + " text=value,\n", + " external_id=entry,\n", + " external_source_name=\"GitHub\",\n", + " )\n", + " i += 1\n", + " print(\" URL {} saved\".format(i))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Use `search` to ask a question:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ask = \"I love Jupyter notebooks, how should I get started?\"\n", + "print(\"===========================\\n\" + \"Query: \" + ask + \"\\n\")\n", + "\n", + "memories = await memory.search(COLLECTION, ask, limit=5, min_relevance_score=0.77)\n", + "\n", + "i = 0\n", + "for memory in memories:\n", + " i += 1\n", + " print(f\"Result {i}:\")\n", + " print(\" URL: : \" + memory.id)\n", + " print(\" Title : \" + memory.description)\n", + " print(\" Relevance: \" + str(memory.relevance))\n", + " print()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.13" + } + }, + "nbformat": 4, + "nbformat_minor": 2 } diff --git a/python/samples/documentation_examples/configuring_prompts.py b/python/samples/documentation_examples/configuring_prompts.py index d7e7b1a4b1..d0588be805 100644 --- a/python/samples/documentation_examples/configuring_prompts.py +++ b/python/samples/documentation_examples/configuring_prompts.py @@ -32,7 +32,7 @@ async def main(): ) # Import the ConversationSummaryPlugin - kernel.import_plugin_from_object( + kernel.add_plugin( ConversationSummaryPlugin(kernel=kernel, prompt_template_config=prompt_template_config), plugin_name="ConversationSummaryPlugin", ) @@ -58,7 +58,7 @@ async def main(): ) # Create the function - chat_function = kernel.create_function_from_prompt( + chat_function = kernel.add_function( prompt=prompt, plugin_name="Summarize_Conversation", function_name="Chat", diff --git a/python/samples/documentation_examples/functions_within_prompts.py b/python/samples/documentation_examples/functions_within_prompts.py index 8e9b591493..d467e89b91 100644 --- a/python/samples/documentation_examples/functions_within_prompts.py +++ b/python/samples/documentation_examples/functions_within_prompts.py @@ -31,7 +31,7 @@ async def main(): ) # Import the ConversationSummaryPlugin - kernel.import_plugin_from_object( + kernel.add_plugin( ConversationSummaryPlugin(kernel=kernel, prompt_template_config=prompt_template_config), plugin_name="ConversationSummaryPlugin", ) @@ -56,7 +56,7 @@ async def main(): ) # Run the prompt - chat_function = kernel.create_function_from_prompt( + chat_function = kernel.add_function( prompt=prompt, plugin_name="Summarize_Conversation", function_name="Chat", diff --git a/python/samples/documentation_examples/plugin.py b/python/samples/documentation_examples/plugin.py index c9f10fab7c..264ee7b383 100644 --- a/python/samples/documentation_examples/plugin.py +++ b/python/samples/documentation_examples/plugin.py @@ -52,7 +52,7 @@ async def main(): # use_chat: True to use chat completion, False to use text completion kernel = add_service(kernel=kernel, use_chat=True) - light_plugin = kernel.import_plugin_from_object( + light_plugin = kernel.add_plugin( LightPlugin(), plugin_name="LightPlugin", ) diff --git a/python/samples/documentation_examples/plugins/MathPlugin/native_function.py b/python/samples/documentation_examples/plugins/MathPlugin/native_function.py index 6a48c148c4..a862b7d336 100644 --- a/python/samples/documentation_examples/plugins/MathPlugin/native_function.py +++ b/python/samples/documentation_examples/plugins/MathPlugin/native_function.py @@ -14,7 +14,7 @@ class Math: Description: MathPlugin provides a set of functions to make Math calculations. Usage: - kernel.import_plugin_from_object(MathPlugin(), plugin_name="math") + kernel.add_plugin(MathPlugin(), plugin_name="math") Examples: {{math.Add}} => Returns the sum of input and amount (provided in the KernelArguments) diff --git a/python/samples/documentation_examples/prompts.py b/python/samples/documentation_examples/prompts.py index 5e44b9ada8..b227b4360c 100644 --- a/python/samples/documentation_examples/prompts.py +++ b/python/samples/documentation_examples/prompts.py @@ -21,9 +21,7 @@ async def main(): prompt = f"What is the intent of this request? {request}" print("0.0 Initial prompt") print("-------------------------") - prompt_function = kernel.create_function_from_prompt( - function_name="sample_zero", plugin_name="sample_plugin", prompt=prompt - ) + prompt_function = kernel.add_function(function_name="sample_zero", plugin_name="sample_plugin", prompt=prompt) result = await kernel.invoke(prompt_function, request=request) print(result) print("-------------------------") @@ -33,9 +31,7 @@ async def main(): You can choose between SendEmail, SendMessage, CompleteTask, CreateDocument.""" print("1.0 Make the prompt more specific") print("-------------------------") - prompt_function = kernel.create_function_from_prompt( - function_name="sample_one", plugin_name="sample_plugin", prompt=prompt - ) + prompt_function = kernel.add_function(function_name="sample_one", plugin_name="sample_plugin", prompt=prompt) result = await kernel.invoke(prompt_function, request=request) print(result) print("-------------------------") @@ -47,9 +43,7 @@ async def main(): Intent: """ print("2.0 Add structure to the output with formatting") print("-------------------------") - prompt_function = kernel.create_function_from_prompt( - function_name="sample_two", plugin_name="sample_plugin", prompt=prompt - ) + prompt_function = kernel.add_function(function_name="sample_two", plugin_name="sample_plugin", prompt=prompt) result = await kernel.invoke(prompt_function, request=request) print(result) print("-------------------------") @@ -80,9 +74,7 @@ async def main(): ## Intent""" print("2.1 Add structure to the output with formatting (using Markdown and JSON)") print("-------------------------") - prompt_function = kernel.create_function_from_prompt( - function_name="sample_two_one", plugin_name="sample_plugin", prompt=prompt - ) + prompt_function = kernel.add_function(function_name="sample_two_one", plugin_name="sample_plugin", prompt=prompt) result = await kernel.invoke(prompt_function, request=request) print(result) print("-------------------------") @@ -101,9 +93,7 @@ async def main(): Intent: """ print("3.0 Provide examples with few-shot prompting") print("-------------------------") - prompt_function = kernel.create_function_from_prompt( - function_name="sample_three", plugin_name="sample_plugin", prompt=prompt - ) + prompt_function = kernel.add_function(function_name="sample_three", plugin_name="sample_plugin", prompt=prompt) result = await kernel.invoke(prompt_function, request=request) print(result) print("-------------------------") @@ -123,9 +113,7 @@ async def main(): Intent: """ print("4.0 Tell the AI what to do to avoid doing something wrong") print("-------------------------") - prompt_function = kernel.create_function_from_prompt( - function_name="sample_four", plugin_name="sample_plugin", prompt=prompt - ) + prompt_function = kernel.add_function(function_name="sample_four", plugin_name="sample_plugin", prompt=prompt) result = await kernel.invoke(prompt_function, request=request) print(result) print("-------------------------") @@ -150,9 +138,7 @@ async def main(): Intent: """ print("5.0 Provide context to the AI") print("-------------------------") - prompt_function = kernel.create_function_from_prompt( - function_name="sample_five", plugin_name="sample_plugin", prompt=prompt - ) + prompt_function = kernel.add_function(function_name="sample_five", plugin_name="sample_plugin", prompt=prompt) result = await kernel.invoke(prompt_function, request=request, history=history) print(result) print("-------------------------") diff --git a/python/samples/documentation_examples/serializing_prompts.py b/python/samples/documentation_examples/serializing_prompts.py index 68d8f8a295..4ced1ee369 100644 --- a/python/samples/documentation_examples/serializing_prompts.py +++ b/python/samples/documentation_examples/serializing_prompts.py @@ -45,7 +45,7 @@ async def main(): ) # Import the ConversationSummaryPlugin - kernel.import_plugin_from_object( + kernel.add_plugin( ConversationSummaryPlugin(kernel=kernel, prompt_template_config=prompt_template_config), plugin_name="ConversationSummaryPlugin", ) diff --git a/python/samples/documentation_examples/templates.py b/python/samples/documentation_examples/templates.py index 62bedc10ef..0c17754e1c 100644 --- a/python/samples/documentation_examples/templates.py +++ b/python/samples/documentation_examples/templates.py @@ -40,7 +40,7 @@ async def main(): ) # Run the prompt - chat_function = kernel.create_function_from_prompt( + chat_function = kernel.add_function( prompt=prompt, plugin_name="Summarize_Conversation", function_name="Chat", diff --git a/python/samples/documentation_examples/using_the_kernel.py b/python/samples/documentation_examples/using_the_kernel.py index fed662b27a..27ad67dfcd 100644 --- a/python/samples/documentation_examples/using_the_kernel.py +++ b/python/samples/documentation_examples/using_the_kernel.py @@ -18,7 +18,7 @@ async def main(): kernel = add_service(kernel=kernel, use_chat=True) # Import the TimePlugin - time = kernel.import_plugin_from_object(TimePlugin(), "TimePlugin") + time = kernel.add_plugin(TimePlugin(), "TimePlugin") # Import the WriterPlugin from the plugins directory. script_directory = os.path.dirname(__file__) diff --git a/python/samples/kernel-syntax-examples/action_planner.py b/python/samples/kernel-syntax-examples/action_planner.py index dfb87040f8..548a008420 100644 --- a/python/samples/kernel-syntax-examples/action_planner.py +++ b/python/samples/kernel-syntax-examples/action_planner.py @@ -18,9 +18,9 @@ async def main(): OpenAIChatCompletion(service_id=service_id, ai_model_id="gpt-3.5-turbo", api_key=api_key, org_id=org_id) ) - kernel.import_plugin_from_object(MathPlugin(), "math") - kernel.import_plugin_from_object(TimePlugin(), "time") - kernel.import_plugin_from_object(TextPlugin(), "text") + kernel.add_plugin(MathPlugin(), "math") + kernel.add_plugin(TimePlugin(), "time") + kernel.add_plugin(TextPlugin(), "text") # create an instance of action planner. planner = ActionPlanner(kernel, service_id) diff --git a/python/samples/kernel-syntax-examples/azure_chat_gpt_api.py b/python/samples/kernel-syntax-examples/azure_chat_gpt_api.py index d224e9dafd..bb09c09e46 100644 --- a/python/samples/kernel-syntax-examples/azure_chat_gpt_api.py +++ b/python/samples/kernel-syntax-examples/azure_chat_gpt_api.py @@ -48,7 +48,7 @@ req_settings.auto_invoke_kernel_functions = True ## The third method is the most specific as the returned request settings class is the one that is registered for the service and has some fields already filled in, like the service_id and ai_model_id. # noqa: E501 E266 -chat_function = kernel.create_function_from_prompt( +chat_function = kernel.add_function( prompt=system_message + """{{$chat_history}}{{$user_input}}""", function_name="chat", plugin_name="chat", diff --git a/python/samples/kernel-syntax-examples/azure_chat_gpt_api_handlebars.py b/python/samples/kernel-syntax-examples/azure_chat_gpt_api_handlebars.py index 3e9c12b93d..904206ae05 100644 --- a/python/samples/kernel-syntax-examples/azure_chat_gpt_api_handlebars.py +++ b/python/samples/kernel-syntax-examples/azure_chat_gpt_api_handlebars.py @@ -35,7 +35,7 @@ req_settings.top_p = 0.8 req_settings.auto_invoke_kernel_functions = False -chat_function = kernel.create_function_from_prompt( +chat_function = kernel.add_function( prompt="""{{system_message}}{{#each chat_history}}{{#message role=role}}{{~content~}}{{/message}} {{/each}}""", function_name="chat", plugin_name="chat", diff --git a/python/samples/kernel-syntax-examples/azure_chat_gpt_api_jinja2.py b/python/samples/kernel-syntax-examples/azure_chat_gpt_api_jinja2.py index fb56619a96..5ad39ce4be 100644 --- a/python/samples/kernel-syntax-examples/azure_chat_gpt_api_jinja2.py +++ b/python/samples/kernel-syntax-examples/azure_chat_gpt_api_jinja2.py @@ -35,7 +35,7 @@ req_settings.top_p = 0.8 req_settings.auto_invoke_kernel_functions = False -chat_function = kernel.create_function_from_prompt( +chat_function = kernel.add_function( prompt="""{{system_message}}{% for item in chat_history %}{{ message(item) }}{% endfor %}""", function_name="chat", plugin_name="chat", diff --git a/python/samples/kernel-syntax-examples/azure_chat_gpt_with_data_api.py b/python/samples/kernel-syntax-examples/azure_chat_gpt_with_data_api.py index 43e51381f1..1111152199 100644 --- a/python/samples/kernel-syntax-examples/azure_chat_gpt_with_data_api.py +++ b/python/samples/kernel-syntax-examples/azure_chat_gpt_with_data_api.py @@ -69,7 +69,7 @@ prompt_template_config = PromptTemplateConfig( ], execution_settings={"default": req_settings}, ) -chat_function = kernel.create_function_from_prompt( +chat_function = kernel.add_function( plugin_name="ChatBot", function_name="Chat", prompt_template_config=prompt_template_config ) diff --git a/python/samples/kernel-syntax-examples/azure_chat_gpt_with_data_api_function_calling.py b/python/samples/kernel-syntax-examples/azure_chat_gpt_with_data_api_function_calling.py index 362838a495..4827e0a388 100644 --- a/python/samples/kernel-syntax-examples/azure_chat_gpt_with_data_api_function_calling.py +++ b/python/samples/kernel-syntax-examples/azure_chat_gpt_with_data_api_function_calling.py @@ -11,9 +11,7 @@ from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.azure_chat_ AzureChatPromptExecutionSettings, ExtraBody, ) -from semantic_kernel.connectors.ai.open_ai.utils import ( - get_tool_call_object, -) +from semantic_kernel.connectors.ai.open_ai.utils import get_tool_call_object from semantic_kernel.contents.chat_history import ChatHistory from semantic_kernel.core_plugins.time_plugin import TimePlugin from semantic_kernel.functions.kernel_arguments import KernelArguments @@ -59,7 +57,7 @@ plugins_directory = os.path.join(__file__, "../../../../samples/plugins") # the joke plugin in the FunPlugins is a semantic plugin and has the function calling disabled. kernel.import_plugin_from_prompt_directory(plugins_directory, "FunPlugin") # the math plugin is a core plugin and has the function calling enabled. -kernel.import_plugin_from_object(TimePlugin(), plugin_name="time") +kernel.add_plugin(TimePlugin(), plugin_name="time") # enabling or disabling function calling is done by setting the tool_choice parameter for the completion. # when the tool_choice parameter is set to "auto" the model will decide which function to use, if any. @@ -81,7 +79,7 @@ history = ChatHistory() history.add_user_message("Hi there, who are you?") history.add_assistant_message("I am an AI assistant here to answer your questions.") -chat_function = kernel.create_function_from_prompt( +chat_function = kernel.add_function( plugin_name="ChatBot", function_name="Chat", prompt_template_config=prompt_template_config ) diff --git a/python/samples/kernel-syntax-examples/azure_chat_gpt_with_data_api_vector_search.py b/python/samples/kernel-syntax-examples/azure_chat_gpt_with_data_api_vector_search.py index 961330f500..93a2132ef8 100644 --- a/python/samples/kernel-syntax-examples/azure_chat_gpt_with_data_api_vector_search.py +++ b/python/samples/kernel-syntax-examples/azure_chat_gpt_with_data_api_vector_search.py @@ -5,9 +5,7 @@ import logging import semantic_kernel as sk import semantic_kernel.connectors.ai.open_ai as sk_oai -from semantic_kernel.connectors.ai.open_ai.contents.azure_chat_message_content import ( - AzureChatMessageContent, -) +from semantic_kernel.connectors.ai.open_ai.contents.azure_chat_message_content import AzureChatMessageContent from semantic_kernel.connectors.ai.open_ai.contents.function_call import FunctionCall from semantic_kernel.connectors.ai.open_ai.contents.tool_calls import ToolCall from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.azure_chat_prompt_execution_settings import ( @@ -82,7 +80,7 @@ chat_history = ChatHistory() chat_history.add_user_message("Hi there, who are you?") chat_history.add_assistant_message("I am an AI assistant here to answer your questions.") -chat_function = kernel.create_function_from_prompt( +chat_function = kernel.add_function( plugin_name="ChatBot", function_name="Chat", prompt_template_config=prompt_template_config ) diff --git a/python/samples/kernel-syntax-examples/azure_cognitive_search_memory.py b/python/samples/kernel-syntax-examples/azure_cognitive_search_memory.py index e6119afb4a..480f55ae11 100644 --- a/python/samples/kernel-syntax-examples/azure_cognitive_search_memory.py +++ b/python/samples/kernel-syntax-examples/azure_cognitive_search_memory.py @@ -82,7 +82,7 @@ async def main() -> None: ) memory = SemanticTextMemory(storage=acs_connector, embeddings_generator=embedding_gen) - kernel.import_plugin_from_object(sk.core_plugins.TextMemoryPlugin(memory), "TextMemoryPlugin") + kernel.add_plugin(sk.core_plugins.TextMemoryPlugin(memory), "TextMemoryPlugin") print("Populating memory...") await populate_memory(kernel) diff --git a/python/samples/kernel-syntax-examples/azure_openai_function_calling_stepwise_planner.py b/python/samples/kernel-syntax-examples/azure_openai_function_calling_stepwise_planner.py index d6967d7bf6..e7f4e68676 100644 --- a/python/samples/kernel-syntax-examples/azure_openai_function_calling_stepwise_planner.py +++ b/python/samples/kernel-syntax-examples/azure_openai_function_calling_stepwise_planner.py @@ -31,8 +31,8 @@ async def main(): cur_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "resources") kernel.import_native_plugin_from_directory(cur_dir, "email_plugin") - kernel.import_plugin_from_object(MathPlugin(), "MathPlugin") - kernel.import_plugin_from_object(TimePlugin(), "TimePlugin") + kernel.add_plugin(MathPlugin(), "MathPlugin") + kernel.add_plugin(TimePlugin(), "TimePlugin") questions = [ "What is the current hour number, plus 5?", diff --git a/python/samples/kernel-syntax-examples/bing_plugin_examples.py b/python/samples/kernel-syntax-examples/bing_plugin_examples.py index 4fb3e76b02..29ed0fec91 100644 --- a/python/samples/kernel-syntax-examples/bing_plugin_examples.py +++ b/python/samples/kernel-syntax-examples/bing_plugin_examples.py @@ -63,7 +63,7 @@ async def example2(kernel: sk.Kernel, service_id: str): question = "Who is the most followed person on TikTok right now? What's the exchange rate EUR:USD?" print(question) - oracle = kernel.create_function_from_prompt( + oracle = kernel.add_function( function_name="oracle", plugin_name="OraclePlugin", template=prompt, @@ -111,7 +111,7 @@ async def main(): bing_connector = BingConnector(api_key=bing_api_key) bing = WebSearchEnginePlugin(bing_connector) - kernel.import_plugin_from_object(bing, "bing") + kernel.add_plugin(bing, "bing") await example1(kernel, "bing") await example2(kernel, service_id) diff --git a/python/samples/kernel-syntax-examples/bing_search_plugin.py b/python/samples/kernel-syntax-examples/bing_search_plugin.py index 4427f3ed6b..aa8cbf10e5 100644 --- a/python/samples/kernel-syntax-examples/bing_search_plugin.py +++ b/python/samples/kernel-syntax-examples/bing_search_plugin.py @@ -26,7 +26,7 @@ async def main(): ), ) connector = BingConnector(api_key=os.getenv("BING_API_KEY")) - web_plugin = kernel.import_plugin_from_object(WebSearchEnginePlugin(connector), "WebSearch") + web_plugin = kernel.add_plugin(WebSearchEnginePlugin(connector), "WebSearch") print("---------------- Question 1 -----------------\n") @@ -46,7 +46,7 @@ async def main(): Answer: """ - req_settings = kernel.get_service("chat-gpt").get_prompt_execution_settings_class()(service_id=service_id) + req_settings = kernel.get_prompt_execution_settings_from_service_id(service_id=service_id) req_settings.temperature = 0.2 prompt_template_config = sk.PromptTemplateConfig( @@ -57,7 +57,7 @@ async def main(): ) question = "What is Semantic Kernel?" - qna = kernel.create_function_from_prompt( + qna = kernel.add_function( function_name="qna", plugin_name="WebSearch", prompt_template_config=prompt_template_config, diff --git a/python/samples/kernel-syntax-examples/chat.py b/python/samples/kernel-syntax-examples/chat.py index 77bd9d8b70..c550400899 100644 --- a/python/samples/kernel-syntax-examples/chat.py +++ b/python/samples/kernel-syntax-examples/chat.py @@ -56,7 +56,7 @@ chat_history = ChatHistory() chat_history.add_user_message("Hi there, who are you?") chat_history.add_assistant_message("I am Mosscap, a chat bot. I'm trying to figure out what people need") -chat_function = kernel.create_function_from_prompt( +chat_function = kernel.add_function( plugin_name="ChatBot", function_name="Chat", prompt_template_config=prompt_template_config ) diff --git a/python/samples/kernel-syntax-examples/chat_gpt_api.py b/python/samples/kernel-syntax-examples/chat_gpt_api.py index 3e061f93c8..1cbf35ecb3 100644 --- a/python/samples/kernel-syntax-examples/chat_gpt_api.py +++ b/python/samples/kernel-syntax-examples/chat_gpt_api.py @@ -30,7 +30,7 @@ settings.max_tokens = 2000 settings.temperature = 0.7 settings.top_p = 0.8 -chat_function = kernel.create_function_from_prompt( +chat_function = kernel.add_function( plugin_name="ChatBot", function_name="Chat", prompt="{{$chat_history}}{{$user_input}}", diff --git a/python/samples/kernel-syntax-examples/chat_gpt_api_function_calling.py b/python/samples/kernel-syntax-examples/chat_gpt_api_function_calling.py index 4b9a38323a..6417e2e93b 100644 --- a/python/samples/kernel-syntax-examples/chat_gpt_api_function_calling.py +++ b/python/samples/kernel-syntax-examples/chat_gpt_api_function_calling.py @@ -3,7 +3,7 @@ import asyncio import os from functools import reduce -from typing import TYPE_CHECKING, Any, Dict, List, Union +from typing import TYPE_CHECKING, Any, Dict, List import semantic_kernel as sk import semantic_kernel.connectors.ai.open_ai as sk_oai @@ -12,7 +12,7 @@ from semantic_kernel.connectors.ai.open_ai.contents.open_ai_streaming_chat_messa OpenAIStreamingChatMessageContent, ) from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.open_ai_prompt_execution_settings import ( - OpenAIPromptExecutionSettings, + OpenAIChatPromptExecutionSettings, ) from semantic_kernel.connectors.ai.open_ai.utils import get_tool_call_object from semantic_kernel.contents.chat_history import ChatHistory @@ -52,10 +52,10 @@ plugins_directory = os.path.join(__file__, "../../../../samples/plugins") # the joke plugin in the FunPlugins is a semantic plugin and has the function calling disabled. # kernel.import_plugin_from_prompt_directory("chat", plugins_directory, "FunPlugin") # the math plugin is a core plugin and has the function calling enabled. -kernel.import_plugin_from_object(MathPlugin(), plugin_name="math") -kernel.import_plugin_from_object(TimePlugin(), plugin_name="time") +kernel.add_plugin(MathPlugin(), plugin_name="math") +kernel.add_plugin(TimePlugin(), plugin_name="time") -chat_function = kernel.create_function_from_prompt( +chat_function = kernel.add_function( prompt="{{$chat_history}}{{$user_input}}", plugin_name="ChatBot", function_name="Chat", @@ -89,10 +89,10 @@ history.add_assistant_message("I am Mosscap, a chat bot. I'm trying to figure ou arguments = KernelArguments(settings=execution_settings) -def print_tool_calls(message: Union[OpenAIChatMessageContent, OpenAIStreamingChatMessageContent]) -> None: +def print_tool_calls(message: OpenAIChatMessageContent) -> None: # A helper method to pretty print the tool calls from the message. # This is only triggered if auto invoke tool calls is disabled. - if isinstance(message, (OpenAIChatMessageContent, OpenAIStreamingChatMessageContent)): + if isinstance(message, OpenAIChatMessageContent): tool_calls = message.tool_calls formatted_tool_calls = [] for i, tool_call in enumerate(tool_calls, start=1): @@ -113,7 +113,7 @@ async def handle_streaming( chat_function: "KernelFunction", user_input: str, history: ChatHistory, - execution_settings: OpenAIPromptExecutionSettings, + execution_settings: OpenAIChatPromptExecutionSettings, ) -> None: response = kernel.invoke_stream( chat_function, diff --git a/python/samples/kernel-syntax-examples/configuring_prompts.py b/python/samples/kernel-syntax-examples/configuring_prompts.py index e6701159f8..6161bb043c 100644 --- a/python/samples/kernel-syntax-examples/configuring_prompts.py +++ b/python/samples/kernel-syntax-examples/configuring_prompts.py @@ -46,7 +46,7 @@ async def main(): ), ) - chat = kernel.create_function_from_prompt( + chat = kernel.add_function( function_name="chat", plugin_name="ChatBot", prompt_template_config=prompt_template_config, 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 1ac813abca..368a12d534 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 @@ -51,7 +51,7 @@ async def setup_chat_with_memory( }, ) - chat_func = kernel.create_function_from_prompt( + chat_func = kernel.add_function( function_name="chat_with_memory", plugin_name="TextMemoryPlugin", prompt_template_config=prompt_template_config, @@ -91,7 +91,7 @@ async def main() -> None: kernel.add_service(palm_chat_completion) memory = SemanticTextMemory(storage=sk.memory.VolatileMemoryStore(), embeddings_generator=palm_text_embed) - kernel.import_plugin_from_object(TextMemoryPlugin(memory), "TextMemoryPlugin") + kernel.add_plugin(TextMemoryPlugin(memory), "TextMemoryPlugin") print("Populating memory...") await populate_memory(memory) diff --git a/python/samples/kernel-syntax-examples/google_palm_chat_with_plugin.py b/python/samples/kernel-syntax-examples/google_palm_chat_with_plugin.py index 0195db6b02..6b2b3532d2 100644 --- a/python/samples/kernel-syntax-examples/google_palm_chat_with_plugin.py +++ b/python/samples/kernel-syntax-examples/google_palm_chat_with_plugin.py @@ -50,7 +50,7 @@ prompt_template_config = sk.PromptTemplateConfig( execution_settings=req_settings, ) -chat_func = kernel.create_function_from_prompt( +chat_func = kernel.add_function( plugin_name="PiratePlugin", function_name="Chat", prompt_template_config=prompt_template_config ) diff --git a/python/samples/kernel-syntax-examples/google_search_plugin.py b/python/samples/kernel-syntax-examples/google_search_plugin.py index cf5926daff..bd5af8dea4 100644 --- a/python/samples/kernel-syntax-examples/google_search_plugin.py +++ b/python/samples/kernel-syntax-examples/google_search_plugin.py @@ -6,8 +6,10 @@ from dotenv import load_dotenv import semantic_kernel as sk from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion +from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings from semantic_kernel.connectors.search_engine import GoogleConnector from semantic_kernel.core_plugins import WebSearchEnginePlugin +from semantic_kernel.functions.kernel_arguments import KernelArguments load_dotenv() @@ -36,15 +38,15 @@ async def main(): ) # Import the WebSearchEnginePlugin and pass the Google Connector to it. - web_plugin = kernel.import_plugin_from_object(WebSearchEnginePlugin(connector), "WebSearch") + web_plugin = kernel.add_plugin(WebSearchEnginePlugin(connector), "WebSearch") # The search query - prompt = "Who is Leonardo DiCaprio's current girlfriend?" search = web_plugin["searchAsync"] + prompt = "Who is Leonardo DiCaprio's current girlfriend?" # By default, only one search result is provided - result = await search.invoke(prompt) - print(result) + result = await search.invoke(kernel, query=prompt) + print(str(result)) """ Output: @@ -61,19 +63,22 @@ async def main(): Answer: """ - qna = kernel.create_semantic_function(prompt, temperature=0.2) - context = kernel.create_new_context() + qna = kernel.add_function( + plugin_name="qa", + function_name="qna", + prompt=prompt, + prompt_execution_settings=PromptExecutionSettings(temperature=0.2), + ) """ Two context parameters can be passed to the search engine plugin. - num_results controls the number of results returned by the web search. - offset controls the number of results to omit. """ - context["num_results"] = "10" - context["offset"] = "0" + arguments = KernelArguments(num_results="10", offset="0") - result = await qna.invoke(context=context) - print(result) + result = await qna.invoke(kernel, arguments) + print(str(result)) """ Output: diff --git a/python/samples/kernel-syntax-examples/grounded.py b/python/samples/kernel-syntax-examples/grounded.py index d3ef181f19..d3f66f9a4f 100644 --- a/python/samples/kernel-syntax-examples/grounded.py +++ b/python/samples/kernel-syntax-examples/grounded.py @@ -3,10 +3,9 @@ import logging import semantic_kernel as sk from samples.utils import Colors -from semantic_kernel.connectors.ai.open_ai import ( - AzureChatCompletion, - OpenAIChatCompletion, -) +from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion, OpenAIChatCompletion +from semantic_kernel.functions.kernel_arguments import KernelArguments +from semantic_kernel.kernel import Kernel def get_grounding_text(): @@ -48,17 +47,17 @@ interment of his friend he conducted her to Geneva and placed her under the prot after this event Caroline became his wife.""" -def setup(use_azure: bool = False): - logger = logging.getLogger() - logger.setLevel(logging.DEBUG) - kernel = sk.Kernel(log=logger) +logger = logging.getLogger() +logger.setLevel(logging.DEBUG) - useAzureOpenAI = use_azure + +def setup(use_azure: bool = False, plugin_name: str = "GroundingPlugin"): + kernel = Kernel() # Configure AI service used by the kernel - if useAzureOpenAI: + if use_azure: deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env() - service_id = ("chat_completion",) + service_id = "chat_completion" kernel.add_service( AzureChatCompletion( service_id=service_id, @@ -66,7 +65,6 @@ def setup(use_azure: bool = False): endpoint=endpoint, api_key=api_key, api_version="2023-12-01-preview", - log=logger, ), ) else: @@ -79,11 +77,9 @@ def setup(use_azure: bool = False): # note: using plugins from the samples folder plugins_directory = "../samples/plugins/" - grounding_semantic_functions = kernel.import_plugin_from_prompt_directory( - service_id, plugins_directory, "GroundingPlugin" - ) + kernel.add_plugin(parent_directory=plugins_directory, plugin_name=plugin_name) - return kernel, grounding_semantic_functions + return kernel def get_summary_text(): @@ -93,37 +89,47 @@ def get_summary_text(): return summary_text -async def run_entity_extraction(kernel, semantic_functions, summary_text): - context = kernel.create_new_context() - context["topic"] = "people and places" - context["example_entities"] = "John, Jane, mother, brother, Paris, Rome" +async def run_entity_extraction(kernel: Kernel, plugin_name: str, summary_text: str): + arguments = KernelArguments( + topic="people and places", example_entities="John, Jane, mother, brother, Paris, Rome", input=summary_text + ) - extraction_result = semantic_functions["ExtractEntities"](summary_text, context=context) + extraction_result = await kernel.invoke( + plugin_name=plugin_name, function_name="ExtractEntities", arguments=arguments + ) - return extraction_result, context + return extraction_result -async def run_reference_check(semantic_functions, extraction_result, context): - context["reference_context"] = get_grounding_text() - - grounding_result = semantic_functions["ReferenceCheckEntities"](extraction_result.result, context=context) - context["ungrounded_entities"] = grounding_result.result - return grounding_result, context +async def run_reference_check(kernel: Kernel, plugin_name: str, extraction_result): + grounding_result = await kernel.invoke( + plugin_name=plugin_name, + function_name="ReferenceCheckEntities", + input=str(extraction_result), + reference_context=get_grounding_text(), + ) + return grounding_result -async def run_entity_excision(semantic_functions, summary_text, context): - excision_result = semantic_functions["ExciseEntities"](summary_text, context=context) - return excision_result, context +async def run_entity_excision(kernel: Kernel, plugin_name: str, summary_text, grounding_result): + excision_result = await kernel.invoke( + plugin_name=plugin_name, + function_name="ExciseEntities", + input=summary_text, + ungrounded_entities=grounding_result, + ) + return excision_result async def run_grounding(use_azure: bool = False): - kernel, semantic_functions = setup(use_azure) - print(f"\n{Colors.CBOLD}Groundingsness Checking Plugins\n{Colors.CEND}") + plugin_name = "GroundingPlugin" + kernel = setup(use_azure, plugin_name=plugin_name) + print(f"\n{Colors.CBOLD.value}Groundingsness Checking Plugins\n{Colors.CEND.value}") print(f"\n{ '-'*80 }\n") print( - f"""{Colors.CGREEN}A well-known problem with large language models (LLMs) is that they make things up. These are sometimes called 'hallucinations' but a safer (and less anthropomorphic) term is 'ungrounded addition' - something in the text which cannot be firmly established. When attempting to establish whether or not something in an LLM response is 'true' we can either check for it in the supplied prompt (this is called 'narrow grounding') or use our general knowledge ('broad grounding'). Note that narrow grounding can lead to things being classified as 'true, but ungrounded.' For example "I live in Switzerland" is **not** _narrowly_ grounded in "I live in Geneva" even though it must be true (it **is** _broadly_ grounded). # noqa: E501 + f"""{Colors.CGREEN.value}A well-known problem with large language models (LLMs) is that they make things up. These are sometimes called 'hallucinations' but a safer (and less anthropomorphic) term is 'ungrounded addition' - something in the text which cannot be firmly established. When attempting to establish whether or not something in an LLM response is 'true' we can either check for it in the supplied prompt (this is called 'narrow grounding') or use our general knowledge ('broad grounding'). Note that narrow grounding can lead to things being classified as 'true, but ungrounded.' For example "I live in Switzerland" is **not** _narrowly_ grounded in "I live in Geneva" even though it must be true (it **is** _broadly_ grounded). -In this sample we run a simple grounding pipeline, to see if a summary text has any ungrounded additions as compared to the original, and use this information to improve the summary text. This can be done in three stages: # noqa: E501 +In this sample we run a simple grounding pipeline, to see if a summary text has any ungrounded additions as compared to the original, and use this information to improve the summary text. This can be done in three stages: 1. Make a list of the entities in the summary text 1. Check to see if these entities appear in the original (grounding) text @@ -132,14 +138,14 @@ In this sample we run a simple grounding pipeline, to see if a summary text has What is an 'entity' in this context? In its simplest form, it's a named object such as a person or place (so 'Dean' or 'Seattle'). However, the idea could be a _claim_ which relates concepts (such as 'Dean lives near Seattle'). In this sample, we will keep to the simpler case of named objects.""" # noqa: E501 ) - print(f"\nThe grounding text: \n{Colors.CGREY}{get_grounding_text()}{Colors.CEND}") + print(f"\nThe grounding text: \n{Colors.CGREY.value}{get_grounding_text()}{Colors.CEND.value}") print(f"\n{ '-'*80 }\n") summary_text = get_summary_text() - print(f"Summary text: \n{Colors.CBLUE}{summary_text}{Colors.CEND}") + print(f"Summary text: \n{Colors.CBLUE.value}{summary_text}{Colors.CEND.value}") print(f"\n{ '-'*80 }\n") print( - f"""{Colors.CGREEN}Some things to note: + f"""{Colors.CGREEN.value}Some things to note: - The implied residence of Geneva has been changed to Milan - Lucerne has been changed to Zurich @@ -153,28 +159,28 @@ The grounding plugin has three stages: 2. Perform a reference check against the grounding text 3. Excise any entities which failed the reference check from the summary -Now, let us start calling individual semantic functions.{Colors.CEND}""" +Now, let us start calling individual semantic functions.{Colors.CEND.value}""" ) print(f"\n{ '-'*80 }\n") print( - f"{Colors.CGREEN}First we run the extraction function on the summary, this results in all the extracted entities.{Colors.CEND}" # noqa: E501 + f"{Colors.CGREEN.value}First we run the extraction function on the summary, this results in all the extracted entities.{Colors.CEND.value}" # noqa: E501 ) - extraction_result, context = await run_entity_extraction(kernel, semantic_functions, summary_text) - print(f"Extraction result: \n{Colors.CBLUE}{extraction_result.result}{Colors.CEND}") + extraction_result = await run_entity_extraction(kernel, plugin_name, summary_text) + print(f"Extraction result: \n{Colors.CBLUE.value}{str(extraction_result)}{Colors.CEND.value}") print(f"\n{ '-'*80 }\n") print( - f"{Colors.CGREEN}Next we run the reference check function on the summary, this loads the grounding text as part of it in order to know the 'truth'. This returns a list of ungrounded entities.{Colors.CEND}" # noqa: E501 + f"{Colors.CGREEN.value}Next we run the reference check function on the summary, this loads the grounding text as part of it in order to know the 'truth'. This returns a list of ungrounded entities.{Colors.CEND.value}" # noqa: E501 ) - grounding_result, context = await run_reference_check(semantic_functions, extraction_result, context) - print(f"Grounding result: \n{Colors.CBLUE}{grounding_result.result}{Colors.CEND}") + grounding_result = await run_reference_check(kernel, plugin_name, extraction_result) + print(f"Grounding result: \n{Colors.CBLUE.value}{str(grounding_result)}{Colors.CEND.value}") print(f"\n{ '-'*80 }\n") print( - f"{Colors.CGREEN}Finally we run the excision function on the summary, this removes the ungrounded entities from the summary.{Colors.CEND}" # noqa: E501 + f"{Colors.CGREEN.value}Finally we run the excision function on the summary, this removes the ungrounded entities from the summary.{Colors.CEND.value}" # noqa: E501 ) - excision_result, context = await run_entity_excision(semantic_functions, summary_text, context) - print(f"The final summary text: \n{Colors.CBLUE}{excision_result.result}{Colors.CEND}") + excision_result = await run_entity_excision(kernel, plugin_name, summary_text, grounding_result) + print(f"The final summary text: \n{Colors.CBLUE.value}{str(excision_result)}{Colors.CEND.value}") print(f"\n{ '-'*80 }\n") - print(f"{Colors.CBOLD}Finished!{Colors.CEND}") + print(f"{Colors.CBOLD.value}Finished!{Colors.CEND.value}") if __name__ == "__main__": diff --git a/python/samples/kernel-syntax-examples/load_yaml_prompt.py b/python/samples/kernel-syntax-examples/load_yaml_prompt.py index 8c41a171ec..ab48fbbfd6 100644 --- a/python/samples/kernel-syntax-examples/load_yaml_prompt.py +++ b/python/samples/kernel-syntax-examples/load_yaml_prompt.py @@ -6,9 +6,7 @@ import os from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion from semantic_kernel.contents.chat_history import ChatHistory from semantic_kernel.kernel import Kernel -from semantic_kernel.utils.settings import ( - openai_settings_from_dot_env, -) +from semantic_kernel.utils.settings import openai_settings_from_dot_env async def main(): @@ -27,7 +25,7 @@ async def main(): chat_history = ChatHistory(system_message="Assistant is a large language model") cur_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "resources") - plugin = kernel.import_plugin_from_prompt_directory(cur_dir, "sample_plugins") + plugin = kernel.add_plugin(plugin_name="sample_plugins", parent_directory=cur_dir) result = await kernel.invoke(plugin["Parrot"], count=2, user_message="I love parrots.", chat_history=chat_history) print(result) diff --git a/python/samples/kernel-syntax-examples/memory.py b/python/samples/kernel-syntax-examples/memory.py index 6f65554c96..0b67db72af 100644 --- a/python/samples/kernel-syntax-examples/memory.py +++ b/python/samples/kernel-syntax-examples/memory.py @@ -46,12 +46,10 @@ async def setup_chat_with_memory( prompt_template_config = PromptTemplateConfig( template=prompt, - execution_settings={ - service_id: kernel.get_service(service_id).get_prompt_execution_settings_class()(service_id=service_id) - }, + execution_settings={service_id: kernel.get_prompt_execution_settings_from_service_id(service_id=service_id)}, ) - chat_func = kernel.create_function_from_prompt( + chat_func = kernel.add_function( function_name="chat_with_memory", plugin_name="TextMemoryPlugin", prompt_template_config=prompt_template_config, @@ -94,7 +92,7 @@ async def main() -> None: kernel.add_service(embedding_gen) memory = SemanticTextMemory(storage=sk.memory.VolatileMemoryStore(), embeddings_generator=embedding_gen) - kernel.import_plugin_from_object(TextMemoryPlugin(memory), "TextMemoryPlugin") + kernel.add_plugin(TextMemoryPlugin(memory), "TextMemoryPlugin") print("Populating memory...") await populate_memory(memory) diff --git a/python/samples/kernel-syntax-examples/openai_function_calling_stepwise_planner.py b/python/samples/kernel-syntax-examples/openai_function_calling_stepwise_planner.py index 709cb82d57..bd91a15483 100644 --- a/python/samples/kernel-syntax-examples/openai_function_calling_stepwise_planner.py +++ b/python/samples/kernel-syntax-examples/openai_function_calling_stepwise_planner.py @@ -29,10 +29,8 @@ async def main(): ) cur_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "resources") - kernel.import_native_plugin_from_directory(cur_dir, "email_plugin") - - kernel.import_plugin_from_object(MathPlugin(), "MathPlugin") - kernel.import_plugin_from_object(TimePlugin(), "TimePlugin") + kernel.add_plugin(parent_directory=cur_dir, plugin_name="email_plugin") + kernel.add_plugins({"MathPlugin": MathPlugin(), "TimePlugin": TimePlugin()}) questions = [ "What is the current hour number, plus 5?", @@ -52,7 +50,7 @@ async def main(): print(f"Q: {question}\nA: {result.final_answer}\n") # Uncomment the following line to view the planner's process for completing the request - # print(f"Chat history: {result.chat_history}\n") + # print(f"\nChat history: {result.chat_history}\n") if __name__ == "__main__": diff --git a/python/samples/kernel-syntax-examples/openai_logit_bias.py b/python/samples/kernel-syntax-examples/openai_logit_bias.py index 32341a5c99..794def81a3 100644 --- a/python/samples/kernel-syntax-examples/openai_logit_bias.py +++ b/python/samples/kernel-syntax-examples/openai_logit_bias.py @@ -8,6 +8,7 @@ import semantic_kernel.connectors.ai.open_ai as sk_oai from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings from semantic_kernel.contents.chat_history import ChatHistory from semantic_kernel.functions.kernel_arguments import KernelArguments +from semantic_kernel.kernel import Kernel from semantic_kernel.prompt_template.input_variable import InputVariable from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig @@ -31,7 +32,7 @@ def _prepare_input_chat(chat: ChatHistory): return "".join([f"{msg.role}: {msg.content}\n" for msg in chat]) -async def chat_request_example(kernel, api_key, org_id): +async def chat_request_example(kernel: Kernel, api_key, org_id): service_id = "chat_service" openai_chat_completion = sk_oai.OpenAIChatCompletion( service_id=service_id, ai_model_id="gpt-3.5-turbo", api_key=api_key, org_id=org_id @@ -70,7 +71,7 @@ async def chat_request_example(kernel, api_key, org_id): ] # Model will try its best to avoid using any of the above words - settings = kernel.get_service(service_id).get_prompt_execution_settings_class()(service_id=service_id) + settings = kernel.get_prompt_execution_settings_from_service_id(service_id=service_id) settings = _config_ban_tokens(settings, keys) prompt_template_config = PromptTemplateConfig( @@ -90,7 +91,7 @@ async def chat_request_example(kernel, api_key, org_id): chat.add_user_message("Hi there, who are you?") chat.add_assistant_message("I am an AI assistant here to answer your questions.") - chat_function = kernel.create_function_from_prompt( + chat_function = kernel.add_function( plugin_name="ChatBot", function_name="Chat", prompt_template_config=prompt_template_config ) @@ -111,7 +112,7 @@ async def chat_request_example(kernel, api_key, org_id): return chat, banned_words -async def text_complete_request_example(kernel, api_key, org_id): +async def text_complete_request_example(kernel: Kernel, api_key, org_id): service_id = "text_service" openai_text_completion = sk_oai.OpenAITextCompletion( service_id=service_id, ai_model_id="gpt-3.5-turbo-instruct", api_key=api_key, org_id=org_id @@ -159,7 +160,7 @@ async def text_complete_request_example(kernel, api_key, org_id): ] # Model will try its best to avoid using any of the above words - settings = kernel.get_service(service_id).get_prompt_execution_settings_class()(service_id=service_id) + settings = kernel.get_prompt_execution_settings_from_service_id(service_id=service_id) settings = _config_ban_tokens(settings, keys) prompt_template_config = PromptTemplateConfig( @@ -178,7 +179,7 @@ async def text_complete_request_example(kernel, api_key, org_id): chat.add_user_message("The best pie flavor to have in autumn is") - text_function = kernel.create_function_from_prompt( + text_function = kernel.add_function( plugin_name="TextBot", function_name="TextCompletion", prompt_template_config=prompt_template_config ) @@ -209,7 +210,7 @@ def _format_output(chat, banned_words) -> None: async def main() -> None: - kernel = sk.Kernel() + kernel = Kernel() api_key, org_id = sk.openai_settings_from_dot_env() print("Chat completion example:") diff --git a/python/samples/kernel-syntax-examples/openai_plugin_azure_key_vault.py b/python/samples/kernel-syntax-examples/openai_plugin_azure_key_vault.py index b0b39492bf..3649e8080a 100644 --- a/python/samples/kernel-syntax-examples/openai_plugin_azure_key_vault.py +++ b/python/samples/kernel-syntax-examples/openai_plugin_azure_key_vault.py @@ -146,7 +146,7 @@ async def main(): http_client = httpx.AsyncClient() - plugin = await kernel.import_plugin_from_openai( + plugin = await kernel.add_plugin_from_openai( plugin_name="AzureKeyVaultPlugin", plugin_str=openai_spec, execution_parameters=OpenAIFunctionExecutionParameters( diff --git a/python/samples/kernel-syntax-examples/openai_plugin_klarna.py b/python/samples/kernel-syntax-examples/openai_plugin_klarna.py index f2b59f00c2..28d8f6cbce 100644 --- a/python/samples/kernel-syntax-examples/openai_plugin_klarna.py +++ b/python/samples/kernel-syntax-examples/openai_plugin_klarna.py @@ -6,12 +6,11 @@ from semantic_kernel.kernel import Kernel async def main(): - # This is an example of how to import a plugin from OpenAI and invoke a function from the plugin # It does not require authentication kernel = Kernel() - plugin = await kernel.import_plugin_from_openai( + plugin = await kernel.add_plugin_from_openai( plugin_name="Klarna", plugin_url="https://www.klarna.com/.well-known/ai-plugin.json", ) diff --git a/python/samples/kernel-syntax-examples/plugins_from_dir.py b/python/samples/kernel-syntax-examples/plugins_from_dir.py index 1602f7e752..396db3d201 100644 --- a/python/samples/kernel-syntax-examples/plugins_from_dir.py +++ b/python/samples/kernel-syntax-examples/plugins_from_dir.py @@ -31,7 +31,7 @@ async def main(): # note: using plugins from the samples folder plugins_directory = os.path.join(__file__, "../../../../samples/plugins") - plugin = kernel.import_plugin_from_prompt_directory(service_id, plugins_directory, "FunPlugin") + plugin = kernel.add_plugin(parent_directory=plugins_directory, plugin_name="FunPlugin") arguments = KernelArguments(input="time travel to dinosaur age", style="super silly") diff --git a/python/samples/kernel-syntax-examples/rag_with_text_memory_plugin.py b/python/samples/kernel-syntax-examples/rag_with_text_memory_plugin.py index 3cf9217a64..acdcd73596 100644 --- a/python/samples/kernel-syntax-examples/rag_with_text_memory_plugin.py +++ b/python/samples/kernel-syntax-examples/rag_with_text_memory_plugin.py @@ -24,7 +24,7 @@ async def main(): kernel.add_service(embedding_gen) memory = SemanticTextMemory(storage=VolatileMemoryStore(), embeddings_generator=embedding_gen) - kernel.import_plugin_from_object(TextMemoryPlugin(memory), "memory") + kernel.add_plugin(TextMemoryPlugin(memory), "memory") await memory.save_information(collection="generic", id="info1", text="My budget for 2024 is $100,000") diff --git a/python/samples/kernel-syntax-examples/resources/email_plugin/native_function.py b/python/samples/kernel-syntax-examples/resources/email_plugin/native_function.py index 35fd82e37c..7f982e8307 100644 --- a/python/samples/kernel-syntax-examples/resources/email_plugin/native_function.py +++ b/python/samples/kernel-syntax-examples/resources/email_plugin/native_function.py @@ -15,7 +15,7 @@ class EmailPlugin: Description: EmailPlugin provides a set of functions to send emails. Usage: - kernel.import_plugin_from_object(EmailPlugin(), plugin_name="email") + kernel.add_plugin(EmailPlugin(), plugin_name="email") Examples: {{email.SendEmail}} => Sends an email with the provided subject and body. diff --git a/python/samples/kernel-syntax-examples/self-critique_rag.py b/python/samples/kernel-syntax-examples/self-critique_rag.py index 5302df0f89..8c9afe6a49 100644 --- a/python/samples/kernel-syntax-examples/self-critique_rag.py +++ b/python/samples/kernel-syntax-examples/self-critique_rag.py @@ -5,13 +5,8 @@ import asyncio from dotenv import dotenv_values import semantic_kernel as sk -from semantic_kernel.connectors.ai.open_ai import ( - AzureChatCompletion, - AzureTextEmbedding, -) -from semantic_kernel.connectors.memory.azure_cognitive_search import ( - AzureCognitiveSearchMemoryStore, -) +from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion, AzureTextEmbedding +from semantic_kernel.connectors.memory.azure_cognitive_search import AzureCognitiveSearchMemoryStore from semantic_kernel.contents.chat_history import ChatHistory from semantic_kernel.core_plugins.text_memory_plugin import TextMemoryPlugin from semantic_kernel.memory.semantic_text_memory import SemanticTextMemory @@ -69,7 +64,7 @@ async def main() -> None: ) memory = SemanticTextMemory(storage=acs_connector, embeddings_generator=embedding_gen) - kernel.import_plugin_from_object(TextMemoryPlugin(memory), "TextMemoryPlugin") + kernel.add_plugin(TextMemoryPlugin(memory), "TextMemoryPlugin") print("Populating memory...") await populate_memory(memory) @@ -96,11 +91,11 @@ Remember, just answer Grounded or Ungrounded or Unclear: """.strip() user_input = "Do I live in Seattle?" print(f"Question: {user_input}") - req_settings = kernel.get_service("dv").get_prompt_execution_settings_class()(service_id="dv") - chat_func = kernel.create_function_from_prompt( + req_settings = kernel.get_prompt_execution_settings_from_service_id(service_id="dv") + chat_func = kernel.add_function( function_name="rag", plugin_name="RagPlugin", prompt=sk_prompt_rag, prompt_execution_settings=req_settings ) - self_critique_func = kernel.create_function_from_prompt( + self_critique_func = kernel.add_function( function_name="self_critique_rag", plugin_name="RagPlugin", prompt=sk_prompt_rag_sc, diff --git a/python/samples/kernel-syntax-examples/sequential_planner.py b/python/samples/kernel-syntax-examples/sequential_planner.py index 11d47bb5f3..e042c68595 100644 --- a/python/samples/kernel-syntax-examples/sequential_planner.py +++ b/python/samples/kernel-syntax-examples/sequential_planner.py @@ -18,9 +18,9 @@ async def main(): kernel.add_service( OpenAIChatCompletion(service_id=service_id, ai_model_id="gpt-3.5-turbo", api_key=api_key, org_id=org_id) ) - kernel.import_plugin_from_object(MathPlugin(), "math") - kernel.import_plugin_from_object(TimePlugin(), "time") - kernel.import_plugin_from_object(TextPlugin(), "text") + kernel.add_plugin(MathPlugin(), "math") + kernel.add_plugin(TimePlugin(), "time") + kernel.add_plugin(TextPlugin(), "text") # create an instance of sequential planner. planner = SequentialPlanner(service_id=service_id, kernel=kernel) diff --git a/python/samples/kernel-syntax-examples/template_language.py b/python/samples/kernel-syntax-examples/template_language.py index 3e5bdb3043..9ea9f323fb 100644 --- a/python/samples/kernel-syntax-examples/template_language.py +++ b/python/samples/kernel-syntax-examples/template_language.py @@ -4,9 +4,7 @@ import asyncio import semantic_kernel as sk import semantic_kernel.connectors.ai.open_ai as sk_oai -from semantic_kernel.core_plugins import ( - TimePlugin, -) +from semantic_kernel.core_plugins import TimePlugin from semantic_kernel.prompt_template.kernel_prompt_template import KernelPromptTemplate from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig @@ -23,7 +21,7 @@ async def main(): sk_oai.OpenAIChatCompletion(service_id=service_id, ai_model_id=model, api_key=api_key, org_id=org_id), ) - kernel.import_plugin_from_object(TimePlugin(), "time") + kernel.add_plugin(TimePlugin(), "time") function_definition = """ Today is: {{time.Date}} @@ -40,7 +38,7 @@ async def main(): rendered_prompt = await prompt_template.render(kernel, arguments=None) print(rendered_prompt) - kind_of_day = kernel.create_function_from_prompt( + kind_of_day = kernel.add_function( plugin_name="TimePlugin", template=function_definition, execution_settings=sk_oai.OpenAIChatPromptExecutionSettings(service_id=service_id, max_tokens=100), diff --git a/python/semantic_kernel/connectors/ai/open_ai/utils.py b/python/semantic_kernel/connectors/ai/open_ai/utils.py index dafea66277..7b020e7309 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/utils.py +++ b/python/semantic_kernel/connectors/ai/open_ai/utils.py @@ -151,13 +151,10 @@ def get_function_calling_object( if exclude_function: exclude_function = [function for function in exclude_function] result = [] - for ( - plugin_name, - plugin, - ) in kernel.plugins.plugins.items(): + for plugin_name, plugin in kernel.plugins.items(): if plugin_name in exclude_plugin or (include_plugin and plugin_name not in include_plugin): continue - for function in plugin.functions.values(): + for function in plugin: if function.fully_qualified_name in exclude_function or ( include_function and function.fully_qualified_name not in include_function ): diff --git a/python/semantic_kernel/connectors/openai_plugin/openai_utils.py b/python/semantic_kernel/connectors/openai_plugin/openai_utils.py index e4963c6631..75f9945139 100644 --- a/python/semantic_kernel/connectors/openai_plugin/openai_utils.py +++ b/python/semantic_kernel/connectors/openai_plugin/openai_utils.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from typing import Any from semantic_kernel.exceptions.function_exceptions import PluginInitializationError @@ -13,7 +14,7 @@ class OpenAIUtils: """Utility functions for OpenAI plugins.""" @staticmethod - def parse_openai_manifest_for_openapi_spec_url(plugin_json): + def parse_openai_manifest_for_openapi_spec_url(plugin_json: dict[str, Any]) -> str: """Extract the OpenAPI Spec URL from the plugin JSON.""" try: diff --git a/python/semantic_kernel/connectors/openapi_plugin/__init__.py b/python/semantic_kernel/connectors/openapi_plugin/__init__.py index ea4e157e54..8ad89fbd56 100644 --- a/python/semantic_kernel/connectors/openapi_plugin/__init__.py +++ b/python/semantic_kernel/connectors/openapi_plugin/__init__.py @@ -3,6 +3,5 @@ from semantic_kernel.connectors.openapi_plugin.openapi_function_execution_parameters import ( OpenAPIFunctionExecutionParameters, ) -from semantic_kernel.connectors.openapi_plugin.openapi_manager import OpenAPIPlugin -__all__ = ["OpenAPIPlugin", "OpenAPIFunctionExecutionParameters"] +__all__ = ["OpenAPIFunctionExecutionParameters"] diff --git a/python/semantic_kernel/connectors/openapi_plugin/openapi_manager.py b/python/semantic_kernel/connectors/openapi_plugin/openapi_manager.py index 980ff64b0f..d1574aed4e 100644 --- a/python/semantic_kernel/connectors/openapi_plugin/openapi_manager.py +++ b/python/semantic_kernel/connectors/openapi_plugin/openapi_manager.py @@ -7,10 +7,13 @@ import logging import sys from typing import TYPE_CHECKING, Any, Callable, Dict, Mapping +from semantic_kernel.functions.kernel_function_from_method import KernelFunctionFromMethod + if sys.version_info >= (3, 9): from typing import Annotated else: from typing_extensions import Annotated + from urllib.parse import urljoin, urlparse, urlunparse import aiohttp @@ -20,12 +23,9 @@ from openapi_core.contrib.requests import RequestsOpenAPIRequest from openapi_core.exceptions import OpenAPIError from prance import ResolvingParser -from semantic_kernel.connectors.ai.open_ai.const import ( - USER_AGENT, -) +from semantic_kernel.connectors.ai.open_ai.const import USER_AGENT from semantic_kernel.exceptions import ServiceInvalidRequestError from semantic_kernel.functions.kernel_function_decorator import kernel_function -from semantic_kernel.functions.kernel_plugin import KernelPlugin if TYPE_CHECKING: from semantic_kernel.connectors.openai_plugin.openai_function_execution_parameters import ( @@ -211,7 +211,7 @@ class OpenApiParser: def create_rest_api_operations( self, parsed_document: Any, - execution_settings: "OpenAIFunctionExecutionParameters" | "OpenAPIFunctionExecutionParameters" | None = None, + execution_settings: "OpenAIFunctionExecutionParameters | OpenAPIFunctionExecutionParameters | None" = None, ) -> Dict[str, RestApiOperation]: """Create the REST API Operations from the parsed OpenAPI document. @@ -303,69 +303,61 @@ class OpenApiRunner: return await response.text() -class OpenAPIPlugin: - @staticmethod - def create( - plugin_name: str, - openapi_document_path: str, - execution_settings: "OpenAIFunctionExecutionParameters" | "OpenAPIFunctionExecutionParameters" | None = None, - ) -> KernelPlugin: - """Creates an OpenAPI plugin +def create_functions_from_openapi( + plugin_name: str, + openapi_document_path: str, + execution_settings: "OpenAIFunctionExecutionParameters | OpenAPIFunctionExecutionParameters | None" = None, +) -> list[KernelFunctionFromMethod]: + """Creates the functions from OpenAPI document. - Args: - plugin_name: The name of the plugin - openapi_document_path: The OpenAPI document path, it must be a file path to the spec. - execution_settings: The execution settings + Args: + plugin_name: The name of the plugin + openapi_document_path: The OpenAPI document path, it must be a file path to the spec. + execution_settings: The execution settings - Returns: - The KernelPlugin - """ - parser = OpenApiParser() - parsed_doc = parser.parse(openapi_document_path) - operations = parser.create_rest_api_operations(parsed_doc, execution_settings=execution_settings) + Returns: + list[KernelFunctionFromMethod]: the operations as functions + """ + parser = OpenApiParser() + parsed_doc = parser.parse(openapi_document_path) + operations = parser.create_rest_api_operations(parsed_doc, execution_settings=execution_settings) - auth_callback = None - if execution_settings and execution_settings.auth_callback: - auth_callback = execution_settings.auth_callback - openapi_runner = OpenApiRunner(parsed_openapi_document=parsed_doc, auth_callback=auth_callback) + auth_callback = None + if execution_settings and execution_settings.auth_callback: + auth_callback = execution_settings.auth_callback + openapi_runner = OpenApiRunner(parsed_openapi_document=parsed_doc, auth_callback=auth_callback) - plugin = {} + return [ + _create_function_from_operation(openapi_runner, operation, plugin_name) for operation in operations.values() + ] - def create_run_operation_function(runner: OpenApiRunner, operation: RestApiOperation): - @kernel_function( - description=operation.summary if operation.summary else operation.description, - name=operation.id, - ) - async def run_openapi_operation( - path_params: Annotated[dict | str | None, "A dictionary of path parameters"] = None, - query_params: Annotated[dict | str | None, "A dictionary of query parameters"] = None, - headers: Annotated[dict | str | None, "A dictionary of headers"] = None, - request_body: Annotated[dict | str | None, "A dictionary of the request body"] = None, - ) -> str: - response = await runner.run_operation( - operation, - path_params=( - json.loads(path_params) - if isinstance(path_params, str) - else path_params if path_params else None - ), - query_params=( - json.loads(query_params) - if isinstance(query_params, str) - else query_params if query_params else None - ), - headers=json.loads(headers) if isinstance(headers, str) else headers if headers else None, - request_body=( - json.loads(request_body) - if isinstance(request_body, str) - else request_body if request_body else None - ), - ) - return response - return run_openapi_operation +def _create_function_from_operation(runner: OpenApiRunner, operation: RestApiOperation, plugin_name: str | None = None): + logger.info(f"Registering OpenAPI operation: {plugin_name}.{operation.id}") - for operation_id, operation in operations.items(): - logger.info(f"Registering OpenAPI operation: {plugin_name}.{operation_id}") - plugin[operation_id] = create_run_operation_function(openapi_runner, operation) - return plugin + @kernel_function( + description=operation.summary if operation.summary else operation.description, + name=operation.id, + ) + async def run_openapi_operation( + path_params: Annotated[dict | str | None, "A dictionary of path parameters"] = None, + query_params: Annotated[dict | str | None, "A dictionary of query parameters"] = None, + headers: Annotated[dict | str | None, "A dictionary of headers"] = None, + request_body: Annotated[dict | str | None, "A dictionary of the request body"] = None, + ) -> str: + response = await runner.run_operation( + operation, + path_params=( + json.loads(path_params) if isinstance(path_params, str) else path_params if path_params else None + ), + query_params=( + json.loads(query_params) if isinstance(query_params, str) else query_params if query_params else None + ), + headers=json.loads(headers) if isinstance(headers, str) else headers if headers else None, + request_body=( + json.loads(request_body) if isinstance(request_body, str) else request_body if request_body else None + ), + ) + return response + + return KernelFunctionFromMethod(run_openapi_operation, plugin_name=plugin_name) diff --git a/python/semantic_kernel/core_plugins/conversation_summary_plugin.py b/python/semantic_kernel/core_plugins/conversation_summary_plugin.py index 1ed7749807..348362da36 100644 --- a/python/semantic_kernel/core_plugins/conversation_summary_plugin.py +++ b/python/semantic_kernel/core_plugins/conversation_summary_plugin.py @@ -45,7 +45,7 @@ class ConversationSummaryPlugin: :param return_key: The key to use for the return value. """ self.return_key = return_key - self._summarizeConversationFunction = kernel.create_function_from_prompt( + self._summarizeConversationFunction = kernel.add_function( prompt=ConversationSummaryPlugin._summarize_conversation_prompt_template, plugin_name=ConversationSummaryPlugin.__name__, function_name="SummarizeConversation", @@ -73,9 +73,7 @@ class ConversationSummaryPlugin: :return: KernelArguments with the summarized conversation result in key self.return_key. """ from semantic_kernel.text import text_chunker - from semantic_kernel.text.function_extension import ( - aggregate_chunked_results, - ) + from semantic_kernel.text.function_extension import aggregate_chunked_results lines = text_chunker._split_text_lines(input, ConversationSummaryPlugin._max_tokens, True) paragraphs = text_chunker._split_text_paragraph(lines, ConversationSummaryPlugin._max_tokens) diff --git a/python/semantic_kernel/functions/__init__.py b/python/semantic_kernel/functions/__init__.py index 2ee0d1a0c7..f713581f8c 100644 --- a/python/semantic_kernel/functions/__init__.py +++ b/python/semantic_kernel/functions/__init__.py @@ -7,7 +7,6 @@ from semantic_kernel.functions.kernel_function_decorator import kernel_function from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata from semantic_kernel.functions.kernel_parameter_metadata import KernelParameterMetadata from semantic_kernel.functions.kernel_plugin import KernelPlugin -from semantic_kernel.functions.kernel_plugin_collection import KernelPluginCollection __all__ = [ "FunctionResult", @@ -17,5 +16,4 @@ __all__ = [ "KernelFunctionMetadata", "KernelParameterMetadata", "KernelPlugin", - "KernelPluginCollection", ] diff --git a/python/semantic_kernel/functions/kernel_arguments.py b/python/semantic_kernel/functions/kernel_arguments.py index 42a79b2a50..97d7b6fbab 100644 --- a/python/semantic_kernel/functions/kernel_arguments.py +++ b/python/semantic_kernel/functions/kernel_arguments.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. +from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings @@ -9,7 +10,7 @@ if TYPE_CHECKING: class KernelArguments(dict): def __init__( self, - settings: Optional[Union["PromptExecutionSettings", List["PromptExecutionSettings"]]] = None, + settings: "PromptExecutionSettings" | list["PromptExecutionSettings"] | None = None, **kwargs: Any, ): """Initializes a new instance of the KernelArguments class, @@ -19,11 +20,11 @@ class KernelArguments(dict): just adds the execution_settings as a dict, with service_id and the settings. Arguments: - settings {Optional[Union[PromptExecutionSettings, List[PromptExecutionSettings]]]} -- + settings (PromptExecutionSettings | List[PromptExecutionSettings] | None) -- The settings for the execution. If a list is given, make sure all items in the list have a unique service_id as that is used as the key for the dict. - **kwargs {Dict[str, Any]} -- The arguments for the function invocation, works similar to a regular dict. + **kwargs (dict[str, Any]) -- The arguments for the function invocation, works similar to a regular dict. """ super().__init__(**kwargs) settings_dict = {} @@ -32,4 +33,4 @@ class KernelArguments(dict): settings_dict = {s.service_id: s for s in settings} else: settings_dict = {settings.service_id: settings} - self.execution_settings: Optional[Dict[str, "PromptExecutionSettings"]] = settings_dict + self.execution_settings: dict[str, "PromptExecutionSettings"] | None = settings_dict diff --git a/python/semantic_kernel/functions/kernel_function.py b/python/semantic_kernel/functions/kernel_function.py index d260540127..5a627bdb23 100644 --- a/python/semantic_kernel/functions/kernel_function.py +++ b/python/semantic_kernel/functions/kernel_function.py @@ -1,7 +1,9 @@ # Copyright (c) Microsoft. All rights reserved. +from __future__ import annotations import logging from abc import abstractmethod +from copy import copy, deepcopy from typing import TYPE_CHECKING, Any, AsyncIterable, Callable, Dict, List, Optional, Union from semantic_kernel.functions.function_result import FunctionResult @@ -227,3 +229,18 @@ class KernelFunction(KernelBaseModel): except Exception as e: logger.error(f"Error occurred while invoking function {self.name}: {e}") yield FunctionResult(function=self.metadata, value=None, metadata={"exception": e, "arguments": arguments}) + + def function_copy(self, plugin_name: str | None = None) -> "KernelFunction": + """Copy the function, can also override the plugin_name. + + Args: + plugin_name (str): The new plugin name. + + Returns: + KernelFunction: The copied function. + """ + cop: KernelFunction = copy(self) + cop.metadata = deepcopy(self.metadata) + if plugin_name: + cop.metadata.plugin_name = plugin_name + return cop diff --git a/python/semantic_kernel/functions/kernel_function_from_prompt.py b/python/semantic_kernel/functions/kernel_function_from_prompt.py index 7f95c30220..c33a9cae6a 100644 --- a/python/semantic_kernel/functions/kernel_function_from_prompt.py +++ b/python/semantic_kernel/functions/kernel_function_from_prompt.py @@ -1,8 +1,11 @@ # Copyright (c) Microsoft. All rights reserved. +from __future__ import annotations import logging +import os from typing import TYPE_CHECKING, Any, AsyncIterable, Dict, List, Optional, Union +import yaml from pydantic import Field, ValidationError, model_validator from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase @@ -30,6 +33,8 @@ if TYPE_CHECKING: logger: logging.Logger = logging.getLogger(__name__) +PROMPT_FILE_NAME = "skprompt.txt" +CONFIG_FILE_NAME = "config.json" PROMPT_RETURN_PARAM = KernelParameterMetadata( name="return", description="The completion result", @@ -48,7 +53,7 @@ class KernelFunctionFromPrompt(KernelFunction): def __init__( self, function_name: str, - plugin_name: str, + plugin_name: Optional[str] = None, description: Optional[str] = None, prompt: Optional[str] = None, template_format: TEMPLATE_FORMAT_TYPES = KERNEL_TEMPLATE_FORMAT_NAME, @@ -319,3 +324,80 @@ through prompt_template_config or in the prompt_template." if parameter.name not in arguments and parameter.default not in {None, "", False, 0}: arguments[parameter.name] = parameter.default return arguments + + @classmethod + def from_yaml(cls, yaml_str: str, plugin_name: str | None = None) -> "KernelFunctionFromPrompt": + """Creates a new instance of the KernelFunctionFromPrompt class from a YAML string.""" + try: + data = yaml.safe_load(yaml_str) + except yaml.YAMLError as exc: # pragma: no cover + raise FunctionInitializationError(f"Invalid YAML content: {yaml_str}, error: {exc}") from exc + + if not isinstance(data, dict): + raise FunctionInitializationError(f"The YAML content must represent a dictionary, got {yaml_str}") + + try: + prompt_template_config = PromptTemplateConfig(**data) + except ValidationError as exc: + raise FunctionInitializationError( + f"Error initializing PromptTemplateConfig: {exc} from yaml data: {data}" + ) from exc + return cls( + function_name=prompt_template_config.name, + plugin_name=plugin_name, + description=prompt_template_config.description, + prompt_template_config=prompt_template_config, + template_format=prompt_template_config.template_format, + ) + + @classmethod + def from_directory(cls, path: str, plugin_name: str | None = None) -> "KernelFunctionFromPrompt": + """Creates a new instance of the KernelFunctionFromPrompt class from a directory. + + The directory needs to contain: + - A prompt file named `skprompt.txt` + - A config file named `config.json` + + Returns: + KernelFunctionFromPrompt: The kernel function from prompt + """ + prompt_path = os.path.join(path, PROMPT_FILE_NAME) + config_path = os.path.join(path, CONFIG_FILE_NAME) + prompt_exists = os.path.exists(prompt_path) + config_exists = os.path.exists(config_path) + if not config_exists and not prompt_exists: + raise FunctionInitializationError( + f"{PROMPT_FILE_NAME} and {CONFIG_FILE_NAME} files are required to create a " + f"function from a directory, path: {str(path)}." + ) + elif not config_exists: + raise FunctionInitializationError( + f"{CONFIG_FILE_NAME} files are required to create a function from a directory, " + f"path: {str(path)}, prompt file is there." + ) + elif not prompt_exists: + raise FunctionInitializationError( + f"{PROMPT_FILE_NAME} files are required to create a function from a directory, " + f"path: {str(path)}, config file is there." + ) + + function_name = os.path.basename(path) + + with open(config_path, "r") as config_file: + prompt_template_config = PromptTemplateConfig.from_json(config_file.read()) + prompt_template_config.name = function_name + + with open(prompt_path, "r") as prompt_file: + prompt_template_config.template = prompt_file.read() + + prompt_template = TEMPLATE_FORMAT_MAP[prompt_template_config.template_format]( # type: ignore + prompt_template_config=prompt_template_config + ) + return cls( + function_name=function_name, + plugin_name=plugin_name, + prompt_template=prompt_template, + prompt_template_config=prompt_template_config, + template_format=prompt_template_config.template_format, + description=prompt_template_config.description, + ) diff --git a/python/semantic_kernel/functions/kernel_plugin.py b/python/semantic_kernel/functions/kernel_plugin.py index 3d57be2366..c6217793a1 100644 --- a/python/semantic_kernel/functions/kernel_plugin.py +++ b/python/semantic_kernel/functions/kernel_plugin.py @@ -1,129 +1,523 @@ # Copyright (c) Microsoft. All rights reserved. +from __future__ import annotations +import importlib +import inspect +import json +import logging +import os import sys -from typing import TYPE_CHECKING, Dict, List, Optional, Union +from collections.abc import Callable, Iterable +from glob import glob +from types import MethodType +from typing import TYPE_CHECKING, Any, ItemsView + +from semantic_kernel.connectors.openapi_plugin.openapi_manager import create_functions_from_openapi +from semantic_kernel.exceptions.function_exceptions import FunctionInitializationError if sys.version_info >= (3, 9): - from typing import Annotated + from typing import Annotated # pragma: no cover else: - from typing_extensions import Annotated + from typing_extensions import Annotated # pragma: no cover +import httpx from pydantic import Field, StringConstraints -from semantic_kernel.exceptions import FunctionInvalidNameError +from semantic_kernel.connectors.openai_plugin.openai_authentication_config import OpenAIAuthenticationConfig +from semantic_kernel.connectors.openai_plugin.openai_function_execution_parameters import ( + OpenAIFunctionExecutionParameters, +) +from semantic_kernel.connectors.openai_plugin.openai_utils import OpenAIUtils +from semantic_kernel.connectors.utils.document_loader import DocumentLoader +from semantic_kernel.exceptions import PluginInitializationError +from semantic_kernel.functions.kernel_function import KernelFunction +from semantic_kernel.functions.kernel_function_from_method import KernelFunctionFromMethod +from semantic_kernel.functions.kernel_function_from_prompt import KernelFunctionFromPrompt +from semantic_kernel.functions.types import KERNEL_FUNCTION_TYPE from semantic_kernel.kernel_pydantic import KernelBaseModel from semantic_kernel.utils.validation import PLUGIN_NAME_REGEX if TYPE_CHECKING: - from semantic_kernel.functions.kernel_function import KernelFunction + from semantic_kernel.connectors.openapi_plugin.openapi_function_execution_parameters import ( + OpenAPIFunctionExecutionParameters, + ) from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata +logger = logging.getLogger(__name__) + class KernelPlugin(KernelBaseModel): """ Represents a Kernel Plugin with functions. + This class behaves mostly like a dictionary, with functions as values and their names as keys. + When you add a function, through `.set` or `__setitem__`, the function is copied, the metadata is deep-copied + and the name of the plugin is set in the metadata and added to the dict of functions. + This is done in the same way as a normal dict, so a existing key will be overwritten. + Attributes: name (str): The name of the plugin. The name can be upper/lower case letters and underscores. description (str): The description of the plugin. functions (Dict[str, KernelFunction]): The functions in the plugin, indexed by their name. + + Methods: + set, __setitem__ (key: str, value: KernelFunction): Set a function in the plugin. + get (key: str, default: KernelFunction | None = None): Get a function from the plugin. + __getitem__ (key: str): Get a function from the plugin. + __contains__ (key: str): Check if a function is in the plugin. + __iter__ (): Iterate over the functions in the plugin. + update(*args: Any, **kwargs: Any): Update the plugin with the functions from another. + setdefault(key: str, value: KernelFunction | None): Set a default value for a key. + get_functions_metadata(): Get the metadata for the functions in the plugin. + + Class methods: + from_object(plugin_name: str, plugin_instance: Any | dict[str, Any], description: str | None = None): + Create a plugin from a existing object, like a custom class with annotated functions. + from_directory(plugin_name: str, parent_directory: str, description: str | None = None): + Create a plugin from a directory, parsing: + .py files, .yaml files and directories with skprompt.txt and config.json files. + from_openapi( + plugin_name: str, + openapi_document_path: str, + execution_settings: OpenAPIFunctionExecutionParameters | None = None, + description: str | None = None): + Create a plugin from an OpenAPI document. + from_openai( + plugin_name: str, + plugin_url: str | None = None, + plugin_str: str | None = None, + execution_parameters: OpenAIFunctionExecutionParameters | None = None, + description: str | None = None): + Create a plugin from the Open AI manifest. + """ name: Annotated[str, StringConstraints(pattern=PLUGIN_NAME_REGEX, min_length=1)] - description: Optional[str] = Field(default=None) - functions: Optional[Dict[str, "KernelFunction"]] = Field(default_factory=dict) + description: str | None = None + functions: dict[str, KernelFunction] = Field(default_factory=dict) def __init__( self, name: str, - description: Optional[str] = None, - functions: Optional[Union[List["KernelFunction"], Dict[str, "KernelFunction"]]] = None, + description: str | None = None, + functions: ( + KERNEL_FUNCTION_TYPE + | KernelPlugin + | list[KERNEL_FUNCTION_TYPE | KernelPlugin] + | dict[str, KERNEL_FUNCTION_TYPE] + | None + ) = None, ): - """ - Initialize a new instance of the KernelPlugin class + """Create a KernelPlugin - Args: - name (str): The name of the plugin. - description (Optional[str]): The description of the plugin. - functions (List[KernelFunction]): The functions in the plugin. + Attributes: + name (str): The name of the plugin. The name can be upper/lower + case letters and underscores. + description (str, optional): The description of the plugin. + functions ( + KernelFunction | + Callable | + list[KernelFunction | Callable | KernelPlugin] | + dict[str, KernelFunction | Callable] | + KernelPlugin | + None): + The functions in the plugin, will be rewritten to a dictionary of functions. Raises: - ValueError: If the functions list contains duplicate function names. + ValueError: If the functions are not of the correct type. + PydanticError: If the name is not a valid plugin name. """ - functions_dict = {} - if functions is not None: - if isinstance(functions, list): - for function in functions: - if function.name in functions_dict: - raise FunctionInvalidNameError(f"Duplicate function name detected: {function.name}") - functions_dict[function.name] = function + super().__init__( + name=name, + description=description, + functions=self._validate_functions(functions=functions, plugin_name=name), + ) + + # region Dict-like methods + + def __setitem__(self, key: str, value: KernelFunction) -> None: + self.functions[key] = KernelPlugin._parse_or_copy(value, self.name) + + def set(self, key: str, value: KernelFunction) -> None: + """Set a function in the plugin. + + Args: + key (str): The name of the function. + value (KernelFunction): The function to set. + + """ + self[key] = value + + def __getitem__(self, key: str) -> KernelFunction: + return self.functions[key] + + def get(self, key: str, default: KernelFunction | None = None) -> KernelFunction | None: + return self.functions.get(key, default) + + def update(self, *args: Any, **kwargs: KernelFunction) -> None: + """Update the plugin with the functions from another. + + Args: + *args: The functions to update the plugin with, can be a dict, list or KernelPlugin. + **kwargs: The kernel functions to update the plugin with. + + """ + if len(args) > 1: + raise TypeError("update expected at most 1 arguments, got %d" % len(args)) + if args: + other = args[0] + if isinstance(other, KernelPlugin): + other = other.functions + if not isinstance(other, (dict, list)): + raise TypeError(f"Expected dict, KernelPlugin or list as arg, got {type(other)}") + if isinstance(other, dict): + for key in other: + self[key] = other[key] else: - functions_dict = functions - super().__init__(name=name, description=description, functions=functions_dict) + for item in other: + if isinstance(item, (KernelFunction, Callable)): + item = KernelPlugin._parse_or_copy(item, self.name) + self[item.name] = item + elif isinstance(item, KernelPlugin): + for key in item.functions: + self[key] = item.functions[key] + if kwargs: + for key in kwargs: + self[key] = kwargs[key] - def __len__(self) -> int: - """ - Gets the number of functions in the plugin. + def setdefault(self, key: str, value: KernelFunction | None = None): + if key not in self.functions: + if value is None: + raise ValueError("Value must be provided for new key.") + self[key] = value + return self[key] - Returns: - The number of functions in the plugin. + def __iter__(self) -> Iterable[KernelFunction]: + for function in self.functions.values(): + yield function - """ - return len(self.functions) + def __contains__(self, key: str) -> bool: + return key in self.functions - def __contains__(self, function_name: str) -> bool: - """ - Checks if the plugin contains a function with the specified name. + # endregion + # region Properties - 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) -> "KernelFunction": - """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["KernelFunction"], plugin_name: str, description: Optional[str] = None - ) -> "KernelPlugin": - """ - Creates a KernelPlugin from a KernelFunction instance. - - Args: - functions (List[KernelFunction]): 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) - - def get_functions_metadata(self) -> List["KernelFunctionMetadata"]: + def get_functions_metadata(self) -> list["KernelFunctionMetadata"]: """ Get the metadata for the functions in the plugin. Returns: A list of KernelFunctionMetadata instances. """ - return [func.metadata for func in self.functions.values()] + return [func.metadata for func in self] + + # endregion + # region Class Methods + + @classmethod + def from_object( + cls, plugin_name: str, plugin_instance: Any | dict[str, Any], description: str | None = None + ) -> "KernelPlugin": + """ + Creates a plugin that wraps the specified target object and imports it into the kernel's plugin collection + + 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. + """ + functions: list[KernelFunction] = [] + candidates: list[tuple[str, MethodType]] | ItemsView[str, Any] = [] + + if isinstance(plugin_instance, dict): + candidates = plugin_instance.items() + else: + candidates = inspect.getmembers(plugin_instance, inspect.ismethod) + # Read every method from the plugin instance + functions = [ + KernelFunctionFromMethod(method=candidate, plugin_name=plugin_name) + for _, candidate in candidates + if hasattr(candidate, "__kernel_function__") + ] + return cls(name=plugin_name, description=description, functions=functions) # type: ignore + + @classmethod + def from_directory( + cls, + plugin_name: str, + parent_directory: str, + description: str | None = None, + class_init_arguments: dict[str, dict[str, Any]] | None = None, + ) -> "KernelPlugin": + """Create a plugin from a specified directory. + + This method does not recurse into subdirectories beyond one level deep from the specified plugin directory. + For YAML files, function names are extracted from the content of the YAML files themselves (the name property). + For directories, the function name is assumed to be the name of the directory. Each KernelFunction object is + initialized with data parsed from the associated files and added to a list of functions that are then assigned + to the created KernelPlugin object. + A .py file is parsed and a plugin created, + the functions within as then combined with any other functions found. + The python file needs to contain a class with one or more kernel_function decorated methods. + If this class has a `__init__` method, it will be called with the arguments provided in the + `class_init_arguments` dictionary, the key needs to be the same as the name of the class, + with the value being a dictionary of arguments to pass to the class (using kwargs). + + Example: + Assuming a plugin directory structure as follows: + MyPlugins/ + |--- pluginA.yaml + |--- pluginB.yaml + |--- native_function.py + |--- Directory1/ + |--- skprompt.txt + |--- config.json + |--- Directory2/ + |--- skprompt.txt + |--- config.json + + Calling `KernelPlugin.from_directory("MyPlugins", "/path/to")` will create a KernelPlugin object named + "MyPlugins", containing KernelFunction objects for `pluginA.yaml`, `pluginB.yaml`, + `Directory1`, and `Directory2`, each initialized with their respective configurations. + And functions for anything within native_function.py. + + Args: + plugin_name (str): The name of the plugin, this is the name of the directory within the parent directory + parent_directory (str): The parent directory path where the plugin directory resides + description (str | None): The description of the plugin + class_init_arguments (dict[str, dict[str, Any]] | None): The class initialization arguments + + Returns: + KernelPlugin: The created plugin of type KernelPlugin. + + Raises: + PluginInitializationError: If the plugin directory does not exist. + PluginInvalidNameError: If the plugin name is invalid. + """ + plugin_directory = os.path.abspath(os.path.join(parent_directory, plugin_name)) + if not os.path.exists(plugin_directory): + raise PluginInitializationError(f"Plugin directory does not exist: {plugin_name}") + + functions: list[KernelFunction] = [] + for object in glob(os.path.join(plugin_directory, "*")): + logger.debug(f"Found object: {object}") + if os.path.isdir(object): + if os.path.basename(object).startswith("__"): + continue + try: + functions.append(KernelFunctionFromPrompt.from_directory(path=object)) + except FunctionInitializationError: + logger.warning(f"Failed to create function from directory: {object}") + elif object.endswith(".yaml") or object.endswith(".yml"): + with open(object, "r") as file: + try: + functions.append(KernelFunctionFromPrompt.from_yaml(file.read())) + except FunctionInitializationError: + logger.warning(f"Failed to create function from YAML file: {object}") + elif object.endswith(".py"): + try: + functions.extend( + cls.from_python_file( + plugin_name=plugin_name, + py_file=object, + description=description, + class_init_arguments=class_init_arguments, + ) + ) + except PluginInitializationError: + logger.warning(f"Failed to create function from Python file: {object}") + else: + logger.warning(f"Unknown file found: {object}") + if not functions: + raise PluginInitializationError(f"No functions found in folder: {parent_directory}/{plugin_name}") + return cls(name=plugin_name, description=description, functions=functions) + + @classmethod + def from_openapi( + cls, + plugin_name: str, + openapi_document_path: str, + execution_settings: "OpenAPIFunctionExecutionParameters | None" = None, + description: str | None = None, + ) -> "KernelPlugin": + """Create a plugin from an OpenAPI document. + + Args: + plugin_name (str): The name of the plugin + plugin_url (str | None): The URL of the plugin + plugin_str (str | None): The JSON string of the plugin + execution_parameters (OpenAIFunctionExecutionParameters | None): The execution parameters + description (str | None): The description of the plugin + + Returns: + KernelPlugin: The created plugin + + Raises: + PluginInitializationError: if the plugin URL or plugin JSON/YAML is not provided + """ + + if not openapi_document_path: + raise PluginInitializationError("OpenAPI document path is required.") + + return cls( + name=plugin_name, + description=description, + functions=create_functions_from_openapi( + plugin_name=plugin_name, + openapi_document_path=openapi_document_path, + execution_settings=execution_settings, + ), + ) + + @classmethod + async def from_openai( + cls, + plugin_name: str, + plugin_url: str | None = None, + plugin_str: str | None = None, + execution_parameters: OpenAIFunctionExecutionParameters | None = None, + description: str | None = None, + ) -> "KernelPlugin": + """Create a plugin from the Open AI manifest. + + Args: + plugin_name (str): The name of the plugin + plugin_url (str | None): The URL of the plugin + plugin_str (str | None): The JSON string of the plugin + execution_parameters (OpenAIFunctionExecutionParameters | None): The execution parameters + + Returns: + KernelPlugin: The created plugin + + Raises: + PluginInitializationError: if the plugin URL or plugin JSON/YAML is not provided + """ + + if execution_parameters is None: + execution_parameters = OpenAIFunctionExecutionParameters() + + if plugin_str is not None: + # Load plugin from the provided JSON string/YAML string + openai_manifest = plugin_str + elif plugin_url is not None: + # Load plugin from the URL + http_client = execution_parameters.http_client if execution_parameters.http_client else httpx.AsyncClient() + openai_manifest = await DocumentLoader.from_uri( + url=plugin_url, http_client=http_client, auth_callback=None, user_agent=execution_parameters.user_agent + ) + else: + raise PluginInitializationError("Either plugin_url or plugin_json must be provided.") + + try: + plugin_json = json.loads(openai_manifest) + except json.JSONDecodeError as ex: + raise PluginInitializationError("Parsing of Open AI manifest for auth config failed.") from ex + openai_auth_config = OpenAIAuthenticationConfig(**plugin_json["auth"]) + openapi_spec_url = OpenAIUtils.parse_openai_manifest_for_openapi_spec_url(plugin_json=plugin_json) + + # Modify the auth callback in execution parameters if it's provided + if execution_parameters and execution_parameters.auth_callback: + initial_auth_callback = execution_parameters.auth_callback + + async def custom_auth_callback(**kwargs: Any): + return await initial_auth_callback(plugin_name, openai_auth_config, **kwargs) # pragma: no cover + + execution_parameters.auth_callback = custom_auth_callback + + return cls( + name=plugin_name, + description=description, + functions=create_functions_from_openapi( + plugin_name=plugin_name, + openapi_document_path=openapi_spec_url, + execution_settings=execution_parameters, + ), + ) + + @classmethod + def from_python_file( + cls, + plugin_name: str, + py_file: str, + description: str | None = None, + class_init_arguments: dict[str, dict[str, Any]] | None = None, + ) -> "KernelPlugin": + module_name = os.path.basename(py_file).replace(".py", "") + spec = importlib.util.spec_from_file_location(module_name, py_file) + module = importlib.util.module_from_spec(spec) + assert spec.loader + spec.loader.exec_module(module) + + for name, cls_instance in inspect.getmembers(module, inspect.isclass): + if cls_instance.__module__ != module_name: + continue + instance = getattr(module, name)(**class_init_arguments.get(name, {}) if class_init_arguments else {}) + return cls.from_object(plugin_name=plugin_name, description=description, plugin_instance=instance) + raise PluginInitializationError(f"No class found in file: {py_file}") + + # endregion + # region Internal Static Methods + + @staticmethod + def _validate_functions( + functions: ( + KERNEL_FUNCTION_TYPE + | list[KERNEL_FUNCTION_TYPE | KernelPlugin] + | dict[str, KERNEL_FUNCTION_TYPE] + | KernelPlugin + | None + ), + plugin_name: str, + ) -> dict[str, KernelFunction]: + """Validates the functions and returns a dictionary of functions.""" + if not functions or not plugin_name: + # if the plugin_name is not present, the validation will fail, so no point in parsing. + return {} + if isinstance(functions, dict): + return { + name: KernelPlugin._parse_or_copy(function=function, plugin_name=plugin_name) + for name, function in functions.items() + } + if isinstance(functions, KernelPlugin): + return { + name: function.function_copy(plugin_name=plugin_name) for name, function in functions.functions.items() + } + if isinstance(functions, KernelFunction): + return {functions.name: KernelPlugin._parse_or_copy(function=functions, plugin_name=plugin_name)} + if isinstance(functions, Callable): + function = KernelPlugin._parse_or_copy(function=functions, plugin_name=plugin_name) + return {function.name: function} + if isinstance(functions, list): + functions_dict: dict[str, KernelFunction] = {} + for function in functions: + if isinstance(function, (KernelFunction, Callable)): + function = KernelPlugin._parse_or_copy(function=function, plugin_name=plugin_name) + functions_dict[function.name] = function + elif isinstance(function, KernelPlugin): # type: ignore + functions_dict.update( + { + name: KernelPlugin._parse_or_copy(function=function, plugin_name=plugin_name) + for name, function in function.functions.items() + } + ) + else: + raise ValueError(f"Invalid type for functions in list: {function} (type: {type(function)})") + return functions_dict + raise ValueError(f"Invalid type for supplied functions: {functions} (type: {type(functions)})") + + @staticmethod + def _parse_or_copy(function: KERNEL_FUNCTION_TYPE, plugin_name: str) -> KernelFunction: + """Handle the function and return a KernelFunction instance.""" + if isinstance(function, KernelFunction): + return function.function_copy(plugin_name=plugin_name) + if isinstance(function, Callable): + return KernelFunctionFromMethod(method=function, plugin_name=plugin_name) + raise ValueError(f"Invalid type for function: {function} (type: {type(function)})") + + # endregion diff --git a/python/semantic_kernel/functions/kernel_plugin_collection.py b/python/semantic_kernel/functions/kernel_plugin_collection.py deleted file mode 100644 index 7e780ad525..0000000000 --- a/python/semantic_kernel/functions/kernel_plugin_collection.py +++ /dev/null @@ -1,231 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import logging -from typing import TYPE_CHECKING, Any, Dict, Iterable, List, TypeVar, Union - -from semantic_kernel.exceptions import ( - PluginInitializationError, - PluginInvalidNameError, -) -from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata -from semantic_kernel.functions.kernel_plugin import KernelPlugin -from semantic_kernel.kernel_pydantic import KernelBaseModel - -# To support Python 3.8, need to use TypeVar since Iterable is not scriptable -KernelPluginType = TypeVar("KernelPluginType", bound=KernelPlugin) - -if TYPE_CHECKING: - from semantic_kernel.functions.kernel_function import KernelFunction - -logger = logging.getLogger(__name__) - - -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: Dict[str, "KernelPlugin"] - - 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 PluginInitializationError("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 PluginInvalidNameError("Plugin and plugin.name must not be None") - if plugin.name in plugins_dict: - raise PluginInvalidNameError(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.name in self.plugins.keys(): - logger.warning(f'Overwriting plugin "{plugin.name}" in collection') - self.plugins[plugin.name] = plugin - - def add_plugin_from_functions(self, plugin_name: str, functions: List["KernelFunction"]) -> None: - """ - Add a function to a new plugin in the collection - - Args: - plugin_name (str): The name of the plugin to create. - functions (List[KernelFunction]): 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 PluginInitializationError("Functions or plugin_name must not be None or empty") - - plugin = KernelPlugin.from_functions(plugin_name=plugin_name, functions=functions) - self.plugins[plugin_name] = plugin - - def add_functions_to_plugin(self, functions: List["KernelFunction"], plugin_name: str) -> None: - """ - Add functions to a plugin in the collection - - Args: - functions (List[KernelFunction]): 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 PluginInitializationError("Functions and plugin_name must not be None or empty") - - if plugin_name not in self.plugins: - self.plugins[plugin_name] = KernelPlugin(name=plugin_name, functions=functions) - return - - for func in functions: - if func.name in self.plugins[plugin_name].functions: - logger.warning(f'Overwriting function "{func.name}" in collection') - self.plugins[plugin_name].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 PluginInitializationError("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_list_of_function_metadata( - self, include_prompt: bool = True, include_native: bool = True - ) -> List[KernelFunctionMetadata]: - """ - Get a list of the function metadata in the plugin collection - - Args: - include_prompt (bool): Whether to include semantic functions in the list. - include_native (bool): Whether to include native functions in the list. - - Returns: - A list of KernelFunctionMetadata objects in the collection. - """ - if not self.plugins: - return [] - return [ - func.metadata - for plugin in self.plugins.values() - for func in plugin.functions.values() - if (include_prompt and func.is_prompt) or (include_native and not func.is_prompt) - ] - - 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/functions/types.py b/python/semantic_kernel/functions/types.py new file mode 100644 index 0000000000..70e8ac7406 --- /dev/null +++ b/python/semantic_kernel/functions/types.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft. All rights reserved. + +from typing import Any, Callable, Union + +from semantic_kernel.functions.kernel_function import KernelFunction + +KERNEL_FUNCTION_TYPE = Union[KernelFunction, Callable[..., Any]] diff --git a/python/semantic_kernel/kernel.py b/python/semantic_kernel/kernel.py index dadf0d0a74..6b5c7c6475 100644 --- a/python/semantic_kernel/kernel.py +++ b/python/semantic_kernel/kernel.py @@ -1,55 +1,28 @@ # Copyright (c) Microsoft. All rights reserved. from __future__ import annotations -import glob -import importlib -import inspect -import json import logging -import os from copy import copy -from types import MethodType -from typing import TYPE_CHECKING, Any, AsyncIterable, Callable, ItemsView, Literal, Type, TypeVar, Union +from typing import TYPE_CHECKING, Any, AsyncIterable, Callable, Literal, Type, TypeVar, Union -import httpx -import yaml from pydantic import Field, field_validator from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings -from semantic_kernel.connectors.openai_plugin.openai_authentication_config import OpenAIAuthenticationConfig -from semantic_kernel.connectors.openai_plugin.openai_function_execution_parameters import ( - OpenAIFunctionExecutionParameters, -) -from semantic_kernel.connectors.openai_plugin.openai_utils import OpenAIUtils -from semantic_kernel.connectors.openapi_plugin.openapi_function_execution_parameters import ( - OpenAPIFunctionExecutionParameters, -) -from semantic_kernel.connectors.openapi_plugin.openapi_manager import OpenAPIPlugin -from semantic_kernel.connectors.utils.document_loader import DocumentLoader from semantic_kernel.contents.streaming_content_mixin import StreamingContentMixin from semantic_kernel.events import FunctionInvokedEventArgs, FunctionInvokingEventArgs from semantic_kernel.exceptions import ( - FunctionInitializationError, - FunctionNameNotUniqueError, KernelFunctionAlreadyExistsError, KernelFunctionNotFoundError, KernelInvokeException, - KernelPluginInvalidConfigurationError, KernelPluginNotFoundError, KernelServiceNotFoundError, - PluginInitializationError, - PluginInvalidNameError, ServiceInvalidTypeError, TemplateSyntaxError, ) from semantic_kernel.functions.function_result import FunctionResult from semantic_kernel.functions.kernel_arguments import KernelArguments -from semantic_kernel.functions.kernel_function import TEMPLATE_FORMAT_MAP, KernelFunction -from semantic_kernel.functions.kernel_function_from_method import KernelFunctionFromMethod -from semantic_kernel.functions.kernel_function_from_prompt import KernelFunctionFromPrompt from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata from semantic_kernel.functions.kernel_plugin import KernelPlugin -from semantic_kernel.functions.kernel_plugin_collection import KernelPluginCollection from semantic_kernel.kernel_pydantic import KernelBaseModel from semantic_kernel.prompt_template.const import KERNEL_TEMPLATE_FORMAT_NAME, TEMPLATE_FORMAT_TYPES from semantic_kernel.prompt_template.prompt_template_base import PromptTemplateBase @@ -58,12 +31,19 @@ from semantic_kernel.reliability.pass_through_without_retry import PassThroughWi from semantic_kernel.reliability.retry_mechanism_base import RetryMechanismBase from semantic_kernel.services.ai_service_client_base import AIServiceClientBase from semantic_kernel.services.ai_service_selector import AIServiceSelector -from semantic_kernel.utils.validation import validate_plugin_name if TYPE_CHECKING: from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase from semantic_kernel.connectors.ai.embeddings.embedding_generator_base import EmbeddingGeneratorBase from semantic_kernel.connectors.ai.text_completion_client_base import TextCompletionClientBase + from semantic_kernel.connectors.openai_plugin.openai_function_execution_parameters import ( + OpenAIFunctionExecutionParameters, + ) + from semantic_kernel.connectors.openapi_plugin.openapi_function_execution_parameters import ( + OpenAPIFunctionExecutionParameters, + ) + from semantic_kernel.functions.kernel_function import KernelFunction + from semantic_kernel.functions.types import KERNEL_FUNCTION_TYPE T = TypeVar("T") @@ -78,7 +58,7 @@ class Kernel(KernelBaseModel): semantic/native functions, and manage plugins, memory, and AI services. Attributes: - plugins (KernelPluginCollection | None): The collection of plugins to be used by the kernel + plugins (dict[str, KernelPlugin] | None): The plugins to be used by the kernel services (dict[str, AIServiceClientBase]): The services to be used by the kernel retry_mechanism (RetryMechanismBase): The retry mechanism to be used by the kernel function_invoking_handlers (dict): The function invoking handlers @@ -87,7 +67,7 @@ class Kernel(KernelBaseModel): # region Init - plugins: KernelPluginCollection = Field(default_factory=KernelPluginCollection) + plugins: dict[str, KernelPlugin] = Field(default_factory=dict) services: dict[str, AIServiceClientBase] = Field(default_factory=dict) ai_service_selector: AIServiceSelector = Field(default_factory=AIServiceSelector) retry_mechanism: RetryMechanismBase = Field(default_factory=PassThroughWithoutRetry) @@ -100,7 +80,7 @@ class Kernel(KernelBaseModel): def __init__( self, - plugins: KernelPluginCollection | None = None, + plugins: KernelPlugin | dict[str, KernelPlugin] | list[KernelPlugin] | None = None, services: AIServiceClientBase | list[AIServiceClientBase] | dict[str, AIServiceClientBase] | None = None, ai_service_selector: AIServiceSelector | None = None, **kwargs: Any, @@ -109,10 +89,10 @@ class Kernel(KernelBaseModel): Initialize a new instance of the Kernel class. Args: - plugins (KernelPluginCollection | None): The collection of plugins to be used by the kernel + plugins (KernelPlugin | dict[str, KernelPlugin] | list[KernelPlugin] | None): + The plugins to be used by the kernel, will be rewritten to a dict with plugin name as key services (AIServiceClientBase | list[AIServiceClientBase] | dict[str, AIServiceClientBase] | None: - The services to be used by the kernel, - will be rewritten to a dict with service_id as key + The services to be used by the kernel, will be rewritten to a dict with service_id as key ai_service_selector (AIServiceSelector | None): The AI service selector to be used by the kernel, default is based on order of execution settings. **kwargs (Any): Additional fields to be passed to the Kernel model, @@ -123,14 +103,27 @@ class Kernel(KernelBaseModel): """ args = { "services": services, + "plugins": plugins, **kwargs, } if ai_service_selector: args["ai_service_selector"] = ai_service_selector - if plugins: - args["plugins"] = plugins super().__init__(**args) + @field_validator("plugins", mode="before") + @classmethod + def rewrite_plugins( + cls, plugins: KernelPlugin | list[KernelPlugin] | dict[str, KernelPlugin] | None = None + ) -> dict[str, KernelPlugin]: + """Rewrite plugins to a dictionary.""" + if not plugins: + return {} + if isinstance(plugins, KernelPlugin): + return {plugins.name: plugins} + if isinstance(plugins, list): + return {p.name: p for p in plugins} + return plugins + @field_validator("services", mode="before") @classmethod def rewrite_services( @@ -151,7 +144,7 @@ class Kernel(KernelBaseModel): async def invoke_stream( self, - function: KernelFunction | None = None, + function: "KernelFunction" | None = None, arguments: KernelArguments | None = None, function_name: str | None = None, plugin_name: str | None = None, @@ -182,7 +175,7 @@ class Kernel(KernelBaseModel): if not function: if not function_name or not plugin_name: raise KernelFunctionNotFoundError("No function(s) or function- and plugin-name provided") - function = self.func(plugin_name, function_name) + function = self.get_function(plugin_name, function_name) function_invoking_args = self.on_function_invoking(function.metadata, arguments) if function_invoking_args.is_cancel_requested: @@ -227,7 +220,7 @@ class Kernel(KernelBaseModel): async def invoke( self, - function: KernelFunction | None = None, + function: "KernelFunction" | None = None, arguments: KernelArguments | None = None, function_name: str | None = None, plugin_name: str | None = None, @@ -255,7 +248,7 @@ class Kernel(KernelBaseModel): if not function: if not function_name or not plugin_name: raise KernelFunctionNotFoundError("No function or plugin name provided") - function = self.func(plugin_name, function_name) + function = self.get_function(plugin_name, function_name) function_invoking_args = self.on_function_invoking(function.metadata, arguments) if function_invoking_args.is_cancel_requested: logger.info( @@ -343,6 +336,8 @@ class Kernel(KernelBaseModel): if not prompt: raise TemplateSyntaxError("The prompt is either null or empty.") + from semantic_kernel.functions.kernel_function_from_prompt import KernelFunctionFromPrompt + function = KernelFunctionFromPrompt( function_name=function_name, plugin_name=plugin_name, @@ -401,201 +396,175 @@ class Kernel(KernelBaseModel): del self.function_invoked_handlers[id(handler)] # endregion - # region Plugins + # region Plugins & Functions - def add_plugin(self, plugin_name: str, functions: list[KernelFunction], plugin: KernelPlugin | None = None) -> None: + def add_plugin( + self, + plugin: KernelPlugin | Any | dict[str, Any] | None = None, + plugin_name: str | None = None, + parent_directory: str | None = None, + description: str | None = None, + class_init_arguments: dict[str, dict[str, Any]] | None = None, + ) -> "KernelPlugin": """ - Adds a plugin to the kernel's collection of plugins. If a plugin instance is provided, + Adds a plugin to the kernel's collection of plugins. If a plugin is provided, it uses that instance instead of creating a new KernelPlugin. + See KernelPlugin.from_directory for more details on how the directory is parsed. Args: - plugin_name (str): The name of the plugin - functions (list[KernelFunction]): The functions to add to the plugin - plugin (KernelPlugin | None): 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 import_plugin_from_object(self, plugin_instance: Any | dict[str, Any], plugin_name: str) -> KernelPlugin: - """ - Creates a plugin that wraps the specified target object and imports it into the kernel's plugin collection - - 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. + plugin (KernelPlugin | Any | dict[str, Any]): The plugin to add. + This can be a KernelPlugin, in which case it is added straightaway and other parameters are ignored, + a custom class that contains methods with the kernel_function decorator + or a dictionary of functions with the kernel_function decorator for one or + several methods. + plugin_name (str | None): The name of the plugin, used if the plugin is not a KernelPlugin, + if the plugin is None and the parent_directory is set, + KernelPlugin.from_directory is called with those parameters, + see `KernelPlugin.from_directory` for details. + parent_directory (str | None): The parent directory path where the plugin directory resides + description (str | None): The description of the plugin, used if the plugin is not a KernelPlugin. + class_init_arguments (dict[str, dict[str, Any]] | None): The class initialization arguments Returns: - KernelPlugin: The imported plugin of type KernelPlugin. - """ - if not plugin_name.strip(): - raise PluginInvalidNameError("Plugin name cannot be empty") - logger.debug(f"Importing plugin {plugin_name}") - - functions: dict[str, KernelFunction] = {} - candidates: list[tuple[str, MethodType]] | ItemsView[str, Any] = [] - - if isinstance(plugin_instance, dict): - candidates = plugin_instance.items() - else: - candidates = inspect.getmembers(plugin_instance, inspect.ismethod) - # Read every method from the plugin instance - for _, candidate in candidates: - # If the method is a prompt function, register it - if not hasattr(candidate, "__kernel_function__"): - continue - - func = KernelFunctionFromMethod(plugin_name=plugin_name, method=candidate) - if func.name in functions: - raise FunctionNameNotUniqueError( - "Overloaded functions are not supported, " "please differentiate function names." - ) - functions[func.name] = func - logger.debug(f"Methods imported: {len(functions)}") - - plugin = KernelPlugin(name=plugin_name, functions=functions) - self.plugins.add(plugin) - - return plugin - - def import_native_plugin_from_directory( - self, parent_directory: str, plugin_directory_name: str - ) -> KernelPlugin | None: - MODULE_NAME = "native_function" - - validate_plugin_name(plugin_directory_name) - - plugin_directory = os.path.abspath(os.path.join(parent_directory, plugin_directory_name)) - native_py_file_path = os.path.join(plugin_directory, f"{MODULE_NAME}.py") - - if not os.path.exists(native_py_file_path): - raise PluginInitializationError(f"Native Plugin Python File does not exist: {native_py_file_path}") - - plugin_name = os.path.basename(plugin_directory) - - spec = importlib.util.spec_from_file_location(MODULE_NAME, native_py_file_path) - if not spec: - raise PluginInitializationError(f"Failed to load plugin: {plugin_name}") - module = importlib.util.module_from_spec(spec) - assert spec.loader - spec.loader.exec_module(module) - - class_name = next( - (name for name, cls in inspect.getmembers(module, inspect.isclass) if cls.__module__ == MODULE_NAME), - None, - ) - if class_name: - plugin_obj = getattr(module, class_name)() - return self.import_plugin_from_object(plugin_obj, plugin_name) - - return None - - def import_plugin_from_prompt_directory(self, parent_directory: str, plugin_directory_name: str) -> KernelPlugin: - """ - Import a plugin from a specified directory, processing both YAML files and subdirectories - containing `skprompt.txt` and `config.json` files to create KernelFunction objects. These objects - are then grouped into a single KernelPlugin instance. - - This method does not recurse into subdirectories beyond one level deep from the specified plugin directory. - For YAML files, function names are extracted from the content of the YAML files themselves (the name property). - For directories, the function name is assumed to be the name of the directory. Each KernelFunction object is - initialized with data parsed from the associated files and added to a list of functions that are then assigned - to the created KernelPlugin object. - - Args: - parent_directory (str): The parent directory path where the plugin directory resides. This should be - an absolute path to ensure correct file resolution. - plugin_directory_name (str): The name of the directory that contains the plugin's YAML files and - subdirectories. This directory name is used as the plugin name and should be directly under the - parent_directory. - - Returns: - KernelPlugin: An instance of KernelPlugin containing all the KernelFunction objects created from - the YAML files and directories found in the specified plugin directory. The name of the - plugin is set to the plugin_directory_name. + KernelPlugin: The plugin that was added. Raises: - PluginInitializationError: If the plugin directory does not exist. - PluginInvalidNameError: If the plugin name is invalid. + ValidationError: If a KernelPlugin needs to be created, but it is not valid. - Example: - Assuming a plugin directory structure as follows: - - MyPlugins/ - |--- pluginA.yaml - |--- pluginB.yaml - |--- Directory1/ - |--- skprompt.txt - |--- config.json - |--- Directory2/ - |--- skprompt.txt - |--- config.json - - Calling `import_plugin("/path/to", "MyPlugins")` will create a KernelPlugin object named - "MyPlugins", containing KernelFunction objects for `pluginA.yaml`, `pluginB.yaml`, - `Directory1`, and `Directory2`, each initialized with their respective configurations. """ - plugin_directory = self._validate_plugin_directory( - parent_directory=parent_directory, plugin_directory_name=plugin_directory_name - ) + if isinstance(plugin, KernelPlugin): + self.plugins[plugin.name] = plugin + return self.plugins[plugin.name] + if not plugin_name: + raise ValueError("plugin_name must be provided if a plugin is not supplied.") + if plugin: + self.plugins[plugin_name] = KernelPlugin.from_object( + plugin_name=plugin_name, plugin_instance=plugin, description=description + ) + return self.plugins[plugin_name] + if plugin is None and parent_directory is not None: + self.plugins[plugin_name] = KernelPlugin.from_directory( + plugin_name=plugin_name, + parent_directory=parent_directory, + description=description, + class_init_arguments=class_init_arguments, + ) + return self.plugins[plugin_name] + raise ValueError("plugin or parent_directory must be provided.") - functions = [] + def add_plugins(self, plugins: list[KernelPlugin | object] | dict[str, KernelPlugin | object]) -> None: + """ + Adds a list of plugins to the kernel's collection of plugins. - # Handle YAML files at the root - yaml_files = glob.glob(os.path.join(plugin_directory, "*.yaml")) - for yaml_file in yaml_files: - with open(yaml_file, "r") as file: - yaml_content = file.read() - functions.append(self.create_function_from_yaml(yaml_content, plugin_name=plugin_directory_name)) + Args: + plugins (list[KernelPlugin] | dict[str, KernelPlugin]): The plugins to add to the kernel + """ + if isinstance(plugins, list): + for plugin in plugins: + self.add_plugin(plugin) + return + for name, plugin in plugins.items(): + self.add_plugin(plugin, plugin_name=name) - # Handle directories containing skprompt.txt and config.json - for item in os.listdir(plugin_directory): - item_path = os.path.join(plugin_directory, item) - if os.path.isdir(item_path): - prompt_path = os.path.join(item_path, "skprompt.txt") - config_path = os.path.join(item_path, "config.json") - - if os.path.exists(prompt_path) and os.path.exists(config_path): - with open(config_path, "r") as config_file: - prompt_template_config = PromptTemplateConfig.from_json(config_file.read()) - prompt_template_config.name = item - - with open(prompt_path, "r") as prompt_file: - prompt = prompt_file.read() - prompt_template_config.template = prompt - - prompt_template = TEMPLATE_FORMAT_MAP[prompt_template_config.template_format]( # type: ignore - prompt_template_config=prompt_template_config - ) - - functions.append( - self.create_function_from_prompt( - plugin_name=plugin_directory_name, - prompt_template=prompt_template, - prompt_template_config=prompt_template_config, - template_format=prompt_template_config.template_format, - function_name=item, - description=prompt_template_config.description, - ) - ) - - return KernelPlugin(name=plugin_directory_name, functions=functions) - - async def import_plugin_from_openai( + def add_function( self, plugin_name: str, - plugin_url: str | None = None, - plugin_str: str | None = None, - execution_parameters: OpenAIFunctionExecutionParameters | None = None, + function: KERNEL_FUNCTION_TYPE | None = None, + function_name: str | None = None, + description: str | None = None, + prompt: str | None = None, + prompt_template_config: PromptTemplateConfig | None = None, + prompt_execution_settings: ( + PromptExecutionSettings | list[PromptExecutionSettings] | dict[str, PromptExecutionSettings] | None + ) = None, + template_format: TEMPLATE_FORMAT_TYPES = KERNEL_TEMPLATE_FORMAT_NAME, + prompt_template: PromptTemplateBase | None = None, + return_plugin: bool = False, + **kwargs: Any, + ) -> "KernelFunction | KernelPlugin": + """ + Adds a function to the specified plugin. + + Args: + plugin_name (str): The name of the plugin to add the function to + function (KernelFunction | Callable[..., Any]): The function to add + function_name (str): The name of the function + plugin_name (str): The name of the plugin + description (str | None): The description of the function + prompt (str | None): The prompt template. + prompt_template_config (PromptTemplateConfig | None): The prompt template configuration + prompt_execution_settings (PromptExecutionSettings | list[PromptExecutionSettings] + | dict[str, PromptExecutionSettings] | None): + The execution settings, will be parsed into a dict. + template_format (str | None): The format of the prompt template + prompt_template (PromptTemplateBase | None): The prompt template + return_plugin (bool): If True, the plugin is returned instead of the function + kwargs (Any): Additional arguments + + Returns: + KernelFunction | KernelPlugin: The function that was added, or the plugin if return_plugin is True + + """ + from semantic_kernel.functions.kernel_function import KernelFunction + + if function is None: + if not function_name or (not prompt and not prompt_template_config and not prompt_template): + raise ValueError( + "function_name and prompt, prompt_template_config or prompt_template must be provided if a function is not supplied." # noqa: E501 + ) + if prompt_execution_settings is None and ( + prompt_template_config is None or prompt_template_config.execution_settings is None + ): + prompt_execution_settings = PromptExecutionSettings(extension_data=kwargs) + + function = KernelFunction.from_prompt( + function_name=function_name, + plugin_name=plugin_name, + description=description, + prompt=prompt, + template_format=template_format, + prompt_template=prompt_template, + prompt_template_config=prompt_template_config, + prompt_execution_settings=prompt_execution_settings, + ) + elif not isinstance(function, KernelFunction): + function = KernelFunction.from_method(plugin_name=plugin_name, method=function) + if plugin_name not in self.plugins: + plugin = KernelPlugin(name=plugin_name, functions=function) + self.add_plugin(plugin) + return plugin if return_plugin else plugin[function.name] + self.plugins[plugin_name][function.name] = function + return self.plugins[plugin_name] if return_plugin else self.plugins[plugin_name][function.name] + + def add_functions( + self, + plugin_name: str, + functions: list[KERNEL_FUNCTION_TYPE] | dict[str, KERNEL_FUNCTION_TYPE], + ) -> "KernelPlugin": + """ + Adds a list of functions to the specified plugin. + + Args: + plugin_name (str): The name of the plugin to add the functions to + functions (list[KernelFunction] | dict[str, KernelFunction]): The functions to add + + Returns: + KernelPlugin: The plugin that the functions were added to. + + """ + if plugin_name in self.plugins: + self.plugins[plugin_name].update(functions) + return self.plugins[plugin_name] + return self.add_plugin(KernelPlugin(name=plugin_name, functions=functions)) # type: ignore + + def add_plugin_from_openapi( + self, + plugin_name: str, + openapi_document_path: str, + execution_settings: "OpenAPIFunctionExecutionParameters | None" = None, + description: str | None = None, ) -> KernelPlugin: - """Create a plugin from the Open AI manifest. + """Add a plugin from the Open AI manifest. Args: plugin_name (str): The name of the plugin @@ -609,246 +578,142 @@ class Kernel(KernelBaseModel): Raises: PluginInitializationError: if the plugin URL or plugin JSON/YAML is not provided """ - - if execution_parameters is None: - execution_parameters = OpenAIFunctionExecutionParameters() - - validate_plugin_name(plugin_name) - - if plugin_str is not None: - # Load plugin from the provided JSON string/YAML string - openai_manifest = plugin_str - elif plugin_url is not None: - # Load plugin from the URL - http_client = execution_parameters.http_client if execution_parameters.http_client else httpx.AsyncClient() - openai_manifest = await DocumentLoader.from_uri( - url=plugin_url, http_client=http_client, auth_callback=None, user_agent=execution_parameters.user_agent + return self.add_plugin( + KernelPlugin.from_openapi( + plugin_name=plugin_name, + openapi_document_path=openapi_document_path, + execution_settings=execution_settings, + description=description, ) - else: - raise PluginInitializationError("Either plugin_url or plugin_json must be provided.") - - try: - plugin_json = json.loads(openai_manifest) - openai_auth_config = OpenAIAuthenticationConfig(**plugin_json["auth"]) - except json.JSONDecodeError as ex: - raise KernelPluginInvalidConfigurationError("Parsing of Open AI manifest for auth config failed.") from ex - - # Modify the auth callback in execution parameters if it's provided - if execution_parameters and execution_parameters.auth_callback: - initial_auth_callback = execution_parameters.auth_callback - - async def custom_auth_callback(**kwargs): - return await initial_auth_callback(plugin_name, openai_auth_config, **kwargs) - - execution_parameters.auth_callback = custom_auth_callback - - try: - openapi_spec_url = OpenAIUtils.parse_openai_manifest_for_openapi_spec_url(plugin_json) - except PluginInitializationError as ex: - raise KernelPluginInvalidConfigurationError( - "Parsing of Open AI manifest for OpenAPI spec URL failed." - ) from ex - - return self.import_plugin_from_openapi( - plugin_name=plugin_name, - openapi_document_path=openapi_spec_url, - execution_settings=execution_parameters, ) - def import_plugin_from_openapi( + async def add_plugin_from_openai( self, plugin_name: str, - openapi_document_path: str, - execution_settings: "OpenAIFunctionExecutionParameters" | "OpenAPIFunctionExecutionParameters" | None = None, + plugin_url: str | None = None, + plugin_str: str | None = None, + execution_parameters: "OpenAIFunctionExecutionParameters | None" = None, + description: str | None = None, ) -> KernelPlugin: - """Create a plugin from an OpenAPI manifest. + """Add a plugin from an OpenAPI document. Args: plugin_name (str): The name of the plugin - openapi_document_path (str): The OpenAPI document path - execution_settings (OpenAIFunctionExecutionParameters | OpenAPIFunctionExecutionParameters | None): - The execution settings + plugin_url (str | None): The URL of the plugin + plugin_str (str | None): The JSON string of the plugin + execution_parameters (OpenAIFunctionExecutionParameters | None): The execution parameters + description (str | None): The description of the plugin Returns: KernelPlugin: The imported plugin - """ - validate_plugin_name(plugin_name) - - if not openapi_document_path: - raise PluginInitializationError("OpenAPI document path is required.") - - plugin = OpenAPIPlugin.create( - plugin_name=plugin_name, - openapi_document_path=openapi_document_path, - execution_settings=execution_settings, - ) - return self.import_plugin_from_object(plugin, plugin_name) - - def _validate_plugin_directory(self, parent_directory: str, plugin_directory_name: str) -> str: - """Validate the plugin name and that the plugin directory exists. - - Args: - parent_directory (str): The parent directory - plugin_directory_name (str): The plugin directory name - - Returns: - str: The plugin directory. Raises: - PluginInitializationError: If the plugin directory does not exist. - PluginInvalidNameError: If the plugin name is invalid. + PluginInitializationError: if the plugin URL or plugin JSON/YAML is not provided """ - validate_plugin_name(plugin_directory_name) - - plugin_directory = os.path.join(parent_directory, plugin_directory_name) - plugin_directory = os.path.abspath(plugin_directory) - - if not os.path.exists(plugin_directory): - raise PluginInitializationError(f"Plugin directory does not exist: {plugin_directory_name}") - - return plugin_directory - - # endregion - # region Functions - - def func(self, plugin_name: str, function_name: str) -> KernelFunction: - if plugin_name not in self.plugins: - raise KernelPluginNotFoundError(f"Plugin '{plugin_name}' not found") - if function_name not in self.plugins[plugin_name]: - raise KernelFunctionNotFoundError(f"Function '{function_name}' not found in plugin '{plugin_name}'") - return self.plugins[plugin_name][function_name] - - def func_from_fully_qualified_function_name(self, fully_qualified_function_name: str) -> KernelFunction: - plugin_name, function_name = fully_qualified_function_name.split("-") - if plugin_name not in self.plugins: - raise KernelPluginNotFoundError(f"Plugin '{plugin_name}' not found") - if function_name not in self.plugins[plugin_name]: - raise KernelFunctionNotFoundError(f"Function '{function_name}' not found in plugin '{plugin_name}'") - return self.plugins[plugin_name][function_name] - - def create_function_from_prompt( - self, - function_name: str, - plugin_name: str, - description: str | None = None, - prompt: str | None = None, - prompt_template_config: PromptTemplateConfig | None = None, - prompt_execution_settings: ( - PromptExecutionSettings | list[PromptExecutionSettings] | dict[str, PromptExecutionSettings] | None - ) = None, - template_format: TEMPLATE_FORMAT_TYPES = KERNEL_TEMPLATE_FORMAT_NAME, - prompt_template: PromptTemplateBase | None = None, - **kwargs: Any, - ) -> KernelFunction: - """ - Create a Kernel Function from a prompt. - - Args: - function_name (str): The name of the function - plugin_name (str): The name of the plugin - description (str | None): The description of the function - prompt (str | None): The prompt template. - prompt_template_config (PromptTemplateConfig | None): The prompt template configuration - prompt_execution_settings (PromptExecutionSettings | list[PromptExecutionSettings] - | dict[str, PromptExecutionSettings] | None): - The execution settings, will be parsed into a dict. - template_format (str | None): The format of the prompt template - prompt_template (PromptTemplateBase | None): The prompt template - kwargs (Any): Additional arguments - - Returns: - KernelFunction: The created Kernel Function - """ - if prompt_execution_settings is None and ( - prompt_template_config is None or prompt_template_config.execution_settings is None - ): - prompt_execution_settings = PromptExecutionSettings(extension_data=kwargs) - - function = KernelFunctionFromPrompt( - function_name=function_name, - plugin_name=plugin_name, - description=description, - prompt=prompt, - template_format=template_format, - prompt_template=prompt_template, - prompt_template_config=prompt_template_config, - prompt_execution_settings=prompt_execution_settings, - ) - - self.add_plugin(plugin_name or function.plugin_name, [function]) - - return function - - def create_function_from_yaml(self, text: str, plugin_name: str) -> KernelFunction: - """ - Import a plugin from a YAML string. - - Args: - text (str): The YAML string - - Returns: - KernelFunction: The created Kernel Function - - Raises: - PluginInitializationError: If the input YAML string is empty - """ - if not text: - raise PluginInitializationError("The input YAML string is empty") - - try: - data = yaml.safe_load(text) - except yaml.YAMLError as exc: - raise PluginInitializationError(f"Error loading YAML: {exc}") from exc - - if not isinstance(data, dict): - raise PluginInitializationError("The YAML content must represent a dictionary") - - try: - prompt_template_config = PromptTemplateConfig(**data) - except TypeError as exc: - raise PluginInitializationError(f"Error initializing PromptTemplateConfig: {exc}") from exc - - return self.create_function_from_prompt( - function_name=prompt_template_config.name, - plugin_name=plugin_name, - description=prompt_template_config.description, - prompt_template_config=prompt_template_config, - template_format=prompt_template_config.template_format, - ) - - def register_function_from_method( - self, - plugin_name: str, - method: Callable[..., Any], - ) -> KernelFunction: - """ - Creates a native function from the plugin name and registers it with the kernel. - - Args: - plugin_name (str | None): The name of the plugin. If empty, a random name will be generated. - kernel_function (Callable): The kernel function - - Returns: - KernelFunction: The created native function - """ - if not hasattr(method, "__kernel_function__"): - raise FunctionInitializationError( - "kernel_function argument must be decorated with @kernel_function", + return self.add_plugin( + await KernelPlugin.from_openai( + plugin_name=plugin_name, + plugin_url=plugin_url, + plugin_str=plugin_str, + execution_parameters=execution_parameters, + description=description, ) - - function = KernelFunctionFromMethod( - method=method, - plugin_name=plugin_name, ) - self.add_plugin(plugin_name or function.plugin_name, [function]) - return function + def get_plugin(self, plugin_name: str) -> "KernelPlugin": + """Get a plugin by name. + + Args: + plugin_name (str): The name of the plugin + + Returns: + KernelPlugin: The plugin + + Raises: + KernelPluginNotFoundError: If the plugin is not found + + """ + if plugin_name not in self.plugins: + raise KernelPluginNotFoundError(f"Plugin '{plugin_name}' not found") + return self.plugins[plugin_name] + + def get_function(self, plugin_name: str | None, function_name: str) -> "KernelFunction": + """Get a function by plugin_name and function_name. + + Args: + plugin_name (str | None): The name of the plugin + function_name (str): The name of the function + + Returns: + KernelFunction: The function + + Raises: + KernelPluginNotFoundError: If the plugin is not found + KernelFunctionNotFoundError: If the function is not found + + """ + if plugin_name is None: + for plugin in self.plugins.values(): + if function_name in plugin: + return plugin[function_name] + raise KernelFunctionNotFoundError(f"Function '{function_name}' not found in any plugin.") + if plugin_name not in self.plugins: + raise KernelPluginNotFoundError(f"Plugin '{plugin_name}' not found") + if function_name not in self.plugins[plugin_name]: + raise KernelFunctionNotFoundError(f"Function '{function_name}' not found in plugin '{plugin_name}'") + return self.plugins[plugin_name][function_name] + + def get_function_from_fully_qualified_function_name(self, fully_qualified_function_name: str) -> "KernelFunction": + """Get a function by its fully qualified name (-). + + Args: + fully_qualified_function_name (str): The fully qualified name of the function, + if there is no '-' in the name, it is assumed that it is only a function_name. + + Returns: + KernelFunction: The function + + Raises: + KernelPluginNotFoundError: If the plugin is not found + KernelFunctionNotFoundError: If the function is not found + + """ + names = fully_qualified_function_name.split("-", maxsplit=1) + if len(names) == 1: + plugin_name = None + function_name = names[0] + else: + plugin_name = names[0] + function_name = names[1] + return self.get_function(plugin_name, function_name) + + def get_list_of_function_metadata( + self, include_prompt: bool = True, include_native: bool = True + ) -> list[KernelFunctionMetadata]: + """ + Get a list of the function metadata in the plugin collection + + Args: + include_prompt (bool): Whether to include semantic functions in the list. + include_native (bool): Whether to include native functions in the list. + + Returns: + A list of KernelFunctionMetadata objects in the collection. + """ + if not self.plugins: + return [] + return [ + func.metadata + for plugin in self.plugins.values() + for func in plugin.functions.values() + if (include_prompt and func.is_prompt) or (include_native and not func.is_prompt) + ] # endregion # region Services def select_ai_service( - self, function: KernelFunction, arguments: KernelArguments + self, function: "KernelFunction", arguments: KernelArguments ) -> tuple[ALL_SERVICE_TYPES, PromptExecutionSettings]: """Uses the AI service selector to select a service for the function.""" return self.ai_service_selector.select_ai_service(self, function, arguments) diff --git a/python/semantic_kernel/planners/action_planner/action_planner.py b/python/semantic_kernel/planners/action_planner/action_planner.py index 5c7cc368ad..5a4075991a 100644 --- a/python/semantic_kernel/planners/action_planner/action_planner.py +++ b/python/semantic_kernel/planners/action_planner/action_planner.py @@ -74,13 +74,13 @@ class ActionPlanner: extension_data={"max_tokens": self.config.max_tokens, "stop_sequences": self._stop_sequence}, ) - self._planner_function = kernel.create_function_from_prompt( - function_name="ActionPlanner", + kernel.add_plugin(self, self.RESTRICTED_PLUGIN_NAME) + self._planner_function = kernel.add_function( plugin_name=self.RESTRICTED_PLUGIN_NAME, + function_name="ActionPlanner", prompt=self._prompt_template, prompt_execution_settings=execute_settings, ) - kernel.import_plugin_from_object(self, self.RESTRICTED_PLUGIN_NAME) self._kernel = kernel self._arguments = KernelArguments() @@ -213,7 +213,7 @@ class ActionPlanner: def list_of_functions(self, goal: Annotated[str, "The current goal processed by the planner"]) -> str: available_functions = [ self._create_function_string(func) - for func in self._kernel.plugins.get_list_of_function_metadata() + for func in self._kernel.get_list_of_function_metadata() if ( func.plugin_name != self.RESTRICTED_PLUGIN_NAME and func.plugin_name not in self.config.excluded_plugins diff --git a/python/semantic_kernel/planners/basic_planner.py b/python/semantic_kernel/planners/basic_planner.py index 63623e23a6..461efc15ad 100644 --- a/python/semantic_kernel/planners/basic_planner.py +++ b/python/semantic_kernel/planners/basic_planner.py @@ -142,9 +142,7 @@ class BasicPlanner: # Get a dictionary of plugin names to all native and semantic functions if not kernel.plugins: return "" - all_functions = { - f"{func.plugin_name}.{func.name}": func for func in kernel.plugins.get_list_of_function_metadata() - } + all_functions = {f"{func.plugin_name}.{func.name}": func for func in kernel.get_list_of_function_metadata()} all_functions_descriptions_dict = {key: func.description for key, func in all_functions.items()} all_functions_params_dict = {key: func.parameters for key, func in all_functions.items()} @@ -190,7 +188,7 @@ class BasicPlanner: ) # Create the prompt function for the planner with the given prompt - planner = kernel.create_function_from_prompt( + planner = kernel.add_function( plugin_name="PlannerPlugin", function_name="CreatePlan", prompt_template_config=prompt_template_config, @@ -225,7 +223,7 @@ class BasicPlanner: for subtask in subtasks: plugin_name, function_name = subtask["function"].split(".") - kernel_function = kernel.func(plugin_name, function_name) + kernel_function = kernel.get_function(plugin_name, function_name) # Get the arguments dictionary for the function args = subtask.get("args", None) if args: diff --git a/python/semantic_kernel/planners/function_calling_stepwise_planner/function_calling_stepwise_planner.py b/python/semantic_kernel/planners/function_calling_stepwise_planner/function_calling_stepwise_planner.py index 7e79c502b6..148f278d2b 100644 --- a/python/semantic_kernel/planners/function_calling_stepwise_planner/function_calling_stepwise_planner.py +++ b/python/semantic_kernel/planners/function_calling_stepwise_planner/function_calling_stepwise_planner.py @@ -15,10 +15,7 @@ from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.open_ai_pro ) from semantic_kernel.connectors.ai.open_ai.services.azure_chat_completion import AzureChatCompletion from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion import OpenAIChatCompletion -from semantic_kernel.connectors.ai.open_ai.utils import ( - get_function_calling_object, - get_tool_call_object, -) +from semantic_kernel.connectors.ai.open_ai.utils import get_function_calling_object, get_tool_call_object from semantic_kernel.contents.chat_history import ChatHistory from semantic_kernel.exceptions.planner_exceptions import PlannerInvalidConfigurationError from semantic_kernel.functions.kernel_arguments import KernelArguments @@ -138,7 +135,7 @@ class FunctionCallingStepwisePlanner(KernelBaseModel): # Clone the kernel so that we can add planner-specific plugins without affecting the original kernel instance cloned_kernel = copy(kernel) - cloned_kernel.import_plugin_from_object(UserInteraction(), "UserInteraction") + cloned_kernel.add_plugin(UserInteraction(), "UserInteraction") # Create and invoke a kernel function to generate the initial plan initial_plan = await self._generate_plan(question=question, kernel=cloned_kernel, arguments=arguments) @@ -224,7 +221,7 @@ class FunctionCallingStepwisePlanner(KernelBaseModel): if "default" in prompt_template_config.execution_settings: settings = prompt_template_config.execution_settings.pop("default") prompt_template_config.execution_settings[self.service_id] = settings - return kernel.create_function_from_prompt( + return kernel.add_function( function_name="create_plan", plugin_name="sequential_planner", description="Create a plan for the given goal", diff --git a/python/semantic_kernel/planners/planner_extensions.py b/python/semantic_kernel/planners/planner_extensions.py index ed5e480b7c..f97dafa12d 100644 --- a/python/semantic_kernel/planners/planner_extensions.py +++ b/python/semantic_kernel/planners/planner_extensions.py @@ -61,7 +61,7 @@ class PlannerKernelExtension: available_functions = [ func - for func in kernel.plugins.get_list_of_function_metadata() + for func in kernel.get_list_of_function_metadata() if (func.plugin_name not in excluded_plugins and func.name not in excluded_functions) ] diff --git a/python/semantic_kernel/planners/sequential_planner/sequential_planner.py b/python/semantic_kernel/planners/sequential_planner/sequential_planner.py index f915612332..1c1f08c1bf 100644 --- a/python/semantic_kernel/planners/sequential_planner/sequential_planner.py +++ b/python/semantic_kernel/planners/sequential_planner/sequential_planner.py @@ -84,7 +84,7 @@ class SequentialPlanner: settings = prompt_config.execution_settings.pop("default") prompt_config.execution_settings[service_id] = settings - return self._kernel.create_function_from_prompt( + return self._kernel.add_function( plugin_name=self.RESTRICTED_PLUGIN_NAME, function_name=self.RESTRICTED_PLUGIN_NAME, prompt_template_config=prompt_config, diff --git a/python/semantic_kernel/planners/sequential_planner/sequential_planner_extensions.py b/python/semantic_kernel/planners/sequential_planner/sequential_planner_extensions.py index 975c455bf2..debdf278fb 100644 --- a/python/semantic_kernel/planners/sequential_planner/sequential_planner_extensions.py +++ b/python/semantic_kernel/planners/sequential_planner/sequential_planner_extensions.py @@ -64,7 +64,7 @@ class SequentialPlannerKernelExtension: available_functions = [ func - for func in kernel.plugins.get_list_of_function_metadata() + for func in kernel.get_list_of_function_metadata() if (func.plugin_name not in excluded_plugins and func.name not in excluded_functions) ] diff --git a/python/semantic_kernel/planners/sequential_planner/sequential_planner_parser.py b/python/semantic_kernel/planners/sequential_planner/sequential_planner_parser.py index fbf082f55a..7ccb899ed2 100644 --- a/python/semantic_kernel/planners/sequential_planner/sequential_planner_parser.py +++ b/python/semantic_kernel/planners/sequential_planner/sequential_planner_parser.py @@ -72,7 +72,7 @@ class SequentialPlanParser: raise PlannerInvalidPlanError(f"Failed to find function '{plugin_function_name}'.") from exc else: try: - func = kernel.func_from_fully_qualified_function_name(plugin_function_name) + func = kernel.get_function_from_fully_qualified_function_name(plugin_function_name) except (KernelFunctionNotFoundError, KernelPluginNotFoundError) as exc: if allow_missing_functions: plan.add_steps([Plan.from_goal(plugin_function_name)]) diff --git a/python/semantic_kernel/planners/stepwise_planner/stepwise_planner.py b/python/semantic_kernel/planners/stepwise_planner/stepwise_planner.py index 1c4ea0a40c..8e2137f275 100644 --- a/python/semantic_kernel/planners/stepwise_planner/stepwise_planner.py +++ b/python/semantic_kernel/planners/stepwise_planner/stepwise_planner.py @@ -86,7 +86,7 @@ class StepwisePlanner: prompt_config.template = prompt_template self._system_step_function = self.import_function_from_prompt(kernel, "StepwiseStep", prompt_config) - self._native_functions = self._kernel.import_plugin_from_object(self, RESTRICTED_PLUGIN_NAME) + self._native_functions = self._kernel.add_plugin(self, RESTRICTED_PLUGIN_NAME) self._arguments = KernelArguments() @@ -324,7 +324,7 @@ class StepwisePlanner: raise PlannerExecutionException(f"The function '{action_name}' was not found.") try: - function = self._kernel.func(target_function.plugin_name, target_function.name) + function = self._kernel.get_function(target_function.plugin_name, target_function.name) action_arguments = self.create_action_arguments(action_variables) result = await function.invoke(self._kernel, action_arguments) @@ -360,7 +360,7 @@ class StepwisePlanner: excluded_functions = self.config.excluded_functions or [] available_functions = [ func - for func in self._kernel.plugins.get_list_of_function_metadata() + for func in self._kernel.get_list_of_function_metadata() if (func.plugin_name not in excluded_plugins and func.name not in excluded_functions) ] available_functions = sorted(available_functions, key=lambda x: (x.plugin_name, x.name)) @@ -379,9 +379,10 @@ class StepwisePlanner: function_name: str, config: PromptTemplateConfig = None, ) -> "KernelFunction": - return kernel.create_function_from_prompt( + kernel.add_function( plugin_name=RESTRICTED_PLUGIN_NAME, function_name=function_name, prompt_template_config=config ) + return kernel.get_function(RESTRICTED_PLUGIN_NAME, function_name) def to_manual_string(self, function: KernelFunctionMetadata) -> str: inputs = [ diff --git a/python/semantic_kernel/prompt_template/handlebars_prompt_template.py b/python/semantic_kernel/prompt_template/handlebars_prompt_template.py index 9a3e7b4b18..3ddd557ea9 100644 --- a/python/semantic_kernel/prompt_template/handlebars_prompt_template.py +++ b/python/semantic_kernel/prompt_template/handlebars_prompt_template.py @@ -72,16 +72,16 @@ class HandlebarsPromptTemplate(PromptTemplateBase): """ if not self._template_compiler: return "" - if not arguments: + if arguments is None: arguments = KernelArguments() helpers = {} - for plugin in kernel.plugins: + for plugin in kernel.plugins.values(): helpers.update( { function.fully_qualified_name: create_template_helper_from_function( function, kernel, arguments, self.prompt_template_config.template_format ) - for function in plugin.functions.values() + for function in plugin } ) helpers.update(HANDLEBAR_SYSTEM_HELPERS) diff --git a/python/semantic_kernel/prompt_template/jinja2_prompt_template.py b/python/semantic_kernel/prompt_template/jinja2_prompt_template.py index b228ce8fbc..7948a39e10 100644 --- a/python/semantic_kernel/prompt_template/jinja2_prompt_template.py +++ b/python/semantic_kernel/prompt_template/jinja2_prompt_template.py @@ -77,11 +77,11 @@ class Jinja2PromptTemplate(PromptTemplateBase): """ if not self._env: return "" - if not arguments: + if arguments is None: arguments = KernelArguments() helpers = {} helpers.update(JINJA2_SYSTEM_HELPERS) - for plugin in kernel.plugins: + for plugin in kernel.plugins.values(): helpers.update( { function.fully_qualified_name.replace("-", "_"): create_template_helper_from_function( @@ -90,7 +90,7 @@ class Jinja2PromptTemplate(PromptTemplateBase): arguments, self.prompt_template_config.template_format, ) - for function in plugin.functions.values() + for function in plugin } ) try: diff --git a/python/semantic_kernel/prompt_template/kernel_prompt_template.py b/python/semantic_kernel/prompt_template/kernel_prompt_template.py index f073ef8ff0..70e4954046 100644 --- a/python/semantic_kernel/prompt_template/kernel_prompt_template.py +++ b/python/semantic_kernel/prompt_template/kernel_prompt_template.py @@ -92,7 +92,7 @@ class KernelPromptTemplate(PromptTemplateBase): Returns: The prompt template ready to be used for an AI request """ - if not arguments: + if arguments is None: arguments = KernelArguments() return await self.render_blocks(self._blocks, kernel, arguments) diff --git a/python/semantic_kernel/template_engine/blocks/code_block.py b/python/semantic_kernel/template_engine/blocks/code_block.py index 438d61cf54..061f9f577a 100644 --- a/python/semantic_kernel/template_engine/blocks/code_block.py +++ b/python/semantic_kernel/template_engine/blocks/code_block.py @@ -2,21 +2,19 @@ import logging from copy import copy -from typing import TYPE_CHECKING, Any, ClassVar, List, Optional +from typing import TYPE_CHECKING, Any, ClassVar, List from pydantic import Field, field_validator, model_validator from semantic_kernel.exceptions import CodeBlockRenderException, CodeBlockTokenError +from semantic_kernel.exceptions.kernel_exceptions import KernelFunctionNotFoundError, KernelPluginNotFoundError from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata -from semantic_kernel.functions.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 from semantic_kernel.template_engine.code_tokenizer import CodeTokenizer if TYPE_CHECKING: from semantic_kernel.functions.kernel_arguments import KernelArguments - from semantic_kernel.functions.kernel_function import KernelFunction from semantic_kernel.kernel import Kernel logger: logging.Logger = logging.getLogger(__name__) @@ -116,11 +114,12 @@ these will be ignored." async def _render_function_call(self, kernel: "Kernel", arguments: "KernelArguments"): function_block = self.tokens[0] - function = self._get_function_from_plugin_collection(kernel.plugins, function_block) - if not function: + try: + function = kernel.get_function(function_block.plugin_name, function_block.function_name) + except (KernelFunctionNotFoundError, KernelPluginNotFoundError) as exc: error_msg = f"Function `{function_block.content}` not found" logger.error(error_msg) - raise CodeBlockRenderException(error_msg) + raise CodeBlockRenderException(error_msg) from exc arguments_clone = copy(arguments) if len(self.tokens) > 1: @@ -152,26 +151,3 @@ these will be ignored." arguments[token.name] = rendered_value return arguments - - def _get_function_from_plugin_collection( - self, plugins: KernelPluginCollection, function_block: FunctionIdBlock - ) -> Optional["KernelFunction"]: - """ - Get the function from the plugin collection - - Args: - plugins: The plugin collection - function_block: The function block that contains the function name - - Returns: - The function if it exists, None otherwise. - """ - if function_block.plugin_name is not None and len(function_block.plugin_name) > 0: - return plugins[function_block.plugin_name][function_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 function_block.function_name in plugin: - return plugin[function_block.function_name] - - return None diff --git a/python/tests/assets/test_native_plugins/TestNativePlugin/native_function.py b/python/tests/assets/test_native_plugins/TestNativePlugin/custom_class.py similarity index 100% rename from python/tests/assets/test_native_plugins/TestNativePlugin/native_function.py rename to python/tests/assets/test_native_plugins/TestNativePlugin/custom_class.py diff --git a/python/tests/assets/test_native_plugins/TestNativePluginArgs/class_args.py b/python/tests/assets/test_native_plugins/TestNativePluginArgs/class_args.py new file mode 100644 index 0000000000..9fa0e7507a --- /dev/null +++ b/python/tests/assets/test_native_plugins/TestNativePluginArgs/class_args.py @@ -0,0 +1,36 @@ +import sys +from typing import Optional + +from semantic_kernel.functions.kernel_function_decorator import kernel_function + +if sys.version_info >= (3, 9): + from typing import Annotated +else: + from typing_extensions import Annotated + + +class TestNativeEchoBotPlugin: + """ + Description: Test Native Plugin for testing purposes + """ + + def __init__(self, static_input: Optional[str] = None): + self.static_input = static_input or "" + + @kernel_function( + description="Echo for input text with static", + name="echo", + ) + def echo(self, text: Annotated[str, "The text to echo"]) -> str: + """ + Echo for input text with a static input + + Example: + "hello world" => "hello world" + Args: + text -- The text to echo + + Returns: + input text + """ + return self.static_input + text diff --git a/python/tests/assets/test_plugins/TestPlugin/TestNoFunction/something_else.txt b/python/tests/assets/test_plugins/TestFunctionBadYaml/bad.yaml similarity index 100% rename from python/tests/assets/test_plugins/TestPlugin/TestNoFunction/something_else.txt rename to python/tests/assets/test_plugins/TestFunctionBadYaml/bad.yaml diff --git a/python/tests/assets/test_plugins/TestFunctionYaml/empty.yaml b/python/tests/assets/test_plugins/TestFunctionYaml/empty.yaml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python/tests/assets/test_plugins/TestPlugin/TestFunctionYaml/test_function.yaml b/python/tests/assets/test_plugins/TestFunctionYaml/test_function.yaml similarity index 100% rename from python/tests/assets/test_plugins/TestPlugin/TestFunctionYaml/test_function.yaml rename to python/tests/assets/test_plugins/TestFunctionYaml/test_function.yaml diff --git a/python/tests/assets/test_plugins/TestPlugin/TestFunctionYamlHandlebars/test_function.yaml b/python/tests/assets/test_plugins/TestFunctionYamlHandlebars/test_function.yaml similarity index 100% rename from python/tests/assets/test_plugins/TestPlugin/TestFunctionYamlHandlebars/test_function.yaml rename to python/tests/assets/test_plugins/TestFunctionYamlHandlebars/test_function.yaml diff --git a/python/tests/assets/test_plugins/TestPlugin/TestFunctionYamlJinja2/test_function.yaml b/python/tests/assets/test_plugins/TestFunctionYamlJinja2/test_function.yaml similarity index 100% rename from python/tests/assets/test_plugins/TestPlugin/TestFunctionYamlJinja2/test_function.yaml rename to python/tests/assets/test_plugins/TestFunctionYamlJinja2/test_function.yaml diff --git a/python/tests/assets/test_plugins/TestMixedPlugin/TestFunction/config.json b/python/tests/assets/test_plugins/TestMixedPlugin/TestFunction/config.json new file mode 100644 index 0000000000..12cd2b235b --- /dev/null +++ b/python/tests/assets/test_plugins/TestMixedPlugin/TestFunction/config.json @@ -0,0 +1,13 @@ +{ + "schema": 1, + "description": "Test Description", + "execution_settings": { + "default": { + "max_tokens": 123, + "temperature": 0.0, + "top_p": 1.0, + "presence_penalty": 0.0, + "frequency_penalty": 2.0 + } + } +} \ No newline at end of file diff --git a/python/tests/assets/test_plugins/TestMixedPlugin/TestFunction/skprompt.txt b/python/tests/assets/test_plugins/TestMixedPlugin/TestFunction/skprompt.txt new file mode 100644 index 0000000000..b57378fade --- /dev/null +++ b/python/tests/assets/test_plugins/TestMixedPlugin/TestFunction/skprompt.txt @@ -0,0 +1,5 @@ +{{$input}} + +== +Test prompt. +== diff --git a/python/tests/assets/test_plugins/TestMixedPlugin/native_function.py b/python/tests/assets/test_plugins/TestMixedPlugin/native_function.py new file mode 100644 index 0000000000..30b42014ee --- /dev/null +++ b/python/tests/assets/test_plugins/TestMixedPlugin/native_function.py @@ -0,0 +1,32 @@ +import sys + +from semantic_kernel.functions.kernel_function_decorator import kernel_function + +if sys.version_info >= (3, 9): + from typing import Annotated +else: + from typing_extensions import Annotated + + +class TestNativeEchoBotPlugin: + """ + Description: Test Native Plugin for testing purposes + """ + + @kernel_function( + description="Echo for input text", + name="echoAsync", + ) + async def echo(self, text: Annotated[str, "The text to echo"]) -> str: + """ + Echo for input text + + Example: + "hello world" => "hello world" + Args: + text -- The text to echo + + Returns: + input text + """ + return text diff --git a/python/tests/assets/test_plugins/TestMixedPlugin/test_function.yaml b/python/tests/assets/test_plugins/TestMixedPlugin/test_function.yaml new file mode 100644 index 0000000000..d7049fa34e --- /dev/null +++ b/python/tests/assets/test_plugins/TestMixedPlugin/test_function.yaml @@ -0,0 +1,12 @@ +name: TestFunctionYaml +template_format: semantic-kernel +template: | + {{$input}} +description: A test function from a yaml file. +execution_settings: + default: + temperature: 0.6 + max_tokens: 123 + top_p: 1.0 + presence_penalty: 0.0 + frequency_penalty: 2.0 diff --git a/python/tests/assets/test_plugins/TestNoFunction/something_else.txt b/python/tests/assets/test_plugins/TestNoFunction/something_else.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python/tests/assets/test_plugins/TestPlugin/TestOpenAIPlugin/akv-openai.json b/python/tests/assets/test_plugins/TestOpenAIPlugin/akv-openai.json similarity index 100% rename from python/tests/assets/test_plugins/TestPlugin/TestOpenAIPlugin/akv-openai.json rename to python/tests/assets/test_plugins/TestOpenAIPlugin/akv-openai.json diff --git a/python/tests/assets/test_plugins/TestPlugin/TestOpenAPIPlugin/akv-openapi.yaml b/python/tests/assets/test_plugins/TestOpenAPIPlugin/akv-openapi.yaml similarity index 100% rename from python/tests/assets/test_plugins/TestPlugin/TestOpenAPIPlugin/akv-openapi.yaml rename to python/tests/assets/test_plugins/TestOpenAPIPlugin/akv-openapi.yaml diff --git a/python/tests/assets/test_plugins/TestPlugin/TestFunctionConfigOnly/config.json b/python/tests/assets/test_plugins/TestPlugin/TestFunctionConfigOnly/config.json new file mode 100644 index 0000000000..12cd2b235b --- /dev/null +++ b/python/tests/assets/test_plugins/TestPlugin/TestFunctionConfigOnly/config.json @@ -0,0 +1,13 @@ +{ + "schema": 1, + "description": "Test Description", + "execution_settings": { + "default": { + "max_tokens": 123, + "temperature": 0.0, + "top_p": 1.0, + "presence_penalty": 0.0, + "frequency_penalty": 2.0 + } + } +} \ No newline at end of file diff --git a/python/tests/assets/test_plugins/TestPlugin/TestFunctionPromptOnly/skprompt.txt b/python/tests/assets/test_plugins/TestPlugin/TestFunctionPromptOnly/skprompt.txt new file mode 100644 index 0000000000..b57378fade --- /dev/null +++ b/python/tests/assets/test_plugins/TestPlugin/TestFunctionPromptOnly/skprompt.txt @@ -0,0 +1,5 @@ +{{$input}} + +== +Test prompt. +== diff --git a/python/tests/conftest.py b/python/tests/conftest.py index cc5f01d963..8b3b777b05 100644 --- a/python/tests/conftest.py +++ b/python/tests/conftest.py @@ -115,6 +115,8 @@ def create_mock_function() -> Callable: mock_function.description = kernel_function_metadata.description mock_function.invoke.return_value = FunctionResult(function=mock_function.metadata, value=value, metadata={}) mock_function.invoke_stream = stream_func + mock_function.function_copy.return_value = mock_function + mock_function.__kernel_function__ = True return mock_function diff --git a/python/tests/integration/completions/conftest.py b/python/tests/integration/completions/conftest.py index 2c6e035273..129aeffbcd 100644 --- a/python/tests/integration/completions/conftest.py +++ b/python/tests/integration/completions/conftest.py @@ -100,7 +100,7 @@ def setup_gp_text_completion_function(kernel: Kernel, get_gp_config): prompt_template_config = PromptTemplateConfig(template=prompt, execution_settings=exec_settings) # Create the semantic function - text2text_function = kernel.create_function_from_prompt( + text2text_function = kernel.add_function( function_name="hello", plugin_name="plugin", prompt_template_config=prompt_template_config ) diff --git a/python/tests/integration/completions/test_azure_oai_chat_service.py b/python/tests/integration/completions/test_azure_oai_chat_service.py index 50ab8b7a04..906f9f31c1 100644 --- a/python/tests/integration/completions/test_azure_oai_chat_service.py +++ b/python/tests/integration/completions/test_azure_oai_chat_service.py @@ -7,6 +7,9 @@ from openai import AsyncAzureOpenAI from test_utils import retry import semantic_kernel.connectors.ai.open_ai as sk_oai +from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.azure_chat_prompt_execution_settings import ( + AzureChatPromptExecutionSettings, +) from semantic_kernel.connectors.ai.open_ai.utils import get_tool_call_object from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings from semantic_kernel.core_plugins.math_plugin import MathPlugin @@ -46,13 +49,11 @@ async def test_azure_e2e_chat_completion_with_plugin(setup_tldr_function_for_oai ) # Create the semantic function - tldr_function = kernel.create_function_from_prompt( - function_name="tldr", plugin_name="plugin", prompt_template_config=prompt_template_config - ) + kernel.add_function(function_name="tldr", plugin_name="plugin", prompt_template_config=prompt_template_config) arguments = KernelArguments(input=text_to_summarize) - summary = await retry(lambda: kernel.invoke(tldr_function, arguments)) + summary = await retry(lambda: kernel.invoke(function_name="tldr", plugin_name="plugin", arguments=arguments)) output = str(summary).strip() print(f"TLDR using input string: '{output}'") assert "First Law" not in output and ("human" in output or "Human" in output or "preserve" in output) @@ -102,13 +103,11 @@ async def test_azure_e2e_chat_completion_with_plugin_and_provided_client( ) # Create the semantic function - tldr_function = kernel.create_function_from_prompt( - function_name="tldr", plugin_name="plugin", prompt_template_config=prompt_template_config - ) + kernel.add_function(function_name="tldr", plugin_name="plugin", prompt_template_config=prompt_template_config) arguments = KernelArguments(input=text_to_summarize) - summary = await retry(lambda: kernel.invoke(tldr_function, arguments)) + summary = await retry(lambda: kernel.invoke(function_name="tldr", plugin_name="plugin", arguments=arguments)) output = str(summary).strip() print(f"TLDR using input string: '{output}'") assert "First Law" not in output and ("human" in output or "Human" in output or "preserve" in output) @@ -116,9 +115,7 @@ async def test_azure_e2e_chat_completion_with_plugin_and_provided_client( @pytest.mark.asyncio -async def test_azure_oai_chat_service_with_tool_call(setup_tldr_function_for_oai_models, get_aoai_config): - kernel, _, _ = setup_tldr_function_for_oai_models - +async def test_azure_oai_chat_service_with_tool_call(kernel: Kernel, get_aoai_config): _, api_key, endpoint = get_aoai_config if "Python_Integration_Tests" in os.environ: @@ -147,9 +144,9 @@ async def test_azure_oai_chat_service_with_tool_call(setup_tldr_function_for_oai ), ) - kernel.import_plugin_from_object(MathPlugin(), plugin_name="math") + kernel.add_plugin(MathPlugin(), plugin_name="math") - execution_settings = sk_oai.AzureChatPromptExecutionSettings( + execution_settings = AzureChatPromptExecutionSettings( service_id="chat_completion", max_tokens=2000, temperature=0.7, @@ -165,11 +162,13 @@ async def test_azure_oai_chat_service_with_tool_call(setup_tldr_function_for_oai ) # Create the prompt function - tldr_function = kernel.create_function_from_prompt( + kernel.add_function( function_name="math_fun", plugin_name="math_int_test", prompt_template_config=prompt_template_config ) - summary = await retry(lambda: kernel.invoke(tldr_function, input="what is 1+1?")) + summary = await retry( + lambda: kernel.invoke(function_name="math_fun", plugin_name="math_int_test", input="what is 1+1?") + ) output = str(summary).strip() print(f"Math output: '{output}'") assert "2" in output @@ -206,10 +205,10 @@ async def test_azure_oai_chat_service_with_tool_call_streaming(kernel: Kernel, g ), ) - kernel.import_plugin_from_object(MathPlugin(), plugin_name="Math") + kernel.add_plugin(MathPlugin(), plugin_name="Math") # Create the prompt function - chat_func = kernel.create_function_from_prompt(prompt="{{$input}}", function_name="chat", plugin_name="chat") + kernel.add_function(prompt="{{$input}}", function_name="chat", plugin_name="chat") execution_settings = sk_oai.AzureChatPromptExecutionSettings( service_id="chat_completion", max_tokens=2000, @@ -223,7 +222,7 @@ async def test_azure_oai_chat_service_with_tool_call_streaming(kernel: Kernel, g arguments = KernelArguments(input="what is 101+102?", settings=execution_settings) result = None - async for message in kernel.invoke_stream(chat_func, arguments=arguments): + async for message in kernel.invoke_stream(function_name="chat", plugin_name="chat", arguments=arguments): result = message[0] if not result else result + message[0] output = str(result) diff --git a/python/tests/integration/completions/test_azure_oai_chat_service_extensions.py b/python/tests/integration/completions/test_azure_oai_chat_service_extensions.py index 873a8beebd..a34d2b4ce6 100644 --- a/python/tests/integration/completions/test_azure_oai_chat_service_extensions.py +++ b/python/tests/integration/completions/test_azure_oai_chat_service_extensions.py @@ -130,10 +130,8 @@ async def create_with_data_chat_function(get_aoai_config, kernel: Kernel, create ) # Create the semantic function - chat_function = kernel.create_function_from_prompt( - function_name="story", plugin_name="plugin", prompt_template_config=prompt_template_config - ) - + kernel.add_function(function_name="story", plugin_name="plugin", prompt_template_config=prompt_template_config) + chat_function = kernel.get_function("plugin", "story") return chat_function, kernel, collection, memory_store except: await memory_store.delete_collection(collection) diff --git a/python/tests/integration/completions/test_azure_oai_text_service.py b/python/tests/integration/completions/test_azure_oai_text_service.py index 74c4099c06..159d464074 100644 --- a/python/tests/integration/completions/test_azure_oai_text_service.py +++ b/python/tests/integration/completions/test_azure_oai_text_service.py @@ -46,7 +46,7 @@ async def test_azure_e2e_text_completion_with_plugin(setup_tldr_function_for_oai ) # Create the semantic function - tldr_function = kernel.create_function_from_prompt( + tldr_function = kernel.add_function( function_name="story", plugin_name="plugin", prompt_template_config=prompt_template_config ) @@ -103,7 +103,7 @@ async def test_azure_e2e_text_completion_with_plugin_with_provided_client( ) # Create the semantic function - tldr_function = kernel.create_function_from_prompt( + tldr_function = kernel.add_function( function_name="tldr", plugin_name="plugin", prompt_template_config=prompt_template_config ) diff --git a/python/tests/integration/completions/test_conversation_summary_plugin.py b/python/tests/integration/completions/test_conversation_summary_plugin.py index 23cfb1cb3c..fb1b432ee0 100644 --- a/python/tests/integration/completions/test_conversation_summary_plugin.py +++ b/python/tests/integration/completions/test_conversation_summary_plugin.py @@ -45,7 +45,7 @@ async def test_azure_summarize_conversation_using_plugin(setup_summarize_convers ), ) - conversationSummaryPlugin = kernel.import_plugin_from_object( + conversationSummaryPlugin = kernel.add_plugin( ConversationSummaryPlugin(kernel, prompt_template_config), "conversationSummary" ) @@ -92,7 +92,7 @@ async def test_oai_summarize_conversation_using_plugin( ), ) - conversationSummaryPlugin = kernel.import_plugin_from_object( + conversationSummaryPlugin = kernel.add_plugin( ConversationSummaryPlugin(kernel, prompt_template_config), "conversationSummary" ) diff --git a/python/tests/integration/completions/test_gp_chat_service.py b/python/tests/integration/completions/test_gp_chat_service.py index ec55f8b6a7..061897f274 100644 --- a/python/tests/integration/completions/test_gp_chat_service.py +++ b/python/tests/integration/completions/test_gp_chat_service.py @@ -40,7 +40,7 @@ async def test_gp_chat_service_with_plugins(setup_tldr_function_for_oai_models, prompt_template_config = PromptTemplateConfig(template=prompt, execution_settings=exec_settings) # Create the semantic function - tldr_function = kernel.create_function_from_prompt( + tldr_function = kernel.add_function( function_name="tldr", plugin_name="plugin", prompt_template_config=prompt_template_config ) diff --git a/python/tests/integration/completions/test_oai_chat_service.py b/python/tests/integration/completions/test_oai_chat_service.py index ecff676981..43273535fb 100644 --- a/python/tests/integration/completions/test_oai_chat_service.py +++ b/python/tests/integration/completions/test_oai_chat_service.py @@ -6,9 +6,7 @@ from openai import AsyncOpenAI from test_utils import retry import semantic_kernel.connectors.ai.open_ai as sk_oai -from semantic_kernel.connectors.ai.open_ai.utils import ( - get_tool_call_object, -) +from semantic_kernel.connectors.ai.open_ai.utils import get_tool_call_object from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings from semantic_kernel.contents.chat_history import ChatHistory from semantic_kernel.core_plugins.math_plugin import MathPlugin @@ -38,7 +36,7 @@ async def test_oai_chat_service_with_plugins(setup_tldr_function_for_oai_models, ) # Create the semantic function - tldr_function = kernel.create_function_from_prompt( + tldr_function = kernel.add_function( function_name="story", plugin_name="plugin", prompt_template_config=prompt_template_config ) @@ -65,7 +63,7 @@ async def test_oai_chat_service_with_tool_call(setup_tldr_function_for_oai_model ), ) - kernel.import_plugin_from_object(MathPlugin(), plugin_name="math") + kernel.add_plugin(MathPlugin(), plugin_name="math") execution_settings = sk_oai.OpenAIChatPromptExecutionSettings( service_id="chat-gpt", @@ -83,7 +81,7 @@ async def test_oai_chat_service_with_tool_call(setup_tldr_function_for_oai_model ) # Create the prompt function - tldr_function = kernel.create_function_from_prompt( + tldr_function = kernel.add_function( function_name="math_fun", plugin_name="math_int_test", prompt_template_config=prompt_template_config ) @@ -110,7 +108,7 @@ async def test_oai_chat_service_with_tool_call_streaming(setup_tldr_function_for ), ) - kernel.import_plugin_from_object(MathPlugin(), plugin_name="math") + kernel.add_plugin(MathPlugin(), plugin_name="math") execution_settings = sk_oai.OpenAIChatPromptExecutionSettings( service_id="chat-gpt", @@ -128,7 +126,7 @@ async def test_oai_chat_service_with_tool_call_streaming(setup_tldr_function_for ) # Create the prompt function - tldr_function = kernel.create_function_from_prompt( + tldr_function = kernel.add_function( function_name="math_fun", plugin_name="math_int_test", prompt_template_config=prompt_template_config ) @@ -175,7 +173,7 @@ async def test_oai_chat_service_with_plugins_with_provided_client(setup_tldr_fun ) # Create the semantic function - tldr_function = kernel.create_function_from_prompt( + tldr_function = kernel.add_function( function_name="story", plugin_name="story_plugin", prompt_template_config=prompt_template_config, @@ -220,7 +218,7 @@ async def test_oai_chat_stream_service_with_plugins(setup_tldr_function_for_oai_ ) # Create the prompt function - tldr_function = kernel.create_function_from_prompt( + tldr_function = kernel.add_function( function_name="story", plugin_name="story_plugin", prompt_template_config=prompt_template_config, @@ -260,9 +258,9 @@ async def test_oai_chat_service_with_yaml_jinja2(setup_tldr_function_for_oai_mod overwrite=True, # Overwrite the service if it already exists since add service says it does ) - plugins_directory = os.path.join(os.path.dirname(__file__), "../../assets/test_plugins", "TestPlugin") + plugins_directory = os.path.join(os.path.dirname(__file__), "../../assets/test_plugins") - plugin = kernel.import_plugin_from_prompt_directory(plugins_directory, "TestFunctionYamlJinja2") + plugin = kernel.add_plugin(parent_directory=plugins_directory, plugin_name="TestFunctionYamlJinja2") assert plugin is not None assert plugin["TestFunctionJinja2"] is not None @@ -299,9 +297,9 @@ async def test_oai_chat_service_with_yaml_handlebars(setup_tldr_function_for_oai overwrite=True, # Overwrite the service if it already exists since add service says it does ) - plugins_directory = os.path.join(os.path.dirname(__file__), "../../assets/test_plugins", "TestPlugin") + plugins_directory = os.path.join(os.path.dirname(__file__), "../../assets/test_plugins") - plugin = kernel.import_plugin_from_prompt_directory(plugins_directory, "TestFunctionYamlHandlebars") + plugin = kernel.add_plugin(parent_directory=plugins_directory, plugin_name="TestFunctionYamlHandlebars") assert plugin is not None assert plugin["TestFunctionHandlebars"] is not None diff --git a/python/tests/integration/completions/test_oai_text_service.py b/python/tests/integration/completions/test_oai_text_service.py index 607802f1cb..8de1fad490 100644 --- a/python/tests/integration/completions/test_oai_text_service.py +++ b/python/tests/integration/completions/test_oai_text_service.py @@ -36,7 +36,7 @@ async def test_oai_text_completion_with_plugins(setup_tldr_function_for_oai_mode ) # Create the semantic function - tldr_function = kernel.create_function_from_prompt( + tldr_function = kernel.add_function( function_name="story", plugin_name="plugin", prompt_template_config=prompt_template_config ) @@ -84,7 +84,7 @@ async def test_oai_text_completion_with_plugins_with_provided_client( ) # Create the semantic function - tldr_function = kernel.create_function_from_prompt( + tldr_function = kernel.add_function( function_name="story", plugin_name="plugin", prompt_template_config=prompt_template_config, @@ -134,7 +134,7 @@ async def test_oai_text_stream_completion_with_plugins(setup_tldr_function_for_o ) # Create the semantic function - tldr_function = kernel.create_function_from_prompt( + tldr_function = kernel.add_function( function_name="story", plugin_name="plugin", prompt_template_config=prompt_template_config, diff --git a/python/tests/integration/embeddings/test_azure_oai_embedding_service.py b/python/tests/integration/embeddings/test_azure_oai_embedding_service.py index 29eeeb7217..49de10ae55 100644 --- a/python/tests/integration/embeddings/test_azure_oai_embedding_service.py +++ b/python/tests/integration/embeddings/test_azure_oai_embedding_service.py @@ -30,7 +30,7 @@ async def test_azure_text_embedding_service(kernel: Kernel, get_aoai_config): kernel.add_service(embeddings_gen) memory = SemanticTextMemory(storage=sk.memory.VolatileMemoryStore(), embeddings_generator=embeddings_gen) - kernel.import_plugin_from_object(sk.core_plugins.TextMemoryPlugin(memory), "TextMemoryPlugin") + kernel.add_plugin(sk.core_plugins.TextMemoryPlugin(memory), "TextMemoryPlugin") await memory.save_information(collection="generic", id="info1", text="My budget for 2024 is $100,000") await memory.save_reference( @@ -66,7 +66,7 @@ async def test_azure_text_embedding_service_with_provided_client(kernel: Kernel, kernel.add_service(embeddings_gen) memory = SemanticTextMemory(storage=sk.memory.VolatileMemoryStore(), embeddings_generator=embeddings_gen) - kernel.import_plugin_from_object(sk.core_plugins.TextMemoryPlugin(memory), "TextMemoryPlugin") + kernel.add_plugin(sk.core_plugins.TextMemoryPlugin(memory), "TextMemoryPlugin") await memory.save_information(collection="generic", id="info1", text="My budget for 2024 is $100,000") await memory.save_reference( diff --git a/python/tests/integration/embeddings/test_gp_embedding_service.py b/python/tests/integration/embeddings/test_gp_embedding_service.py index e64658c4ce..fcc944b239 100644 --- a/python/tests/integration/embeddings/test_gp_embedding_service.py +++ b/python/tests/integration/embeddings/test_gp_embedding_service.py @@ -29,7 +29,7 @@ async def test_gp_embedding_service(kernel: Kernel, get_gp_config): kernel.add_service(palm_text_embed) memory = SemanticTextMemory(storage=sk.memory.VolatileMemoryStore(), embeddings_generator=palm_text_embed) - kernel.import_plugin_from_object(sk.core_plugins.TextMemoryPlugin(memory), "TextMemoryPlugin") + kernel.add_plugin(sk.core_plugins.TextMemoryPlugin(memory), "TextMemoryPlugin") await memory.save_information(collection="generic", id="info1", text="My budget for 2024 is $100,000") await memory.save_reference( diff --git a/python/tests/integration/embeddings/test_hf_embedding_service.py b/python/tests/integration/embeddings/test_hf_embedding_service.py index cdc058b80b..388f7bdf71 100644 --- a/python/tests/integration/embeddings/test_hf_embedding_service.py +++ b/python/tests/integration/embeddings/test_hf_embedding_service.py @@ -19,7 +19,7 @@ async def test_hf_embeddings_with_memories(): kernel.add_service(embedding_gen) memory = SemanticTextMemory(storage=sk.memory.VolatileMemoryStore(), embeddings_generator=embedding_gen) - kernel.import_plugin_from_object(sk.core_plugins.TextMemoryPlugin(memory), "TextMemoryPlugin") + kernel.add_plugin(sk.core_plugins.TextMemoryPlugin(memory), "TextMemoryPlugin") await memory.save_reference( "test", diff --git a/python/tests/integration/embeddings/test_oai_embedding_service.py b/python/tests/integration/embeddings/test_oai_embedding_service.py index 954d2d867a..58542e3333 100644 --- a/python/tests/integration/embeddings/test_oai_embedding_service.py +++ b/python/tests/integration/embeddings/test_oai_embedding_service.py @@ -20,7 +20,7 @@ async def test_oai_embedding_service(kernel: Kernel, get_oai_config): kernel.add_service(embedding_gen) memory = SemanticTextMemory(storage=sk.memory.VolatileMemoryStore(), embeddings_generator=embedding_gen) - kernel.import_plugin_from_object(sk.core_plugins.TextMemoryPlugin(memory), "TextMemoryPlugin") + kernel.add_plugin(sk.core_plugins.TextMemoryPlugin(memory), "TextMemoryPlugin") await memory.save_reference( "test", @@ -45,7 +45,7 @@ async def test_oai_embedding_service_with_provided_client(kernel: Kernel, get_oa kernel.add_service(embedding_gen) memory = SemanticTextMemory(storage=sk.memory.VolatileMemoryStore(), embeddings_generator=embedding_gen) - kernel.import_plugin_from_object(sk.core_plugins.TextMemoryPlugin(memory), "TextMemoryPlugin") + kernel.add_plugin(sk.core_plugins.TextMemoryPlugin(memory), "TextMemoryPlugin") await memory.save_reference( "test", diff --git a/python/tests/integration/planning/function_calling_stepwise_planner/test_int_function_calling_stepwise_planner.py b/python/tests/integration/planning/function_calling_stepwise_planner/test_int_function_calling_stepwise_planner.py index 2e947b5ac0..8cee73be73 100644 --- a/python/tests/integration/planning/function_calling_stepwise_planner/test_int_function_calling_stepwise_planner.py +++ b/python/tests/integration/planning/function_calling_stepwise_planner/test_int_function_calling_stepwise_planner.py @@ -31,7 +31,7 @@ async def test_can_execute_function_calling_stepwise_plan(kernel: Kernel, get_oa ), ) - kernel.import_plugin_from_object(MathPlugin(), "MathPlugin") + kernel.add_plugin(MathPlugin(), "MathPlugin") questions = [ "What is the current hour number, plus 5?", diff --git a/python/tests/integration/planning/sequential_planner/test_sequential_plan_parser.py b/python/tests/integration/planning/sequential_planner/test_sequential_plan_parser.py index b09a7081f0..960630971f 100644 --- a/python/tests/integration/planning/sequential_planner/test_sequential_plan_parser.py +++ b/python/tests/integration/planning/sequential_planner/test_sequential_plan_parser.py @@ -24,9 +24,9 @@ async def test_can_call_to_plan_from_xml(get_aoai_config): api_key=api_key, ), ) - kernel.import_plugin_from_object(EmailPluginFake(), "email") - kernel.import_plugin_from_object(SummarizePluginFake(), "SummarizePlugin") - kernel.import_plugin_from_object(WriterPluginFake(), "WriterPlugin") + kernel.add_plugin(EmailPluginFake(), "email") + kernel.add_plugin(SummarizePluginFake(), "SummarizePlugin") + kernel.add_plugin(WriterPluginFake(), "WriterPlugin") plan_string = """ 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 e62d8e6f4f..b2f422365a 100644 --- a/python/tests/integration/planning/sequential_planner/test_sequential_planner.py +++ b/python/tests/integration/planning/sequential_planner/test_sequential_planner.py @@ -89,8 +89,8 @@ async def test_create_plan_function_flow(get_aoai_config, use_chat_model, prompt service_id = "chat_completion" if use_chat_model else "text_completion" kernel = initialize_kernel(get_aoai_config, False, use_chat_model) - kernel.import_plugin_from_object(EmailPluginFake(), "email_plugin_fake") - kernel.import_plugin_from_object(FunPluginFake(), "fun_plugin_fake") + kernel.add_plugin(EmailPluginFake(), "email_plugin_fake") + kernel.add_plugin(FunPluginFake(), "fun_plugin_fake") planner = SequentialPlanner(kernel, service_id=service_id) @@ -120,8 +120,8 @@ 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_from_object(EmailPluginFake(), "email_plugin_fake") - kernel.import_plugin_from_object(WriterPluginFake(), "WriterPlugin") + kernel.add_plugin(EmailPluginFake(), "email_plugin_fake") + kernel.add_plugin(WriterPluginFake(), "WriterPlugin") planner = SequentialPlanner(kernel, service_id="text_completion") @@ -155,9 +155,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_from_object(EmailPluginFake(), "email_plugin_fake") - kernel.import_plugin_from_object(FunPluginFake(), "fun_plugin_fake") - kernel.import_plugin_from_object(WriterPluginFake(), "writer_plugin_fake") + kernel.add_plugin(EmailPluginFake(), "email_plugin_fake") + kernel.add_plugin(FunPluginFake(), "fun_plugin_fake") + kernel.add_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 f94f7ab6a1..3e9711b2d6 100644 --- a/python/tests/integration/planning/stepwise_planner/test_stepwise_planner.py +++ b/python/tests/integration/planning/stepwise_planner/test_stepwise_planner.py @@ -116,8 +116,8 @@ async def test_can_create_stepwise_plan( kernel = initialize_kernel(get_aoai_config, use_embeddings, use_chat_model) bing_connector = BingConnector(api_key=get_bing_config) web_search_engine_plugin = TempWebSearchEnginePlugin(bing_connector) - kernel.import_plugin_from_object(web_search_engine_plugin, "WebSearch") - kernel.import_plugin_from_object(TimePlugin(), "time") + kernel.add_plugin(web_search_engine_plugin, "WebSearch") + kernel.add_plugin(TimePlugin(), "time") planner = StepwisePlanner(kernel, StepwisePlannerConfig(max_iterations=10, min_iteration_time_ms=1000)) @@ -152,9 +152,9 @@ async def test_can_execute_stepwise_plan( kernel = initialize_kernel(get_aoai_config, use_embeddings, use_chat_model) bing_connector = BingConnector(api_key=get_bing_config) web_search_engine_plugin = TempWebSearchEnginePlugin(bing_connector) - kernel.import_plugin_from_object(web_search_engine_plugin, "WebSearch") - kernel.import_plugin_from_object(TimePlugin(), "time") - kernel.import_plugin_from_object(MathPlugin(), "math") + kernel.add_plugin(web_search_engine_plugin, "WebSearch") + kernel.add_plugin(TimePlugin(), "time") + kernel.add_plugin(MathPlugin(), "math") planner = StepwisePlanner(kernel, StepwisePlannerConfig(max_iterations=10, min_iteration_time_ms=1000)) diff --git a/python/tests/unit/connectors/hugging_face/test_hf_local_text_completions.py b/python/tests/unit/connectors/hugging_face/test_hf_local_text_completions.py index 5ab629a439..25f7e21a67 100644 --- a/python/tests/unit/connectors/hugging_face/test_hf_local_text_completions.py +++ b/python/tests/unit/connectors/hugging_face/test_hf_local_text_completions.py @@ -52,16 +52,16 @@ async def test_text_completion(model_name, task, input_str): prompt_template_config = PromptTemplateConfig(template=prompt, execution_settings=exec_settings) - test_func = kernel.create_function_from_prompt( + kernel.add_function( prompt_template_config=prompt_template_config, function_name="TestFunction", plugin_name="TestPlugin", - execution_settings=exec_settings, + prompt_execution_settings=exec_settings, ) arguments = KernelArguments(input=input_str) - summary = await kernel.invoke(test_func, arguments) + summary = await kernel.invoke(function_name="TestFunction", plugin_name="TestPlugin", arguments=arguments) output = str(summary).strip() try: @@ -70,7 +70,7 @@ async def test_text_completion(model_name, task, input_str): pytest.xfail("The output is empty, but completed invoke") stream_summary = "" - async for text in kernel.invoke_stream(test_func, arguments): + async for text in kernel.invoke_stream(function_name="TestFunction", plugin_name="TestPlugin", arguments=arguments): stream_summary += str(text[0]) stream_output = str(stream_summary).strip() diff --git a/python/tests/unit/core_plugins/test_http_plugin.py b/python/tests/unit/core_plugins/test_http_plugin.py index d5eba175e7..3c13eb3800 100644 --- a/python/tests/unit/core_plugins/test_http_plugin.py +++ b/python/tests/unit/core_plugins/test_http_plugin.py @@ -20,7 +20,7 @@ async def test_it_can_be_instantiated(): async def test_it_can_be_imported(): kernel = Kernel() plugin = HttpPlugin() - assert kernel.import_plugin_from_object(plugin, "http") + kernel.add_plugin(plugin, "http") assert kernel.plugins["http"] is not None assert kernel.plugins["http"].name == "http" assert kernel.plugins["http"]["getAsync"] is not None diff --git a/python/tests/unit/core_plugins/test_math_plugin.py b/python/tests/unit/core_plugins/test_math_plugin.py index 1d6fa94636..28687d6da3 100644 --- a/python/tests/unit/core_plugins/test_math_plugin.py +++ b/python/tests/unit/core_plugins/test_math_plugin.py @@ -14,7 +14,7 @@ def test_can_be_instantiated(): def test_can_be_imported(): kernel = Kernel() - assert kernel.import_plugin_from_object(MathPlugin(), "math") + kernel.add_plugin(MathPlugin(), "math") assert kernel.plugins["math"] is not None assert kernel.plugins["math"].name == "math" assert kernel.plugins["math"]["Add"] is not None diff --git a/python/tests/unit/core_plugins/test_text_plugin.py b/python/tests/unit/core_plugins/test_text_plugin.py index 622172ec70..a76fdbbda6 100644 --- a/python/tests/unit/core_plugins/test_text_plugin.py +++ b/python/tests/unit/core_plugins/test_text_plugin.py @@ -8,13 +8,13 @@ def test_can_be_instantiated(): def test_can_be_imported(): kernel = sk.Kernel() - assert kernel.import_plugin_from_object(TextPlugin(), "text_plugin") + kernel.add_plugin(TextPlugin(), "text_plugin") assert not kernel.plugins["text_plugin"]["trim"].is_prompt def test_can_be_imported_with_name(): kernel = sk.Kernel() - assert kernel.import_plugin_from_object(TextPlugin(), "text") + kernel.add_plugin(TextPlugin(), "text") assert not kernel.plugins["text"]["trim"].is_prompt diff --git a/python/tests/unit/core_plugins/test_time_plugin.py b/python/tests/unit/core_plugins/test_time_plugin.py index 72a4ba4e60..a92713aad2 100644 --- a/python/tests/unit/core_plugins/test_time_plugin.py +++ b/python/tests/unit/core_plugins/test_time_plugin.py @@ -14,7 +14,7 @@ def test_can_be_instantiated(): def test_can_be_imported(): kernel = sk.Kernel() - assert kernel.import_plugin_from_object(TimePlugin(), "time") + kernel.add_plugin(TimePlugin(), "time") assert kernel.plugins["time"] is not None assert kernel.plugins["time"].name == "time" assert kernel.plugins["time"]["now"] is not None diff --git a/python/tests/unit/functions/test_kernel_function_from_prompt.py b/python/tests/unit/functions/test_kernel_function_from_prompt.py index 88c12adfac..48b4335c09 100644 --- a/python/tests/unit/functions/test_kernel_function_from_prompt.py +++ b/python/tests/unit/functions/test_kernel_function_from_prompt.py @@ -1,3 +1,4 @@ +import os from unittest.mock import patch import pytest @@ -287,3 +288,36 @@ def test_create_with_multiple_settings(): assert ( function.prompt_template.prompt_template_config.execution_settings["test2"].extension_data["temperature"] == 1.0 ) + + +def test_from_yaml_fail(): + with pytest.raises(FunctionInitializationError): + KernelFunctionFromPrompt.from_yaml("template_format: something_else") + + +def test_from_directory_prompt_only(): + with pytest.raises(FunctionInitializationError): + KernelFunctionFromPrompt.from_directory( + path=os.path.join( + os.path.dirname(__file__), + "../../assets", + "test_plugins", + "TestPlugin", + "TestFunctionPromptOnly", + ), + plugin_name="test", + ) + + +def test_from_directory_config_only(): + with pytest.raises(FunctionInitializationError): + KernelFunctionFromPrompt.from_directory( + path=os.path.join( + os.path.dirname(__file__), + "../../assets", + "test_plugins", + "TestPlugin", + "TestFunctionConfigOnly", + ), + plugin_name="test", + ) diff --git a/python/tests/unit/functions/test_kernel_plugin_collection.py b/python/tests/unit/functions/test_kernel_plugin_collection.py deleted file mode 100644 index 32ee5059a3..0000000000 --- a/python/tests/unit/functions/test_kernel_plugin_collection.py +++ /dev/null @@ -1,103 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -from string import ascii_uppercase - -import pytest - -from semantic_kernel.functions.kernel_plugin import KernelPlugin -from semantic_kernel.functions.kernel_plugin_collection import KernelPluginCollection - - -@pytest.fixture(scope="function") -def collection(): - return 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(collection: KernelPluginCollection): - expected_description = "Test Description" - 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(collection: KernelPluginCollection): - num_plugins = 3 - 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 - - -def test_overwrite_plugin_with_same_name_succeeds(): - plugin = KernelPlugin(name="TestPluginOne") - collection = KernelPluginCollection(plugins=[plugin]) - - plugin2 = KernelPlugin(name="TestPluginOne") - collection.add(plugin2) - - assert len(collection) == 1 diff --git a/python/tests/unit/functions/test_kernel_plugins.py b/python/tests/unit/functions/test_kernel_plugins.py index 0f957d1ac6..4ba7bfae11 100644 --- a/python/tests/unit/functions/test_kernel_plugins.py +++ b/python/tests/unit/functions/test_kernel_plugins.py @@ -1,10 +1,20 @@ # Copyright (c) Microsoft. All rights reserved. +from __future__ import annotations +import os +from typing import Any, Callable +from unittest.mock import AsyncMock, patch +import httpx import pytest +from pytest import raises from semantic_kernel.connectors.ai import PromptExecutionSettings -from semantic_kernel.exceptions.function_exceptions import FunctionInvalidNameError +from semantic_kernel.connectors.openai_plugin.openai_function_execution_parameters import ( + OpenAIFunctionExecutionParameters, +) +from semantic_kernel.exceptions.function_exceptions import PluginInitializationError +from semantic_kernel.functions import kernel_function from semantic_kernel.functions.kernel_function import KernelFunction from semantic_kernel.functions.kernel_function_from_method import KernelFunctionFromMethod from semantic_kernel.functions.kernel_function_from_prompt import KernelFunctionFromPrompt @@ -13,45 +23,53 @@ from semantic_kernel.prompt_template.input_variable import InputVariable from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig -def test_throws_for_missing_name(): - with pytest.raises(TypeError): +@pytest.fixture +def mock_function() -> Callable[..., Any]: + @kernel_function + def mock_function(input: str) -> None: + pass + + return mock_function + + +# region Init + + +def test_init_fail_no_name(): + with raises(TypeError): KernelPlugin(description="A unit test plugin") -def test_default_kernel_plugin_construction_with_no_functions(): +def test_init_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 + assert not plugin.functions -def test_default_kernel_plugin_construction_with_native_functions(): +def test_init_with_kernel_functions(mock_function): + function_plugin_name = "MockPlugin" expected_plugin_name = "test_plugin" expected_plugin_description = "A unit test plugin" - def mock_function(input: str) -> None: - pass + native_function = KernelFunction.from_method(method=mock_function, plugin_name=function_plugin_name) - 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_parameters__ = [ - { - "name": "input", - "description": "Param 1 description", - "default_value": "default_param1_value", - } - ] - mock_function.__kernel_function_return_description__ = "" - mock_function.__kernel_function_return_required__ = True - mock_function.__kernel_function_return_type__ = "None" + 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"].plugin_name == expected_plugin_name + assert native_function.plugin_name == function_plugin_name - mock_method = mock_function - native_function = KernelFunction.from_method(method=mock_method, plugin_name="MockPlugin") +def test_init_with_kernel_functions_list(mock_function): + function_plugin_name = "MockPlugin" + expected_plugin_name = "test_plugin" + expected_plugin_description = "A unit test plugin" + + native_function = KernelFunction.from_method(method=mock_function, plugin_name=function_plugin_name) plugin = KernelPlugin( name=expected_plugin_name, description=expected_plugin_description, functions=[native_function] @@ -59,35 +77,100 @@ def test_default_kernel_plugin_construction_with_native_functions(): assert plugin.name == expected_plugin_name assert plugin.description == expected_plugin_description assert len(plugin.functions) == 1 - assert plugin["mock_function"] == native_function + assert plugin["mock_function"].plugin_name == expected_plugin_name + assert native_function.plugin_name == function_plugin_name -def test_default_kernel_plugin_exposes_the_native_function_it_contains(): +def test_init_with_list_other_fail(): + with raises(ValueError): + KernelPlugin(name="test_plugin", description="A unit test plugin", functions=["str"]) + + +def test_init_with_other_fail(): + with raises(ValueError): + KernelPlugin(name="test_plugin", description="A unit test plugin", functions="str") + + +def test_init_with_kernel_functions_dict(mock_function): + function_plugin_name = "MockPlugin" expected_plugin_name = "test_plugin" expected_plugin_description = "A unit test plugin" - def mock_function(input: str) -> None: - pass + native_function = KernelFunction.from_method(method=mock_function, plugin_name=function_plugin_name) - 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_parameters__ = [ - { - "name": "param1", - "description": "Param 1 description", - "default_value": "default_param1_value", - } - ] - mock_function.__kernel_function_return_description__ = "" - mock_function.__kernel_function_return_required__ = True - mock_function.__kernel_function_return_type__ = "None" + plugin = KernelPlugin( + name=expected_plugin_name, + description=expected_plugin_description, + functions={native_function.name: native_function}, + ) + assert plugin.name == expected_plugin_name + assert plugin.description == expected_plugin_description + assert len(plugin.functions) == 1 + assert plugin["mock_function"].plugin_name == expected_plugin_name + assert native_function.plugin_name == function_plugin_name - mock_method = mock_function - native_function = KernelFunction.from_method(method=mock_method, plugin_name="MockPlugin") +def test_init_with_callable_functions(mock_function): + expected_plugin_name = "test_plugin" + expected_plugin_description = "A unit test plugin" + + plugin = KernelPlugin(name=expected_plugin_name, description=expected_plugin_description, functions=mock_function) + assert plugin.name == expected_plugin_name + assert plugin.description == expected_plugin_description + assert len(plugin.functions) == 1 + assert plugin["mock_function"].plugin_name == expected_plugin_name + + +def test_init_with_callable_functions_list(mock_function): + expected_plugin_name = "test_plugin" + expected_plugin_description = "A unit test plugin" + + plugin = KernelPlugin(name=expected_plugin_name, description=expected_plugin_description, functions=[mock_function]) + assert plugin.name == expected_plugin_name + assert plugin.description == expected_plugin_description + assert len(plugin.functions) == 1 + assert plugin["mock_function"].plugin_name == expected_plugin_name + + +def test_init_with_kernel_plugin(mock_function): + function_plugin_name = "MockPlugin" + expected_plugin_name = "test_plugin" + expected_plugin_description = "A unit test plugin" + + native_function = KernelFunction.from_method(method=mock_function, plugin_name=function_plugin_name) + first_plugin = KernelPlugin( + name=expected_plugin_name, description=expected_plugin_description, functions=native_function + ) + plugin = KernelPlugin(name=expected_plugin_name, description=expected_plugin_description, functions=first_plugin) + assert plugin.name == expected_plugin_name + assert plugin.description == expected_plugin_description + assert len(plugin.functions) == 1 + assert plugin["mock_function"].plugin_name == expected_plugin_name + assert native_function.plugin_name == function_plugin_name + + +def test_init_with_kernel_plugin_list(mock_function): + function_plugin_name = "MockPlugin" + expected_plugin_name = "test_plugin" + expected_plugin_description = "A unit test plugin" + + native_function = KernelFunction.from_method(method=mock_function, plugin_name=function_plugin_name) + first_plugin = KernelPlugin( + name=expected_plugin_name, description=expected_plugin_description, functions=native_function + ) + plugin = KernelPlugin(name=expected_plugin_name, description=expected_plugin_description, functions=[first_plugin]) + assert plugin.name == expected_plugin_name + assert plugin.description == expected_plugin_description + assert len(plugin.functions) == 1 + assert plugin["mock_function"].plugin_name == expected_plugin_name + assert native_function.plugin_name == function_plugin_name + + +def test_init_exposes_the_native_function_it_contains(mock_function): + expected_plugin_name = "test_plugin" + expected_plugin_description = "A unit test plugin" + + native_function = KernelFunction.from_method(method=mock_function, plugin_name="MockPlugin") plugin = KernelPlugin( name=expected_plugin_name, description=expected_plugin_description, functions=[native_function] @@ -95,14 +178,10 @@ def test_default_kernel_plugin_exposes_the_native_function_it_contains(): 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 + assert plugin["mock_function"].name == native_function.name -def test_default_kernel_plugin_construction_with_prompt_function(): +def test_init_with_prompt_function(): req_settings = PromptExecutionSettings(extension_data={"max_tokens": 2000, "temperature": 0.7, "top_p": 0.8}) prompt = "Use this input: {{$request}}" @@ -138,7 +217,7 @@ def test_default_kernel_plugin_construction_with_prompt_function(): assert plugin["mock_function"] == semantic_function -def test_default_kernel_plugin_construction_with_both_function_types(): +def test_init_with_both_function_types(mock_function): req_settings = PromptExecutionSettings(extension_data={"max_tokens": 2000, "temperature": 0.7, "top_p": 0.8}) prompt = "Use this input: {{$request}}" @@ -154,7 +233,7 @@ def test_default_kernel_plugin_construction_with_both_function_types(): ) expected_plugin_name = "test_plugin" - expected_function_name = "mock_function" + expected_function_name = "prompt_function" semantic_function = KernelFunction.from_prompt( prompt=prompt, prompt_template_config=prompt_template_config, @@ -162,29 +241,7 @@ def test_default_kernel_plugin_construction_with_both_function_types(): function_name=expected_function_name, ) - # Construct a nativate function - def mock_function(input: str) -> 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_parameters__ = [ - { - "name": "param1", - "description": "Param 1 description", - "default_value": "default_param1_value", - } - ] - mock_function.__kernel_function_return_description__ = "" - mock_function.__kernel_function_return_required__ = True - mock_function.__kernel_function_return_type__ = "None" - - mock_method = mock_function - - native_function = KernelFunctionFromMethod(method=mock_method, plugin_name="MockPlugin") + native_function = KernelFunctionFromMethod(method=mock_function, plugin_name="MockPlugin") # Add both types to the default kernel plugin expected_plugin_description = "A unit test plugin" @@ -201,10 +258,10 @@ def test_default_kernel_plugin_construction_with_both_function_types(): for func in [semantic_function, native_function]: assert func.name in plugin - assert plugin[func.name] == func + assert plugin[func.name].name == func.name -def test_default_kernel_plugin_construction_with_same_function_names_throws(): +def test_init_with_same_function_names(mock_function): req_settings = PromptExecutionSettings(extension_data={"max_tokens": 2000, "temperature": 0.7, "top_p": 0.8}) prompt = "Use this input: {{$request}}" @@ -228,28 +285,318 @@ def test_default_kernel_plugin_construction_with_same_function_names_throws(): function_name=expected_function_name, ) - # Construct a nativate function - def mock_function(input: str) -> None: - pass + native_function = KernelFunctionFromMethod(method=mock_function, plugin_name="MockPlugin") - 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_parameters__ = [ - { - "name": "param1", - "description": "Param 1 description", - "default_value": "default_param1_value", - } - ] - mock_function.__kernel_function_return_description__ = "" - mock_function.__kernel_function_return_required__ = True - mock_function.__kernel_function_return_type__ = "None" + plugin = KernelPlugin(name=expected_plugin_name, functions=[semantic_function, native_function]) + assert len(plugin.functions) == 1 - mock_method = mock_function - native_function = KernelFunctionFromMethod(method=mock_method, plugin_name="MockPlugin") - with pytest.raises(FunctionInvalidNameError): - KernelPlugin(name=expected_plugin_name, functions=[semantic_function, native_function]) +# region Dict-like methods + + +def test_set_item(mock_function): + expected_plugin_name = "test_plugin" + expected_plugin_description = "A unit test plugin" + + native_function = KernelFunction.from_method(method=mock_function, plugin_name=expected_plugin_name) + + plugin = KernelPlugin(name=expected_plugin_name, description=expected_plugin_description) + plugin["mock_function"] = native_function + + assert plugin.name == expected_plugin_name + assert plugin.description == expected_plugin_description + assert len(plugin.functions) == 1 + assert plugin["mock_function"].metadata == native_function.metadata + + function = plugin["mock_function"] + assert function.name == "mock_function" + + +def test_set(mock_function): + expected_plugin_name = "test_plugin" + expected_plugin_description = "A unit test plugin" + + native_function = KernelFunction.from_method(method=mock_function, plugin_name=expected_plugin_name) + + plugin = KernelPlugin(name=expected_plugin_name, description=expected_plugin_description) + plugin.set("mock_function", native_function) + + assert plugin.name == expected_plugin_name + assert plugin.description == expected_plugin_description + assert len(plugin.functions) == 1 + assert plugin["mock_function"].metadata == native_function.metadata + + function = plugin.get("mock_function", None) + assert function.name == "mock_function" + function2 = plugin.get("mock_function2", None) + assert function2 is None + + +def test_set_default(mock_function): + expected_plugin_name = "test_plugin" + expected_plugin_description = "A unit test plugin" + + native_function = KernelFunction.from_method(method=mock_function, plugin_name=expected_plugin_name) + native_function2 = KernelFunction.from_method(method=mock_function, plugin_name="other") + + plugin = KernelPlugin(name=expected_plugin_name, description=expected_plugin_description) + native_function == plugin.setdefault("mock_function", native_function) + native_function == plugin.setdefault("mock_function", native_function2) + + assert len(plugin.functions) == 1 + + with raises(ValueError): + plugin.setdefault("mock_function2", None) + + +def test_update(mock_function): + expected_plugin_name = "test_plugin" + expected_plugin_description = "A unit test plugin" + + native_function = KernelFunction.from_method(method=mock_function, plugin_name=expected_plugin_name) + native_function2 = KernelFunction.from_method(method=mock_function, plugin_name="p2") + native_function3 = KernelFunction.from_method(method=mock_function, plugin_name="p3") + + plugin = KernelPlugin(name=expected_plugin_name, description=expected_plugin_description) + plugin.update(mock_function=native_function) + assert len(plugin.functions) == 1 + + plugin.update([native_function2]) + assert len(plugin.functions) == 1 + + plugin.update({"mock_function": native_function3}) + assert len(plugin.functions) == 1 + + plugin2 = KernelPlugin(name="p2", description="p2") + plugin2.update(plugin) + assert len(plugin2.functions) == 1 + + plugin2.update([plugin]) + assert len(plugin2.functions) == 1 + + with raises(TypeError): + plugin.update(1) + + with raises(TypeError): + plugin.update(1, 2) + + +def test_iter(): + expected_plugin_name = "test_plugin" + expected_plugin_description = "A unit test plugin" + + func1 = KernelFunctionFromPrompt("test1", expected_plugin_name, None, "test prompt") + func2 = KernelFunctionFromPrompt("test1", expected_plugin_name, None, "test prompt") + func3 = KernelFunctionFromPrompt("test1", expected_plugin_name, None, "test prompt") + + plugin = KernelPlugin( + name=expected_plugin_name, description=expected_plugin_description, functions=[func1, func2, func3] + ) + + for func in plugin: + assert func.metadata in [func1.metadata, func2.metadata, func3.metadata] + + +# region Properties + + +def test_get_functions_metadata(mock_function): + expected_plugin_name = "test_plugin" + expected_plugin_description = "A unit test plugin" + + native_function = KernelFunction.from_method(method=mock_function, plugin_name=expected_plugin_name) + + plugin = KernelPlugin(name=expected_plugin_name, description=expected_plugin_description, functions=native_function) + metadatas = plugin.get_functions_metadata() + assert len(metadatas) == 1 + assert metadatas[0] == native_function.metadata + + +# region Class Methods + + +def test_from_directory(): + plugins_directory = os.path.join(os.path.dirname(__file__), "../../assets", "test_plugins") + plugin = KernelPlugin.from_directory("TestMixedPlugin", plugins_directory) + assert plugin is not None + assert len(plugin.functions) == 3 + assert plugin.name == "TestMixedPlugin" + assert plugin.get("TestFunctionYaml") is not None + assert plugin.get("echoAsync") is not None + assert plugin.get("TestFunction") is not None + + +def test_from_directory_parent_directory_does_not_exist(): + # import plugins + plugins_directory = os.path.join(os.path.dirname(__file__), "../../assets", "test_plugins_fail") + # path to plugins directory + with raises(PluginInitializationError, match="Plugin directory does not exist"): + KernelPlugin.from_directory("TestPlugin", plugins_directory) + + +def test_from_python_fail(): + with raises(PluginInitializationError, match="No class found in file"): + KernelPlugin.from_python_file( + "TestNativePluginNoClass", + os.path.join( + os.path.dirname(__file__), + "../../assets", + "test_native_plugins", + "TestNativePluginNoClass", + "native_function.py", + ), + ) + + +def test_from_python_in_directory_fail(): + plugins_directory = os.path.join(os.path.dirname(__file__), "../../assets", "test_native_plugins") + # path to plugins directory + with raises(PluginInitializationError, match="No functions found in folder"): + KernelPlugin.from_directory("TestNativePluginNoClass", plugins_directory) + + +def test_from_yaml_in_directory_fail(): + plugins_directory = os.path.join(os.path.dirname(__file__), "../../assets", "test_plugins") + # path to plugins directory + with raises(PluginInitializationError, match="No functions found in folder"): + KernelPlugin.from_directory("TestFunctionBadYaml", plugins_directory) + + +def test_from_directory_other(): + plugins_directory = os.path.join(os.path.dirname(__file__), "../../assets", "test_plugins") + # path to plugins directory + with raises(PluginInitializationError, match="No functions found in folder"): + KernelPlugin.from_directory("TestNoFunction", plugins_directory) + + +def test_from_directory_with_args(): + plugins_directory = os.path.join(os.path.dirname(__file__), "../../assets", "test_native_plugins") + # path to plugins directory + plugin = KernelPlugin.from_directory( + "TestNativePluginArgs", + plugins_directory, + class_init_arguments={"TestNativeEchoBotPlugin": {"static_input": "prefix "}}, + ) + result = plugin["echo"].method(text="test") + assert result == "prefix test" + + +def test_from_object_function(decorated_native_function): + plugin = KernelPlugin.from_object("TestPlugin", {"getLightStatusFunc": decorated_native_function}) + assert plugin is not None + assert len(plugin.functions) == 1 + assert plugin.functions.get("getLightStatus") is not None + + +def test_from_object_class(custom_plugin_class): + plugin = KernelPlugin.from_object("TestPlugin", custom_plugin_class()) + assert plugin is not None + assert len(plugin.functions) == 1 + assert plugin.functions.get("getLightStatus") is not None + + +@pytest.mark.asyncio +@patch("semantic_kernel.connectors.openai_plugin.openai_utils.OpenAIUtils.parse_openai_manifest_for_openapi_spec_url") +async def test_from_openai_from_file(mock_parse_openai_manifest): + openai_spec_file = os.path.join(os.path.dirname(__file__), "../../assets/test_plugins") + with open(os.path.join(openai_spec_file, "TestOpenAIPlugin", "akv-openai.json"), "r") as file: + openai_spec = file.read() + + openapi_spec_file_path = os.path.join( + os.path.dirname(__file__), "../../assets/test_plugins", "TestOpenAPIPlugin", "akv-openapi.yaml" + ) + mock_parse_openai_manifest.return_value = openapi_spec_file_path + + plugin = await KernelPlugin.from_openai( + plugin_name="TestOpenAIPlugin", + plugin_str=openai_spec, + execution_parameters=OpenAIFunctionExecutionParameters( + http_client=AsyncMock(), + auth_callback=AsyncMock(), + server_url_override="http://localhost", + enable_dynamic_payload=True, + ), + ) + assert plugin is not None + assert plugin.name == "TestOpenAIPlugin" + assert plugin.functions.get("GetSecret") is not None + assert plugin.functions.get("SetSecret") is not None + + +@pytest.mark.asyncio +@patch("httpx.AsyncClient.get") +@patch("semantic_kernel.connectors.openai_plugin.openai_utils.OpenAIUtils.parse_openai_manifest_for_openapi_spec_url") +async def test_from_openai_plugin_from_url(mock_parse_openai_manifest, mock_get): + openai_spec_file_path = os.path.join( + os.path.dirname(__file__), "../../assets/test_plugins", "TestOpenAIPlugin", "akv-openai.json" + ) + with open(openai_spec_file_path, "r") as file: + openai_spec = file.read() + + openapi_spec_file_path = os.path.join( + os.path.dirname(__file__), "../../assets/test_plugins", "TestOpenAPIPlugin", "akv-openapi.yaml" + ) + mock_parse_openai_manifest.return_value = openapi_spec_file_path + + request = httpx.Request(method="GET", url="http://fake-url.com/akv-openai.json") + + response = httpx.Response(200, text=openai_spec, request=request) + mock_get.return_value = response + + fake_plugin_url = "http://fake-url.com/akv-openai.json" + plugin = await KernelPlugin.from_openai( + plugin_name="TestOpenAIPlugin", + plugin_url=fake_plugin_url, + execution_parameters=OpenAIFunctionExecutionParameters( + auth_callback=AsyncMock(), + server_url_override="http://localhost", + enable_dynamic_payload=True, + ), + ) + assert plugin is not None + assert plugin.name == "TestOpenAIPlugin" + assert plugin.functions.get("GetSecret") is not None + assert plugin.functions.get("SetSecret") is not None + + mock_get.assert_awaited_once_with(fake_plugin_url, headers={"User-Agent": "Semantic-Kernel"}) + + +@pytest.mark.asyncio +async def test_from_openai_fail(): + with raises(PluginInitializationError): + await KernelPlugin.from_openai(plugin_name="TestOpenAIPlugin") + + +@pytest.mark.asyncio +async def test_from_openai_fail_json_parsing(): + with raises(PluginInitializationError): + await KernelPlugin.from_openai(plugin_name="TestOpenAIPlugin", plugin_str="test") + + +def test_from_openapi(): + openapi_spec_file = os.path.join( + os.path.dirname(__file__), "../../assets/test_plugins", "TestOpenAPIPlugin", "akv-openapi.yaml" + ) + + plugin = KernelPlugin.from_openapi( + plugin_name="TestOpenAPIPlugin", + openapi_document_path=openapi_spec_file, + ) + assert plugin is not None + assert plugin.name == "TestOpenAPIPlugin" + assert plugin.functions.get("GetSecret") is not None + assert plugin.functions.get("SetSecret") is not None + + +def test_from_openapi_missing_document_throws(): + with raises(PluginInitializationError): + KernelPlugin.from_openapi( + plugin_name="TestOpenAPIPlugin", + openapi_document_path=None, + ) + + +# region Static Methods +def test_parse_or_copy_fail(): + with raises(ValueError): + KernelPlugin._parse_or_copy(None, "test") diff --git a/python/tests/unit/kernel/test_kernel.py b/python/tests/unit/kernel/test_kernel.py index 77eaf86975..b89dbc2311 100644 --- a/python/tests/unit/kernel/test_kernel.py +++ b/python/tests/unit/kernel/test_kernel.py @@ -5,9 +5,7 @@ import sys from typing import Union from unittest.mock import AsyncMock, patch -import httpx import pytest -from pydantic import ValidationError from semantic_kernel import Kernel from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase @@ -18,23 +16,16 @@ from semantic_kernel.connectors.openai_plugin.openai_function_execution_paramete from semantic_kernel.events.function_invoked_event_args import FunctionInvokedEventArgs from semantic_kernel.events.function_invoking_event_args import FunctionInvokingEventArgs from semantic_kernel.exceptions import ( - FunctionInitializationError, KernelFunctionAlreadyExistsError, KernelServiceNotFoundError, ServiceInvalidTypeError, ) -from semantic_kernel.exceptions.function_exceptions import ( - FunctionNameNotUniqueError, - PluginInitializationError, - PluginInvalidNameError, -) from semantic_kernel.exceptions.kernel_exceptions import KernelFunctionNotFoundError, KernelPluginNotFoundError from semantic_kernel.exceptions.template_engine_exceptions import TemplateSyntaxError from semantic_kernel.functions.function_result import FunctionResult from semantic_kernel.functions.kernel_arguments import KernelArguments -from semantic_kernel.functions.kernel_function import KernelFunction +from semantic_kernel.functions.kernel_function_decorator import kernel_function from semantic_kernel.functions.kernel_plugin import KernelPlugin -from semantic_kernel.functions.kernel_plugin_collection import KernelPluginCollection from semantic_kernel.services.ai_service_client_base import AIServiceClientBase from semantic_kernel.services.ai_service_selector import AIServiceSelector @@ -76,7 +67,7 @@ def test_kernel_init_with_services_list(service: AIServiceClientBase): def test_kernel_init_with_plugins(): - plugins = KernelPluginCollection() + plugins = {"plugin": KernelPlugin(name="plugin")} kernel = Kernel(plugins=plugins) assert kernel.plugins is not None @@ -88,7 +79,6 @@ def test_kernel_init_with_plugins(): @pytest.mark.asyncio async def test_invoke_function(kernel: Kernel, create_mock_function): mock_function = create_mock_function(name="test_function") - kernel.plugins.add(KernelPlugin(name="test", functions=[mock_function])) await kernel.invoke(mock_function, KernelArguments()) @@ -98,7 +88,7 @@ async def test_invoke_function(kernel: Kernel, create_mock_function): @pytest.mark.asyncio async def test_invoke_functions_by_name(kernel: Kernel, create_mock_function): mock_function = create_mock_function(name="test_function") - kernel.plugins.add(KernelPlugin(name="test", functions=[mock_function])) + kernel.add_plugin(KernelPlugin(name="test", functions=[mock_function])) await kernel.invoke(function_name="test_function", plugin_name="test", arguments=KernelArguments()) @@ -111,7 +101,7 @@ async def test_invoke_functions_by_name(kernel: Kernel, create_mock_function): @pytest.mark.asyncio async def test_invoke_function_fail(kernel: Kernel, create_mock_function): mock_function = create_mock_function(name="test_function") - kernel.plugins.add(KernelPlugin(name="test", functions=[mock_function])) + kernel.add_plugin(KernelPlugin(name="test", functions=[mock_function])) with pytest.raises(KernelFunctionNotFoundError): await kernel.invoke(arguments=KernelArguments()) @@ -124,7 +114,7 @@ async def test_invoke_function_fail(kernel: Kernel, create_mock_function): @pytest.mark.asyncio async def test_invoke_stream_function(kernel: Kernel, create_mock_function): mock_function = create_mock_function(name="test_function") - kernel.plugins.add(KernelPlugin(name="test", functions=[mock_function])) + kernel.add_plugin(KernelPlugin(name="test", functions=[mock_function])) async for part in kernel.invoke_stream(mock_function, input="test"): assert part[0].text == "test" @@ -135,7 +125,7 @@ async def test_invoke_stream_function(kernel: Kernel, create_mock_function): @pytest.mark.asyncio async def test_invoke_stream_functions_throws_exception(kernel: Kernel, create_mock_function): mock_function = create_mock_function(name="test_function") - kernel.plugins.add(KernelPlugin(name="test", functions=[mock_function])) + kernel.add_plugin(KernelPlugin(name="test", functions=[mock_function])) functions = [mock_function] function_result_with_exception = FunctionResult( @@ -198,7 +188,7 @@ def test_invoke_handles_remove(kernel_with_handlers: Kernel): @pytest.mark.asyncio async def test_invoke_handles_pre_invocation(kernel: Kernel, create_mock_function): mock_function = create_mock_function(name="test_function") - kernel.plugins.add(KernelPlugin(name="test", functions=[mock_function])) + kernel.add_plugin(KernelPlugin(name="test", functions=[mock_function])) invoked = 0 @@ -312,12 +302,11 @@ async def test_invoke_change_variable_invoked_handler(kernel: Kernel, create_moc # region Plugins -def test_prompt_plugin_can_be_imported(kernel: Kernel): +def test_add_plugin_from_directory(kernel: Kernel): # import plugins plugins_directory = os.path.join(os.path.dirname(__file__), "../../assets", "test_plugins") # path to plugins directory - plugin = kernel.import_plugin_from_prompt_directory(plugins_directory, "TestPlugin") - + plugin = kernel.add_plugin(plugin_name="TestPlugin", parent_directory=plugins_directory) assert plugin is not None assert len(plugin.functions) == 2 func = plugin.functions["TestFunction"] @@ -326,75 +315,25 @@ def test_prompt_plugin_can_be_imported(kernel: Kernel): assert func_handlebars is not None -def test_prompt_plugin_not_found(kernel: Kernel): - # import plugins - plugins_directory = os.path.join(os.path.dirname(__file__), "../../assets", "test_plugins_fail") - # path to plugins directory - with pytest.raises(PluginInitializationError): - kernel.import_plugin_from_prompt_directory(plugins_directory, "TestPlugin") +def test_plugin_no_plugin(kernel: Kernel): + with pytest.raises(ValueError): + kernel.add_plugin(plugin_name="test") def test_plugin_name_error(kernel: Kernel): - with pytest.raises(PluginInvalidNameError): - kernel.import_plugin_from_object(None, " ") + with pytest.raises(ValueError): + kernel.add_plugin(" ", None) -def test_native_plugin_can_be_imported(kernel: Kernel): - # import plugins - plugins_directory = os.path.join(os.path.dirname(__file__), "../../assets", "test_native_plugins") - # path to plugins directory - plugin = kernel.import_native_plugin_from_directory(plugins_directory, "TestNativePlugin") +def test_plugins_add_plugins(kernel: Kernel): + plugin1 = KernelPlugin(name="TestPlugin") + plugin2 = KernelPlugin(name="TestPlugin2") - 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" + kernel.add_plugins([plugin1, plugin2]) + assert len(kernel.plugins) == 2 -def test_native_plugin_cannot_be_imported(kernel: Kernel): - # import plugins - plugins_directory = os.path.join(os.path.dirname(__file__), "../../assets", "test_native_plugins") - # path to plugins directory - plugin = kernel.import_native_plugin_from_directory(plugins_directory, "TestNativePluginNoClass") - - assert not plugin - - -def test_native_plugin_not_found(kernel: Kernel): - # import plugins - plugins_directory = os.path.join(os.path.dirname(__file__), "../../assets", "test_native_plugins_fail") - # path to plugins directory - with pytest.raises(PluginInitializationError): - kernel.import_native_plugin_from_directory(plugins_directory, "TestNativePlugin") - - -def test_plugin_from_object_dict(kernel: Kernel, decorated_native_function): - plugin_obj = {"getLightStatusFunc": decorated_native_function} - plugin = kernel.import_plugin_from_object(plugin_obj, "TestPlugin") - - assert plugin is not None - assert len(plugin.functions) == 1 - assert plugin.functions.get("getLightStatus") is not None - - -def test_plugin_from_object_custom_class(kernel: Kernel, custom_plugin_class): - plugin = kernel.import_plugin_from_object(custom_plugin_class(), "TestPlugin") - - assert plugin is not None - assert len(plugin.functions) == 1 - assert plugin.functions.get("getLightStatus") is not None - - -def test_plugin_from_object_custom_class_name_not_unique(kernel: Kernel, custom_plugin_class): - plugin_obj = custom_plugin_class() - plugin_obj.decorated_native_function_2 = plugin_obj.decorated_native_function - with pytest.raises(FunctionNameNotUniqueError): - kernel.import_plugin_from_object(plugin_obj, "TestPlugin") - - -def test_create_function_from_prompt_succeeds(kernel: Kernel): +def test_add_function_from_prompt(kernel: Kernel): prompt = """ Write a short story about two Corgis on an adventure. The story must be: @@ -406,7 +345,7 @@ def test_create_function_from_prompt_succeeds(kernel: Kernel): - The two names of the corgis are {{GenerateNames.generate_names}} """ - func = kernel.create_function_from_prompt( + kernel.add_function( prompt=prompt, function_name="TestFunction", plugin_name="TestPlugin", @@ -415,57 +354,58 @@ def test_create_function_from_prompt_succeeds(kernel: Kernel): extension_data={"max_tokens": 500, "temperature": 0.5, "top_p": 0.5} ), ) + func = kernel.get_function("TestPlugin", "TestFunction") assert func.name == "TestFunction" assert func.description == "Write a short story." assert len(func.parameters) == 2 -def test_create_function_from_yaml_empty_string(kernel: Kernel): - with pytest.raises(PluginInitializationError): - kernel.create_function_from_yaml("", "plugin_name") +def test_add_function_not_provided(kernel: Kernel): + with pytest.raises(ValueError): + kernel.add_function(function_name="TestFunction", plugin_name="TestPlugin") -def test_create_function_from_yaml_malformed_string(kernel: Kernel): - with pytest.raises(PluginInitializationError): - kernel.create_function_from_yaml("not yaml dict", "plugin_name") +def test_add_functions(kernel: Kernel): + @kernel_function(name="func1") + def func1(arg1: str) -> str: + return "test" + + @kernel_function(name="func2") + def func2(arg1: str) -> str: + return "test" + + plugin = kernel.add_functions(plugin_name="test", functions=[func1, func2]) + assert len(plugin.functions) == 2 -def test_create_function_from_valid_yaml(kernel: Kernel): - plugins_directory = os.path.join(os.path.dirname(__file__), "../../assets/test_plugins", "TestPlugin") +def test_add_functions_to_existing(kernel: Kernel): + kernel.add_plugin(KernelPlugin(name="test")) - plugin = kernel.import_plugin_from_prompt_directory(plugins_directory, "TestFunctionYaml") - assert plugin is not None + @kernel_function(name="func1") + def func1(arg1: str) -> str: + return "test" + @kernel_function(name="func2") + def func2(arg1: str) -> str: + return "test" -def test_create_function_from_valid_yaml_handlebars(kernel: Kernel): - plugins_directory = os.path.join(os.path.dirname(__file__), "../../assets/test_plugins", "TestPlugin") - - plugin = kernel.import_plugin_from_prompt_directory(plugins_directory, "TestFunctionYamlHandlebars") - assert plugin is not None - assert plugin["TestFunctionHandlebars"] is not None - - -def test_create_function_from_valid_yaml_jinja2(kernel: Kernel): - plugins_directory = os.path.join(os.path.dirname(__file__), "../../assets/test_plugins", "TestPlugin") - - plugin = kernel.import_plugin_from_prompt_directory(plugins_directory, "TestFunctionYamlJinja2") - assert plugin is not None - assert plugin["TestFunctionJinja2"] is not None + plugin = kernel.add_functions(plugin_name="test", functions=[func1, func2]) + assert len(plugin.functions) == 2 @pytest.mark.asyncio @patch("semantic_kernel.connectors.openai_plugin.openai_utils.OpenAIUtils.parse_openai_manifest_for_openapi_spec_url") -async def test_import_openai_plugin_from_file(mock_parse_openai_manifest, kernel: Kernel): - openai_spec_file = os.path.join(os.path.dirname(__file__), "../../assets/test_plugins", "TestPlugin") - with open(os.path.join(openai_spec_file, "TestOpenAIPlugin", "akv-openai.json"), "r") as file: +async def test_add_plugin_from_openai(mock_parse_openai_manifest, kernel: Kernel): + base_folder = os.path.join(os.path.dirname(__file__), "../../assets/test_plugins") + with open(os.path.join(base_folder, "TestOpenAIPlugin", "akv-openai.json"), "r") as file: openai_spec = file.read() openapi_spec_file_path = os.path.join( - os.path.dirname(__file__), "../../assets/test_plugins", "TestPlugin", "TestOpenAPIPlugin", "akv-openapi.yaml" + os.path.dirname(__file__), base_folder, "TestOpenAPIPlugin", "akv-openapi.yaml" ) mock_parse_openai_manifest.return_value = openapi_spec_file_path - plugin = await kernel.import_plugin_from_openai( + await kernel.add_plugin_from_openai( plugin_name="TestOpenAIPlugin", plugin_str=openai_spec, execution_parameters=OpenAIFunctionExecutionParameters( @@ -475,114 +415,67 @@ async def test_import_openai_plugin_from_file(mock_parse_openai_manifest, kernel enable_dynamic_payload=True, ), ) + plugin = kernel.plugins["TestOpenAIPlugin"] assert plugin is not None assert plugin.name == "TestOpenAIPlugin" assert plugin.functions.get("GetSecret") is not None assert plugin.functions.get("SetSecret") is not None -@pytest.mark.asyncio -@patch("httpx.AsyncClient.get") -@patch("semantic_kernel.connectors.openai_plugin.openai_utils.OpenAIUtils.parse_openai_manifest_for_openapi_spec_url") -async def test_import_openai_plugin_from_url(mock_parse_openai_manifest, mock_get, kernel: Kernel): - openai_spec_file_path = os.path.join( - os.path.dirname(__file__), "../../assets/test_plugins", "TestPlugin", "TestOpenAIPlugin", "akv-openai.json" - ) - with open(openai_spec_file_path, "r") as file: - openai_spec = file.read() - - openapi_spec_file_path = os.path.join( - os.path.dirname(__file__), "../../assets/test_plugins", "TestPlugin", "TestOpenAPIPlugin", "akv-openapi.yaml" - ) - mock_parse_openai_manifest.return_value = openapi_spec_file_path - - request = httpx.Request(method="GET", url="http://fake-url.com/akv-openai.json") - - response = httpx.Response(200, text=openai_spec, request=request) - mock_get.return_value = response - - fake_plugin_url = "http://fake-url.com/akv-openai.json" - plugin = await kernel.import_plugin_from_openai( - plugin_name="TestOpenAIPlugin", - plugin_url=fake_plugin_url, - execution_parameters=OpenAIFunctionExecutionParameters( - auth_callback=AsyncMock(), - server_url_override="http://localhost", - enable_dynamic_payload=True, - ), - ) - - assert plugin is not None - assert plugin.name == "TestOpenAIPlugin" - assert plugin.functions.get("GetSecret") is not None - assert plugin.functions.get("SetSecret") is not None - - mock_get.assert_awaited_once_with(fake_plugin_url, headers={"User-Agent": "Semantic-Kernel"}) - - def test_import_plugin_from_openapi(kernel: Kernel): openapi_spec_file = os.path.join( - os.path.dirname(__file__), "../../assets/test_plugins", "TestPlugin", "TestOpenAPIPlugin", "akv-openapi.yaml" + os.path.dirname(__file__), "../../assets/test_plugins", "TestOpenAPIPlugin", "akv-openapi.yaml" ) - plugin = kernel.import_plugin_from_openapi( + kernel.add_plugin_from_openapi( plugin_name="TestOpenAPIPlugin", openapi_document_path=openapi_spec_file, ) - + plugin = kernel.plugins["TestOpenAPIPlugin"] assert plugin is not None assert plugin.name == "TestOpenAPIPlugin" assert plugin.functions.get("GetSecret") is not None assert plugin.functions.get("SetSecret") is not None -def test_import_plugin_from_openapi_missing_document_throws(kernel: Kernel): - with pytest.raises(PluginInitializationError): - kernel.import_plugin_from_openapi( - plugin_name="TestOpenAPIPlugin", - openapi_document_path=None, - ) +def test_get_plugin(kernel: Kernel): + kernel.add_plugin(KernelPlugin(name="TestPlugin")) + plugin = kernel.get_plugin("TestPlugin") + assert plugin is not None -# endregion -# region Functions +def test_get_plugin_not_found(kernel: Kernel): + with pytest.raises(KernelPluginNotFoundError): + kernel.get_plugin("TestPlugin2") -def test_func(kernel: Kernel, custom_plugin_class): - kernel.import_plugin_from_object(custom_plugin_class(), "TestPlugin") - func = kernel.func("TestPlugin", "getLightStatus") +def test_get_function(kernel: Kernel, custom_plugin_class): + kernel.add_plugin(custom_plugin_class(), "TestPlugin") + func = kernel.get_function("TestPlugin", "getLightStatus") assert func def test_func_plugin_not_found(kernel: Kernel): with pytest.raises(KernelPluginNotFoundError): - kernel.func("TestPlugin", "TestFunction") + kernel.get_function("TestPlugin", "TestFunction") def test_func_function_not_found(kernel: Kernel, custom_plugin_class): - kernel.import_plugin_from_object(custom_plugin_class(), "TestPlugin") + kernel.add_plugin(custom_plugin_class(), "TestPlugin") with pytest.raises(KernelFunctionNotFoundError): - kernel.func("TestPlugin", "TestFunction") + kernel.get_function("TestPlugin", "TestFunction") -@pytest.mark.asyncio -async def test_register_valid_native_function(kernel: Kernel, decorated_native_function): - registered_func = kernel.register_function_from_method("TestPlugin", decorated_native_function) - - assert isinstance(registered_func, KernelFunction) - assert kernel.plugins["TestPlugin"]["getLightStatus"] == registered_func - func_result = await registered_func.invoke(kernel, KernelArguments(arg1="testtest")) - assert str(func_result) == "test" +def test_get_function_from_fqn(kernel: Kernel, custom_plugin_class): + kernel.add_plugin(custom_plugin_class(), "TestPlugin") + func = kernel.get_function_from_fully_qualified_function_name("TestPlugin-getLightStatus") + assert func -def test_register_undecorated_native_function(kernel: Kernel, not_decorated_native_function): - with pytest.raises(FunctionInitializationError): - kernel.register_function_from_method("TestPlugin", not_decorated_native_function) - - -def test_register_with_none_plugin_name(kernel: Kernel, decorated_native_function): - with pytest.raises(ValidationError): - kernel.register_function_from_method(method=decorated_native_function, plugin_name=None) +def test_get_function_from_fqn_wo_plugin(kernel: Kernel, custom_plugin_class): + kernel.add_plugin(custom_plugin_class(), "TestPlugin") + func = kernel.get_function_from_fully_qualified_function_name("getLightStatus") + assert func # endregion diff --git a/python/tests/unit/kernel/test_register_functions.py b/python/tests/unit/kernel/test_register_functions.py index b4b4afc768..abcb7d5892 100644 --- a/python/tests/unit/kernel/test_register_functions.py +++ b/python/tests/unit/kernel/test_register_functions.py @@ -7,14 +7,15 @@ import pytest from pydantic import ValidationError from semantic_kernel import Kernel -from semantic_kernel.exceptions import FunctionInitializationError +from semantic_kernel.exceptions.function_exceptions import FunctionInitializationError from semantic_kernel.functions.kernel_arguments import KernelArguments from semantic_kernel.functions.kernel_function import KernelFunction @pytest.mark.asyncio async def test_register_valid_native_function(kernel: Kernel, decorated_native_function: Callable): - registered_func = kernel.register_function_from_method("TestPlugin", decorated_native_function) + kernel.add_function("TestPlugin", function=decorated_native_function) + registered_func = kernel.get_function("TestPlugin", "getLightStatus") assert isinstance(registered_func, KernelFunction) assert kernel.plugins["TestPlugin"]["getLightStatus"] == registered_func @@ -24,9 +25,9 @@ async def test_register_valid_native_function(kernel: Kernel, decorated_native_f def test_register_undecorated_native_function(kernel: Kernel, not_decorated_native_function: Callable): with pytest.raises(FunctionInitializationError): - kernel.register_function_from_method("TestPlugin", not_decorated_native_function) + kernel.add_function("TestPlugin", not_decorated_native_function) def test_register_with_none_plugin_name(kernel: Kernel, decorated_native_function: Callable): with pytest.raises(ValidationError): - kernel.register_function_from_method(method=decorated_native_function, plugin_name=None) + kernel.add_function(function=decorated_native_function, plugin_name=None) diff --git a/python/tests/unit/planners/action_planner/test_action_planner.py b/python/tests/unit/planners/action_planner/test_action_planner.py index 3b9a864da1..c71fa6ce8a 100644 --- a/python/tests/unit/planners/action_planner/test_action_planner.py +++ b/python/tests/unit/planners/action_planner/test_action_planner.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. from textwrap import dedent -from unittest.mock import MagicMock, Mock +from unittest.mock import Mock import pytest @@ -15,146 +15,10 @@ from semantic_kernel.exceptions import ( from semantic_kernel.functions.function_result import FunctionResult from semantic_kernel.functions.kernel_function import KernelFunction from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata -from semantic_kernel.functions.kernel_plugin import KernelPlugin -from semantic_kernel.functions.kernel_plugin_collection import KernelPluginCollection from semantic_kernel.planners import ActionPlanner from semantic_kernel.planners.action_planner.action_planner_config import ActionPlannerConfig -def create_mock_function(kernel_function_metadata: KernelFunctionMetadata) -> Mock(spec=KernelFunction): - mock_function = Mock(spec=KernelFunction) - mock_function.metadata = kernel_function_metadata - mock_function.name = kernel_function_metadata.name - mock_function.plugin_name = kernel_function_metadata.plugin_name - mock_function.is_prompt = kernel_function_metadata.is_prompt - mock_function.description = kernel_function_metadata.description - mock_function.prompt_execution_settings = PromptExecutionSettings() - return mock_function - - -def test_throw_without_kernel(): - with pytest.raises(PlannerInvalidConfigurationError): - ActionPlanner(None, None) - - -@pytest.fixture -def mock_kernel(plugins_input): - kernel = Mock(spec=Kernel) - plugins = MagicMock(spec=KernelPluginCollection) - functions_list = [] - - mock_plugins = {} - - for name, plugin_name, description, is_prompt in plugins_input: - kernel_function_metadata = KernelFunctionMetadata( - name=name, - plugin_name=plugin_name, - description=description, - parameters=[], - is_prompt=is_prompt, - is_asynchronous=True, - ) - mock_function = create_mock_function(kernel_function_metadata) - functions_list.append(kernel_function_metadata) - - if plugin_name not in mock_plugins: - mock_plugins[plugin_name] = {} - mock_plugins[plugin_name][name] = mock_function - - mock_function.invoke.return_value = FunctionResult( - function=kernel_function_metadata, value="MOCK FUNCTION CALLED", metadata={"arguments": {}} - ) - - plugins.__getitem__.side_effect = lambda plugin_name: MagicMock(__getitem__=mock_plugins[plugin_name].__getitem__) - - kernel.plugins = plugins - kernel.plugins.get_list_of_function_metadata.return_value = functions_list - return kernel - - -@pytest.mark.asyncio -async def test_plan_creation(): - goal = "Translate Happy birthday to German." - plan_str = dedent( - """Here is a plan that can achieve the given task:\n\n{""plan"":\n{""rationale"": - ""the list contains a function that allows to translate one language to another."", - ""function"": ""WriterPlugin.Translate"",""parameters"": \n{""translate_from"": - ""english"",""translate_to"": ""german"",""input"": ""Happy birthday""}\n}\n}\n\n - This plan makes use of the Translate function in WriterPlugin to translate the message - `Happy birthday` from english to german.""" - ) - - kernel = Mock(spec=Kernel) - mock_function = Mock(spec=KernelFunction) - plugins = KernelPluginCollection() - kernel.plugins = plugins - - kernel_function_metadata = KernelFunctionMetadata( - name="Translate", - description="Translate something", - plugin_name="WriterPlugin", - is_prompt=False, - parameters=[], - ) - mock_function = create_mock_function(kernel_function_metadata) - - kernel.plugins.add(plugin=KernelPlugin(name=kernel_function_metadata.plugin_name, functions=[mock_function])) - - function_result = FunctionResult(function=kernel_function_metadata, value=plan_str, metadata={}) - mock_function.invoke.return_value = function_result - - kernel.create_function_from_prompt.return_value = mock_function - - planner = ActionPlanner(kernel, service_id="test") - plan = await planner.create_plan(goal) - - assert plan is not None - assert plan.description == mock_function.description - assert "translate_from" in plan.state - assert "translate_to" in plan.state - assert "input" in plan.state - - -@pytest.mark.asyncio -async def test_no_parameter_plan_creation(): - goal = "What date is it today?" - plan_str = dedent( - """Here is a plan that can achieve the given task:\n\n{""plan"":\n{""rationale"": - ""the list contains a function that allows to get today's date."", - ""function"": ""TimePlugin.today""\n}\n}\n\n - This plan makes use of the today function in TimePlugin to get today's date.""" - ) - - kernel = Mock(spec=Kernel) - mock_function = Mock(spec=KernelFunction) - plugins = KernelPluginCollection() - kernel.plugins = plugins - - kernel_function_metadata = KernelFunctionMetadata( - name="today", - description="Get Today's date", - plugin_name="TimePlugin", - is_prompt=False, - parameters=[], - ) - mock_function = create_mock_function(kernel_function_metadata) - - kernel.plugins.add(plugin=KernelPlugin(name=kernel_function_metadata.plugin_name, functions=[mock_function])) - - function_result = FunctionResult(function=kernel_function_metadata, value=plan_str, metadata={}) - mock_function.invoke.return_value = function_result - - kernel.create_function_from_prompt.return_value = mock_function - - planner = ActionPlanner(kernel, service_id="test") - plan = await planner.create_plan(goal) - - assert plan is not None - assert plan.parameters == {} - assert plan.state == {} - assert plan.description == mock_function.description - - @pytest.fixture def plugins_input(): return [ @@ -166,6 +30,139 @@ def plugins_input(): ] +def create_mock_function( + kernel_function_metadata: KernelFunctionMetadata, return_value: FunctionResult +) -> KernelFunction: + mock_function = Mock(spec=KernelFunction) + mock_function.metadata = kernel_function_metadata + mock_function.name = kernel_function_metadata.name + mock_function.plugin_name = kernel_function_metadata.plugin_name + mock_function.is_prompt = kernel_function_metadata.is_prompt + mock_function.description = kernel_function_metadata.description + mock_function.prompt_execution_settings = PromptExecutionSettings() + mock_function.invoke.return_value = return_value + mock_function.function_copy.return_value = mock_function + return mock_function + + +def test_throw_without_kernel(): + with pytest.raises(PlannerInvalidConfigurationError): + ActionPlanner(None, None) + + +@pytest.fixture +def mock_kernel(plugins_input, kernel: Kernel): + for name, plugin_name, description, is_prompt in plugins_input: + kernel_function_metadata = KernelFunctionMetadata( + name=name, + plugin_name=plugin_name, + description=description, + parameters=[], + is_prompt=is_prompt, + is_asynchronous=True, + ) + kernel.add_function( + plugin_name, + function=create_mock_function( + kernel_function_metadata, + FunctionResult( + function=kernel_function_metadata, value="MOCK FUNCTION CALLED", metadata={"arguments": {}} + ), + ), + ) + + return kernel + + +@pytest.mark.asyncio +async def test_plan_creation(kernel: Kernel): + goal = "Translate Happy birthday to German." + plan_str = dedent( + """Here is a plan that can achieve the given task:\n\n{""plan"":\n{""rationale"": + ""the list contains a function that allows to translate one language to another."", + ""function"": ""WriterPlugin.Translate"",""parameters"": \n{""translate_from"": + ""english"",""translate_to"": ""german"",""input"": ""Happy birthday""}\n}\n}\n\n + This plan makes use of the Translate function in WriterPlugin to translate the message + `Happy birthday` from english to german.""" + ) + + mock_function = Mock(spec=KernelFunction) + + kernel_function_metadata = KernelFunctionMetadata( + name="Translate", + description="Translate something", + plugin_name="WriterPlugin", + is_prompt=False, + parameters=[], + ) + mock_function = create_mock_function( + kernel_function_metadata, FunctionResult(function=kernel_function_metadata, value=plan_str, metadata={}) + ) + + kernel.add_function("WriterPlugin", function=mock_function) + + planner = ActionPlanner(kernel, service_id="test") + planner._planner_function = create_mock_function( + KernelFunctionMetadata( + name="ActionPlanner", + description="Translate something", + plugin_name=planner.RESTRICTED_PLUGIN_NAME, + is_prompt=True, + parameters=[], + ), + FunctionResult(function=kernel_function_metadata, value=plan_str, metadata={}), + ) + plan = await planner.create_plan(goal) + + assert plan is not None + assert plan.description == mock_function.description + assert "translate_from" in plan.state + assert "translate_to" in plan.state + assert "input" in plan.state + + +@pytest.mark.asyncio +async def test_no_parameter_plan_creation(kernel: Kernel): + goal = "What date is it today?" + plan_str = dedent( + """Here is a plan that can achieve the given task:\n\n{""plan"":\n{""rationale"": + ""the list contains a function that allows to get today's date."", + ""function"": ""TimePlugin.today""\n}\n}\n\n + This plan makes use of the today function in TimePlugin to get today's date.""" + ) + + kernel_function_metadata = KernelFunctionMetadata( + name="today", + description="Get Today's date", + plugin_name="TimePlugin", + is_prompt=False, + parameters=[], + ) + mock_function = create_mock_function( + kernel_function_metadata, FunctionResult(function=kernel_function_metadata, value=plan_str, metadata={}) + ) + + kernel.add_function("TimePlugin", function=mock_function) + + planner = ActionPlanner(kernel, service_id="test") + planner._planner_function = create_mock_function( + KernelFunctionMetadata( + name="ActionPlanner", + description="Translate something", + plugin_name=planner.RESTRICTED_PLUGIN_NAME, + is_prompt=True, + parameters=[], + ), + FunctionResult(function=kernel_function_metadata, value=plan_str, metadata={}), + ) + plan = await planner.create_plan(goal) + + assert plan is not None + assert plan.parameters == {} + assert plan.state == {} + assert plan.description == mock_function.description + + def test_available_functions(plugins_input, mock_kernel): goal = "Translate Happy birthday to German." @@ -213,13 +210,9 @@ def test_exclude_functions(plugins_input, mock_kernel): @pytest.mark.asyncio -async def test_empty_goal_throw(): +async def test_empty_goal_throw(kernel: Kernel): goal = "" - - kernel = Mock(spec=Kernel) mock_function = Mock(spec=KernelFunction) - plugins = MagicMock(spec=KernelPluginCollection) - kernel.plugins = plugins kernel_function_metadata = KernelFunctionMetadata( name="Translate", @@ -228,9 +221,10 @@ async def test_empty_goal_throw(): is_prompt=False, parameters=[], ) - mock_function = create_mock_function(kernel_function_metadata) - kernel.plugins.__getitem__.return_value = MagicMock(__getitem__=MagicMock(return_value=mock_function)) - + mock_function = create_mock_function( + kernel_function_metadata, FunctionResult(function=kernel_function_metadata, value="", metadata={}) + ) + kernel.add_function("WriterPlugin", mock_function) planner = ActionPlanner(kernel, service_id="test") with pytest.raises(PlannerInvalidGoalError): @@ -238,14 +232,10 @@ async def test_empty_goal_throw(): @pytest.mark.asyncio -async def test_invalid_json_throw(): +async def test_invalid_json_throw(kernel: Kernel): goal = "Translate Happy birthday to German." plan_str = '{"":{""function"": ""WriterPlugin.Translate""}}' - kernel = Mock(spec=Kernel) - plugins = MagicMock(spec=KernelPluginCollection) - kernel.plugins = plugins - kernel_function_metadata = KernelFunctionMetadata( name="Translate", plugin_name="WriterPlugin", @@ -253,16 +243,22 @@ async def test_invalid_json_throw(): is_prompt=False, parameters=[], ) - mock_function = create_mock_function(kernel_function_metadata) - - plugins.__getitem__.return_value = MagicMock(__getitem__=MagicMock(return_value=mock_function)) - - function_result = FunctionResult(function=kernel_function_metadata, value=plan_str, metadata={}) - mock_function.invoke.return_value = function_result - - kernel.create_function_from_prompt.return_value = mock_function + mock_function = create_mock_function( + kernel_function_metadata, FunctionResult(function=kernel_function_metadata, value=plan_str, metadata={}) + ) + kernel.add_function("WriterPlugin", mock_function) planner = ActionPlanner(kernel, service_id="test") + planner._planner_function = create_mock_function( + KernelFunctionMetadata( + name="ActionPlanner", + description="Translate something", + plugin_name=planner.RESTRICTED_PLUGIN_NAME, + is_prompt=True, + parameters=[], + ), + FunctionResult(function=kernel_function_metadata, value=plan_str, metadata={}), + ) with pytest.raises(PlannerInvalidPlanError): await planner.create_plan(goal) diff --git a/python/tests/unit/planners/sequential_planner/test_sequential_planner.py b/python/tests/unit/planners/sequential_planner/test_sequential_planner.py index 9e66e17451..0cb00f0b4b 100644 --- a/python/tests/unit/planners/sequential_planner/test_sequential_planner.py +++ b/python/tests/unit/planners/sequential_planner/test_sequential_planner.py @@ -1,6 +1,6 @@ # Copyright (c) Microsoft. All rights reserved. -from unittest.mock import Mock, patch +from unittest.mock import Mock import pytest @@ -9,14 +9,13 @@ from semantic_kernel.exceptions import PlannerException, PlannerInvalidGoalError from semantic_kernel.functions.function_result import FunctionResult from semantic_kernel.functions.kernel_function import KernelFunction from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata -from semantic_kernel.functions.kernel_plugin import KernelPlugin -from semantic_kernel.functions.kernel_plugin_collection import KernelPluginCollection from semantic_kernel.kernel import Kernel -from semantic_kernel.memory.semantic_text_memory import SemanticTextMemoryBase from semantic_kernel.planners.sequential_planner.sequential_planner import SequentialPlanner -def create_mock_function(kernel_function_metadata: KernelFunctionMetadata): +def create_mock_function( + kernel_function_metadata: KernelFunctionMetadata, return_value: FunctionResult +) -> KernelFunction: mock_function = Mock(spec=KernelFunction) mock_function.metadata = kernel_function_metadata mock_function.name = kernel_function_metadata.name @@ -24,6 +23,8 @@ def create_mock_function(kernel_function_metadata: KernelFunctionMetadata): mock_function.is_prompt = kernel_function_metadata.is_prompt mock_function.description = kernel_function_metadata.description mock_function.prompt_execution_settings = PromptExecutionSettings() + mock_function.invoke.return_value = return_value + mock_function.function_copy.return_value = mock_function return mock_function @@ -38,28 +39,24 @@ async def test_it_can_create_plan(goal, kernel: Kernel): ("Summarize", "SummarizePlugin", "Summarize something", True), ] - functions_list = [] - kernel.plugins = KernelPluginCollection() - mock_functions = [] - for name, pluginName, description, is_prompt in input: + for name, plugin_name, description, is_prompt in input: kernel_function_metadata = KernelFunctionMetadata( name=name, - plugin_name=pluginName, + plugin_name=plugin_name, description=description, parameters=[], is_prompt=is_prompt, is_asynchronous=True, ) - mock_function = create_mock_function(kernel_function_metadata) - functions_list.append(kernel_function_metadata) - mock_function.invoke.return_value = FunctionResult( - function=kernel_function_metadata, value="MOCK FUNCTION CALLED", metadata={} + kernel.add_function( + plugin_name, + function=create_mock_function( + kernel_function_metadata, + FunctionResult( + function=kernel_function_metadata, value="MOCK FUNCTION CALLED", metadata={"arguments": {}} + ), + ), ) - mock_functions.append(mock_function) - - if pluginName not in kernel.plugins.plugins: - kernel.plugins.add(KernelPlugin(name=pluginName, description="Mock plugin")) - kernel.plugins.add_functions_to_plugin([mock_function], pluginName) expected_functions = [x[0] for x in input] expected_plugins = [x[1] for x in input] @@ -80,26 +77,24 @@ async def test_it_can_create_plan(goal, kernel: Kernel): value=plan_string, metadata={}, ) - with patch("semantic_kernel.kernel.Kernel.create_function_from_prompt", return_value=mock_function_flow_function): - planner = SequentialPlanner(kernel, service_id="test") - # Act - plan = await planner.create_plan(goal) + planner = SequentialPlanner(kernel, service_id="test") + planner._function_flow_function = mock_function_flow_function + # Act + plan = await planner.create_plan(goal) - # Assert - assert plan.description == goal - assert any(step.name in expected_functions and step.plugin_name in expected_plugins for step in plan._steps) - for expected_function in expected_functions: - assert any(step.name == expected_function for step in plan._steps) - for expectedPlugin in expected_plugins: - assert any(step.plugin_name == expectedPlugin for step in plan._steps) + # Assert + assert plan.description == goal + assert any(step.name in expected_functions and step.plugin_name in expected_plugins for step in plan._steps) + for expected_function in expected_functions: + assert any(step.name == expected_function for step in plan._steps) + for expectedPlugin in expected_plugins: + assert any(step.plugin_name == expectedPlugin for step in plan._steps) @pytest.mark.asyncio -async def test_empty_goal_throws(): +async def test_empty_goal_throws(kernel: Kernel): # Arrange - kernel = Mock(spec=Kernel) - kernel.prompt_template_engine = Mock() planner = SequentialPlanner(kernel, service_id="test") # Act & Assert @@ -108,16 +103,8 @@ async def test_empty_goal_throws(): @pytest.mark.asyncio -async def test_invalid_xml_throws(): +async def test_invalid_xml_throws(kernel: Kernel): # Arrange - kernel = Mock(spec=Kernel) - kernel.prompt_template_engine = Mock() - memory = Mock(spec=SemanticTextMemoryBase) - kernel.memory = memory - plugins = Mock(spec=KernelPluginCollection) - - functions_list = [] - plugins.get_list_of_function_metadata.return_value = functions_list plan_string = "notvalid<" function_result = FunctionResult( @@ -131,10 +118,8 @@ async def test_invalid_xml_throws(): mock_function_flow_function = Mock(spec=KernelFunction) mock_function_flow_function.invoke.return_value = function_result - kernel.plugins = plugins - kernel.create_function_from_prompt.return_value = mock_function_flow_function - planner = SequentialPlanner(kernel, service_id="test") + planner._function_flow_function = mock_function_flow_function # Act & Assert with pytest.raises(PlannerException): diff --git a/python/tests/unit/planners/sequential_planner/test_sequential_planner_extensions.py b/python/tests/unit/planners/sequential_planner/test_sequential_planner_extensions.py index d65161b90b..161be79bb6 100644 --- a/python/tests/unit/planners/sequential_planner/test_sequential_planner_extensions.py +++ b/python/tests/unit/planners/sequential_planner/test_sequential_planner_extensions.py @@ -1,31 +1,42 @@ # Copyright (c) Microsoft. All rights reserved. + from unittest.mock import Mock import pytest +from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings +from semantic_kernel.functions.function_result import FunctionResult from semantic_kernel.functions.kernel_arguments import KernelArguments +from semantic_kernel.functions.kernel_function import KernelFunction from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata -from semantic_kernel.functions.kernel_plugin_collection import ( - KernelPluginCollection, -) from semantic_kernel.kernel import Kernel -from semantic_kernel.planners.sequential_planner.sequential_planner_config import ( - SequentialPlannerConfig, -) -from semantic_kernel.planners.sequential_planner.sequential_planner_extensions import ( - SequentialPlannerKernelExtension, -) +from semantic_kernel.planners.sequential_planner.sequential_planner_config import SequentialPlannerConfig +from semantic_kernel.planners.sequential_planner.sequential_planner_extensions import SequentialPlannerKernelExtension async def _async_generator(query_result): yield query_result +def create_mock_function( + kernel_function_metadata: KernelFunctionMetadata, return_value: FunctionResult +) -> KernelFunction: + mock_function = Mock(spec=KernelFunction) + mock_function.metadata = kernel_function_metadata + mock_function.name = kernel_function_metadata.name + mock_function.plugin_name = kernel_function_metadata.plugin_name + mock_function.is_prompt = kernel_function_metadata.is_prompt + mock_function.description = kernel_function_metadata.description + mock_function.prompt_execution_settings = PromptExecutionSettings() + mock_function.invoke.return_value = return_value + mock_function.function_copy.return_value = mock_function + return mock_function + + @pytest.mark.asyncio -async def test_can_call_get_available_functions_with_no_functions(): +async def test_can_call_get_available_functions_with_no_functions(kernel: Kernel): arguments = KernelArguments() - kernel = Kernel() # Arrange GetAvailableFunctionsAsync parameters config = SequentialPlannerConfig() @@ -39,10 +50,8 @@ async def test_can_call_get_available_functions_with_no_functions(): @pytest.mark.asyncio -async def test_can_call_get_available_functions_with_functions(): +async def test_can_call_get_available_functions_with_functions(kernel: Kernel): arguments = KernelArguments() - kernel = Kernel() - functions_list = [] kernel_function_metadata = KernelFunctionMetadata( name="functionName", plugin_name="pluginName", @@ -59,13 +68,8 @@ async def test_can_call_get_available_functions_with_functions(): is_prompt=False, is_asynchronous=False, ) - functions_list.append(kernel_function_metadata) - functions_list.append(native_kernel_function_metadata) - - mock_plugins = Mock(spec=KernelPluginCollection) - mock_plugins.get_list_of_function_metadata.return_value = functions_list - - kernel.plugins = mock_plugins + kernel.add_function("pluginName", create_mock_function(kernel_function_metadata, None)) + kernel.add_function("pluginName", create_mock_function(native_kernel_function_metadata, None)) # Arrange GetAvailableFunctionsAsync parameters config = SequentialPlannerConfig() @@ -93,11 +97,8 @@ async def test_can_call_get_available_functions_with_functions(): @pytest.mark.asyncio -async def test_can_call_get_available_functions_with_default_relevancy(): +async def test_can_call_get_available_functions_with_default_relevancy(kernel: Kernel): # Arrange - plugins = KernelPluginCollection() - kernel = Kernel() - kernel.plugins = plugins arguments = KernelArguments() # Arrange GetAvailableFunctionsAsync parameters diff --git a/python/tests/unit/planners/sequential_planner/test_sequential_planner_parser.py b/python/tests/unit/planners/sequential_planner/test_sequential_planner_parser.py index 2edbd7ef01..6edd272c1e 100644 --- a/python/tests/unit/planners/sequential_planner/test_sequential_planner_parser.py +++ b/python/tests/unit/planners/sequential_planner/test_sequential_planner_parser.py @@ -22,6 +22,7 @@ def create_mock_function(kernel_function_metadata: KernelFunctionMetadata) -> Ke mock_function.description = kernel_function_metadata.description mock_function.is_prompt = kernel_function_metadata.is_prompt mock_function.prompt_execution_settings = PromptExecutionSettings() + mock_function.function_copy.return_value = mock_function return mock_function @@ -43,7 +44,7 @@ def create_kernel_and_functions_mock(functions) -> Kernel: mock_function.invoke.return_value = FunctionResult( function=kernel_function_metadata, value=result_string, metadata={} ) - kernel.plugins.add(KernelPlugin(name=plugin_name, functions=[mock_function])) + kernel.add_plugin(KernelPlugin(name=plugin_name, functions=[mock_function])) return kernel diff --git a/python/tests/unit/planners/stepwise_planner/test_stepwise_planner_parse_result.py b/python/tests/unit/planners/stepwise_planner/test_stepwise_planner_parse_result.py index 7e0ddcbad5..08524e5da5 100644 --- a/python/tests/unit/planners/stepwise_planner/test_stepwise_planner_parse_result.py +++ b/python/tests/unit/planners/stepwise_planner/test_stepwise_planner_parse_result.py @@ -1,6 +1,5 @@ # Copyright (c) Microsoft. All rights reserved. -from unittest.mock import Mock import pytest @@ -20,9 +19,8 @@ from semantic_kernel.planners.stepwise_planner.stepwise_planner import StepwiseP ("I think I have everything I need.\n[FINAL ANSWER]\n 42\n\n\n", "42"), ], ) -def test_when_input_is_final_answer_returns_final_answer(input: str, expected: str): - kernel = Mock(spec=Kernel) - kernel.prompt_template_engine = Mock() +def test_when_input_is_final_answer_returns_final_answer(kernel: Kernel, input: str, expected: str): + # kernel.prompt_template_engine = Mock() planner = StepwisePlanner(kernel) result = planner.parse_result(input) @@ -39,9 +37,7 @@ def test_when_input_is_final_answer_returns_final_answer(input: str, expected: s ("My thought\n\n\n", "My thought"), ], ) -def test_when_input_is_only_thought_does_not_throw_error(input: str, expected: str): - kernel = Mock(spec=Kernel) - kernel.prompt_template_engine = Mock() +def test_when_input_is_only_thought_does_not_throw_error(kernel: Kernel, input: str, expected: str): planner = StepwisePlanner(kernel) result = planner.parse_result(input) assert result.thought == expected diff --git a/python/tests/unit/planners/test_plan_creation.py b/python/tests/unit/planners/test_plan_creation.py index 42b19d78f6..36c3db6572 100644 --- a/python/tests/unit/planners/test_plan_creation.py +++ b/python/tests/unit/planners/test_plan_creation.py @@ -1,8 +1,8 @@ # Copyright (c) Microsoft. All rights reserved. -import semantic_kernel as sk from semantic_kernel.core_plugins.math_plugin import MathPlugin from semantic_kernel.functions.kernel_arguments import KernelArguments +from semantic_kernel.kernel import Kernel from semantic_kernel.planners import Plan @@ -74,15 +74,12 @@ def test_create_plan_with_state_and_parameters(): assert plan._steps == [] -def test_create_plan_with_name_and_native_function(): - # create a kernel - kernel = sk.Kernel() - +def test_create_plan_with_name_and_native_function(kernel: Kernel): # import test (math) plugin plugin = MathPlugin() - plugin = kernel.import_plugin_from_object(plugin, "math") + kernel.add_plugin(plugin, "math") - test_function = plugin["Add"] + test_function = kernel.get_function("math", "Add") plan = Plan(name="test", function=test_function) assert plan is not None @@ -100,16 +97,12 @@ def test_create_plan_with_name_and_native_function(): assert plan._steps == [] -def test_create_multistep_plan_with_functions(): - # create a kernel - kernel = sk.Kernel() - +def test_create_multistep_plan_with_functions(kernel: Kernel): # import test (math) plugin - plugin = MathPlugin() - plugin = kernel.import_plugin_from_object(plugin, "math") + kernel.add_plugin(MathPlugin(), "math") - test_function1 = plugin["Add"] - test_function2 = plugin["Subtract"] + test_function1 = kernel.get_function("math", "Add") + test_function2 = kernel.get_function("math", "Subtract") plan = Plan(name="multistep_test") plan.add_steps([test_function1, test_function2]) @@ -129,16 +122,11 @@ def test_create_multistep_plan_with_functions(): assert len(plan._steps) == 2 -def test_create_multistep_plan_with_plans(): - # create a kernel - kernel = sk.Kernel() +def test_create_multistep_plan_with_plans(kernel: Kernel): + kernel.add_plugin(MathPlugin(), "math") - # import test (math) plugin - plugin = MathPlugin() - plugin = kernel.import_plugin_from_object(plugin, "math") - - test_function1 = plugin["Add"] - test_function2 = plugin["Subtract"] + test_function1 = kernel.get_function("math", "Add") + test_function2 = kernel.get_function("math", "Subtract") plan = Plan(name="multistep_test") plan_step1 = Plan(name="step1", function=test_function1) @@ -160,16 +148,11 @@ def test_create_multistep_plan_with_plans(): assert len(plan._steps) == 2 -def test_add_step_to_plan(): - # create a kernel - kernel = sk.Kernel() +def test_add_step_to_plan(kernel: Kernel): + kernel.add_plugin(MathPlugin(), "math") - # import test (math) plugin - plugin = MathPlugin() - plugin = kernel.import_plugin_from_object(plugin, "math") - - test_function1 = plugin["Add"] - test_function2 = plugin["Subtract"] + test_function1 = kernel.get_function("math", "Add") + test_function2 = kernel.get_function("math", "Subtract") plan = Plan(name="multistep_test", function=test_function1) plan.add_steps([test_function2]) diff --git a/python/tests/unit/planners/test_plan_execution.py b/python/tests/unit/planners/test_plan_execution.py index e313c6da8f..ea338b7431 100644 --- a/python/tests/unit/planners/test_plan_execution.py +++ b/python/tests/unit/planners/test_plan_execution.py @@ -2,30 +2,25 @@ import pytest -import semantic_kernel as sk from semantic_kernel.core_plugins.math_plugin import MathPlugin from semantic_kernel.core_plugins.text_plugin import TextPlugin from semantic_kernel.functions.kernel_arguments import KernelArguments +from semantic_kernel.kernel import Kernel from semantic_kernel.planners import Plan @pytest.mark.asyncio -async def test_invoke_empty_plan(): - kernel = sk.Kernel() +async def test_invoke_empty_plan(kernel: Kernel): plan = Plan() result = await plan.invoke(kernel) assert str(result) == "" @pytest.mark.asyncio -async def test_invoke_plan_constructed_with_function(): - # create a kernel - kernel = sk.Kernel() - +async def test_invoke_plan_constructed_with_function(kernel: Kernel): # import test (text) plugin - plugin = TextPlugin() - plugin = kernel.import_plugin_from_object(plugin, "text") - test_function = plugin["uppercase"] + kernel.add_plugin(TextPlugin(), "text") + test_function = kernel.get_function("text", "uppercase") plan = Plan(name="test", function=test_function) result = await plan.invoke(kernel, KernelArguments(input="hello world ")) @@ -33,14 +28,10 @@ async def test_invoke_plan_constructed_with_function(): @pytest.mark.asyncio -async def test_invoke_empty_plan_with_added_function_step(): - # create a kernel - kernel = sk.Kernel() - +async def test_invoke_empty_plan_with_added_function_step(kernel: Kernel): # import test (text) plugin - plugin = TextPlugin() - plugin = kernel.import_plugin_from_object(plugin, "text") - test_function = plugin["uppercase"] + kernel.add_plugin(TextPlugin(), "text") + test_function = kernel.get_function("text", "uppercase") plan = Plan(name="test") plan.add_steps([test_function]) @@ -50,14 +41,10 @@ async def test_invoke_empty_plan_with_added_function_step(): @pytest.mark.asyncio -async def test_invoke_empty_plan_with_added_plan_step(): - # create a kernel - kernel = sk.Kernel() - +async def test_invoke_empty_plan_with_added_plan_step(kernel: Kernel): # import test (text) plugin - plugin = TextPlugin() - plugin = kernel.import_plugin_from_object(plugin, "text") - test_function = plugin["uppercase"] + kernel.add_plugin(TextPlugin(), "text") + test_function = kernel.get_function("text", "uppercase") plan = Plan(name="test") new_step = Plan(name="test", function=test_function) @@ -67,15 +54,11 @@ async def test_invoke_empty_plan_with_added_plan_step(): @pytest.mark.asyncio -async def test_invoke_multi_step_plan(): - # create a kernel - kernel = sk.Kernel() - +async def test_invoke_multi_step_plan(kernel: Kernel): # import test (text) plugin - plugin = TextPlugin() - plugin = kernel.import_plugin_from_object(plugin, "text") - test_function = plugin["uppercase"] - test_function2 = plugin["trim_end"] + kernel.add_plugin(TextPlugin(), "text") + test_function = kernel.get_function("text", "uppercase") + test_function2 = kernel.get_function("text", "trim_end") plan = Plan(name="test") new_step = Plan(name="test", function=test_function) @@ -86,15 +69,11 @@ async def test_invoke_multi_step_plan(): @pytest.mark.asyncio -async def test_invoke_multi_step_plan_with_arguments(): - # create a kernel - kernel = sk.Kernel() - +async def test_invoke_multi_step_plan_with_arguments(kernel: Kernel): # import test (text) plugin - plugin = MathPlugin() - plugin = kernel.import_plugin_from_object(plugin, "math") - test_function = plugin["Add"] - test_function2 = plugin["Subtract"] + kernel.add_plugin(MathPlugin(), "math") + test_function = kernel.get_function("math", "Add") + test_function2 = kernel.get_function("math", "Subtract") plan = Plan(name="test") diff --git a/python/tests/unit/prompt_template/test_handlebars_prompt_template.py b/python/tests/unit/prompt_template/test_handlebars_prompt_template.py index 7890fb7f9a..348e4e5876 100644 --- a/python/tests/unit/prompt_template/test_handlebars_prompt_template.py +++ b/python/tests/unit/prompt_template/test_handlebars_prompt_template.py @@ -113,7 +113,7 @@ async def test_it_renders_list(kernel: Kernel): @mark.asyncio async def test_it_renders_kernel_functions_arg_from_template(kernel: Kernel, decorated_native_function): - kernel.register_function_from_method(plugin_name="plug", method=decorated_native_function) + kernel.add_function(plugin_name="plug", function=decorated_native_function) template = "Function: {{plug-getLightStatus arg1='test'}}" target = create_handlebars_prompt_template(template) @@ -123,7 +123,7 @@ async def test_it_renders_kernel_functions_arg_from_template(kernel: Kernel, dec @mark.asyncio async def test_it_renders_kernel_functions_arg_from_arguments(kernel: Kernel, decorated_native_function): - kernel.register_function_from_method(plugin_name="plug", method=decorated_native_function) + kernel.add_function(plugin_name="plug", function=decorated_native_function) template = "Function: {{plug-getLightStatus}}" target = create_handlebars_prompt_template(template) diff --git a/python/tests/unit/prompt_template/test_handlebars_prompt_template_e2e.py b/python/tests/unit/prompt_template/test_handlebars_prompt_template_e2e.py index ac0c145a28..49e74a8917 100644 --- a/python/tests/unit/prompt_template/test_handlebars_prompt_template_e2e.py +++ b/python/tests/unit/prompt_template/test_handlebars_prompt_template_e2e.py @@ -49,7 +49,7 @@ class TestHandlebarsPromptTemplateEngine: async def test_it_allows_to_pass_variables_to_functions(self, kernel: Kernel): # Arrange template = "== {{my-check123 input=call}} ==" - kernel.import_plugin_from_object(MyPlugin(), "my") + kernel.add_plugin(MyPlugin(), "my") arguments = KernelArguments(call="123") # Act @@ -62,7 +62,7 @@ class TestHandlebarsPromptTemplateEngine: async def test_it_allows_to_pass_values_to_functions(self, kernel: Kernel): # Arrange template = "== {{my-check123 input=234}} ==" - kernel.import_plugin_from_object(MyPlugin(), "my") + kernel.add_plugin(MyPlugin(), "my") # Act result = await create_handlebars_prompt_template(template).render(kernel, None) @@ -74,7 +74,7 @@ class TestHandlebarsPromptTemplateEngine: async def test_it_allows_to_pass_escaped_values1_to_functions(self, kernel: Kernel): # Arrange template = "== {{my-check123 input='a\\'b'}} ==" - kernel.import_plugin_from_object(MyPlugin(), "my") + kernel.add_plugin(MyPlugin(), "my") # Act result = await create_handlebars_prompt_template(template).render(kernel, None) @@ -85,7 +85,7 @@ class TestHandlebarsPromptTemplateEngine: async def test_it_allows_to_pass_escaped_values2_to_functions(self, kernel: Kernel): # Arrange template = '== {{my-check123 input="a\\"b"}} ==' - kernel.import_plugin_from_object(MyPlugin(), "my") + kernel.add_plugin(MyPlugin(), "my") # Act result = await create_handlebars_prompt_template(template).render(kernel, None) diff --git a/python/tests/unit/prompt_template/test_jinja2_prompt_template.py b/python/tests/unit/prompt_template/test_jinja2_prompt_template.py index ad283ade58..7e6a9d1f23 100644 --- a/python/tests/unit/prompt_template/test_jinja2_prompt_template.py +++ b/python/tests/unit/prompt_template/test_jinja2_prompt_template.py @@ -104,7 +104,7 @@ async def test_it_renders_list(kernel: Kernel): @pytest.mark.asyncio async def test_it_renders_kernel_functions_arg_from_template(kernel: Kernel, decorated_native_function): - kernel.register_function_from_method(plugin_name="plug", method=decorated_native_function) + kernel.add_function(plugin_name="plug", function=decorated_native_function) template = "Function: {{ plug_getLightStatus(arg1='test') }}" target = create_jinja2_prompt_template(template) @@ -114,7 +114,7 @@ async def test_it_renders_kernel_functions_arg_from_template(kernel: Kernel, dec @pytest.mark.asyncio async def test_it_renders_kernel_functions_arg_from_arguments(kernel: Kernel, decorated_native_function): - kernel.register_function_from_method(plugin_name="plug", method=decorated_native_function) + kernel.add_function(plugin_name="plug", function=decorated_native_function) template = "Function: {{ plug_getLightStatus() }}" target = create_jinja2_prompt_template(template) diff --git a/python/tests/unit/prompt_template/test_jinja2_prompt_template_e2e.py b/python/tests/unit/prompt_template/test_jinja2_prompt_template_e2e.py index 4da26b0828..42023c4abf 100644 --- a/python/tests/unit/prompt_template/test_jinja2_prompt_template_e2e.py +++ b/python/tests/unit/prompt_template/test_jinja2_prompt_template_e2e.py @@ -50,7 +50,7 @@ async def test_it_supports_variables(kernel: Kernel): async def test_it_allows_to_pass_variables_to_functions(kernel: Kernel): # Arrange template = "== {{ my_check123() }} ==" - kernel.import_plugin_from_object(MyPlugin(), "my") + kernel.add_plugin(MyPlugin(), "my") arguments = KernelArguments(input="123") # Act @@ -64,7 +64,7 @@ async def test_it_allows_to_pass_variables_to_functions(kernel: Kernel): async def test_it_allows_to_pass_values_to_functions(kernel: Kernel): # Arrange template = "== {{ my_check123(input=234) }} ==" - kernel.import_plugin_from_object(MyPlugin(), "my") + kernel.add_plugin(MyPlugin(), "my") # Act result = await create_jinja2_prompt_template(template).render(kernel, None) @@ -77,7 +77,7 @@ async def test_it_allows_to_pass_values_to_functions(kernel: Kernel): async def test_it_allows_to_pass_escaped_values1_to_functions(kernel: Kernel): # Arrange template = """== {{ my_check123(input="a'b") }} ==""" - kernel.import_plugin_from_object(MyPlugin(), "my") + kernel.add_plugin(MyPlugin(), "my") # Act result = await create_jinja2_prompt_template(template).render(kernel, None) @@ -89,7 +89,7 @@ async def test_it_allows_to_pass_escaped_values1_to_functions(kernel: Kernel): async def test_it_allows_to_pass_escaped_values2_to_functions(kernel: Kernel): # Arrange template = '== {{my_check123(input="a\\"b")}} ==' - kernel.import_plugin_from_object(MyPlugin(), "my") + kernel.add_plugin(MyPlugin(), "my") # Act result = await create_jinja2_prompt_template(template).render(kernel, None) diff --git a/python/tests/unit/prompt_template/test_kernel_prompt_template.py b/python/tests/unit/prompt_template/test_kernel_prompt_template.py index 32be7fe4a5..167c680a41 100644 --- a/python/tests/unit/prompt_template/test_kernel_prompt_template.py +++ b/python/tests/unit/prompt_template/test_kernel_prompt_template.py @@ -1,11 +1,8 @@ -from unittest.mock import Mock - import pytest from semantic_kernel.functions.kernel_arguments import KernelArguments from semantic_kernel.functions.kernel_function import KernelFunction from semantic_kernel.functions.kernel_function_decorator import kernel_function -from semantic_kernel.functions.kernel_plugin_collection import KernelPluginCollection from semantic_kernel.kernel import Kernel from semantic_kernel.prompt_template.input_variable import InputVariable from semantic_kernel.prompt_template.kernel_prompt_template import KernelPromptTemplate @@ -20,11 +17,6 @@ def create_kernel_prompt_template(template: str) -> KernelPromptTemplate: ) -@pytest.fixture -def plugins(): - return Mock(spec=KernelPluginCollection) - - def test_init(): template = KernelPromptTemplate( prompt_template_config=PromptTemplateConfig(name="test", description="test", template="{{$input}}") @@ -63,8 +55,7 @@ def test_extract_from_empty(): assert len(blocks) == 0 -def test_it_renders_variables(plugins): - kernel = Kernel(plugins=plugins) +def test_it_renders_variables(kernel: Kernel): arguments = KernelArguments() template = ( @@ -150,8 +141,7 @@ def test_it_renders_variables(plugins): @pytest.mark.asyncio -async def test_it_renders_code(): - kernel = Kernel() +async def test_it_renders_code(kernel: Kernel): arguments = KernelArguments() @kernel_function(name="function") @@ -160,7 +150,7 @@ async def test_it_renders_code(): func = KernelFunction.from_method(my_function, "test") assert func is not None - kernel.plugins.add_plugin_from_functions("test", [func]) + kernel.add_function("test", func) arguments["_a"] = "foo" arguments["arg"] = "bar" @@ -176,8 +166,7 @@ async def test_it_renders_code(): @pytest.mark.asyncio -async def test_it_renders_code_using_input(): - kernel = Kernel() +async def test_it_renders_code_using_input(kernel: Kernel): arguments = KernelArguments() @kernel_function(name="function") @@ -186,7 +175,7 @@ async def test_it_renders_code_using_input(): func = KernelFunction.from_method(my_function, "test") assert func is not None - kernel.plugins.add_plugin_from_functions("test", [func]) + kernel.add_function("test", func) arguments["input"] = "INPUT-BAR" template = "foo-{{test.function}}-baz" @@ -197,8 +186,7 @@ async def test_it_renders_code_using_input(): @pytest.mark.asyncio -async def test_it_renders_code_using_variables(): - kernel = Kernel() +async def test_it_renders_code_using_variables(kernel: Kernel): arguments = KernelArguments() @kernel_function(name="function") @@ -207,7 +195,7 @@ async def test_it_renders_code_using_variables(): func = KernelFunction.from_method(my_function, "test") assert func is not None - kernel.plugins.add_plugin_from_functions("test", [func]) + kernel.add_function("test", func) arguments["myVar"] = "BAR" template = "foo-{{test.function $myVar}}-baz" @@ -218,8 +206,7 @@ async def test_it_renders_code_using_variables(): @pytest.mark.asyncio -async def test_it_renders_code_using_variables_async(): - kernel = Kernel() +async def test_it_renders_code_using_variables_async(kernel: Kernel): arguments = KernelArguments() @kernel_function(name="function") @@ -228,7 +215,7 @@ async def test_it_renders_code_using_variables_async(): func = KernelFunction.from_method(my_function, "test") assert func is not None - kernel.plugins.add_plugin_from_functions("test", [func]) + kernel.add_function("test", func) arguments["myVar"] = "BAR" diff --git a/python/tests/unit/prompt_template/test_prompt_template_e2e.py b/python/tests/unit/prompt_template/test_prompt_template_e2e.py index 327c24281a..67cf056742 100644 --- a/python/tests/unit/prompt_template/test_prompt_template_e2e.py +++ b/python/tests/unit/prompt_template/test_prompt_template_e2e.py @@ -37,24 +37,23 @@ def _get_template_language_tests() -> List[Tuple[str, str]]: class MyPlugin: - @kernel_function() + @kernel_function def check123(self, input: str) -> str: return "123 ok" if input == "123" else f"{input} != 123" - @kernel_function() + @kernel_function def asis(self, input: Optional[str] = None) -> str: return input or "" class TestPromptTemplateEngine: @mark.asyncio - async def test_it_supports_variables(self): + async def test_it_supports_variables(self, kernel: Kernel): # Arrange input = "template tests" winner = "SK" template = "And the winner\n of {{$input}} \nis: {{ $winner }}!" - kernel = Kernel() arguments = KernelArguments(input=input, winner=winner) # Act result = await KernelPromptTemplate( @@ -65,12 +64,11 @@ class TestPromptTemplateEngine: assert expected == result @mark.asyncio - async def test_it_supports_values(self): + async def test_it_supports_values(self, kernel: Kernel): # Arrange template = "And the winner\n of {{'template\ntests'}} \nis: {{ \"SK\" }}!" expected = "And the winner\n of template\ntests \nis: SK!" - kernel = Kernel() # Act result = await KernelPromptTemplate( prompt_template_config=PromptTemplateConfig(name="test", description="test", template=template) @@ -80,11 +78,10 @@ class TestPromptTemplateEngine: assert expected == result @mark.asyncio - async def test_it_allows_to_pass_variables_to_functions(self): + async def test_it_allows_to_pass_variables_to_functions(self, kernel: Kernel): # Arrange template = "== {{my.check123 $call}} ==" - kernel = Kernel() - kernel.import_plugin_from_object(MyPlugin(), "my") + kernel.add_plugin(MyPlugin(), "my") arguments = KernelArguments(call="123") # Act @@ -96,11 +93,10 @@ class TestPromptTemplateEngine: assert "== 123 ok ==" == result @mark.asyncio - async def test_it_allows_to_pass_values_to_functions(self): + async def test_it_allows_to_pass_values_to_functions(self, kernel: Kernel): # Arrange template = "== {{my.check123 '234'}} ==" - kernel = Kernel() - kernel.import_plugin_from_object(MyPlugin(), "my") + kernel.add_plugin(MyPlugin(), "my") # Act result = await KernelPromptTemplate( @@ -111,11 +107,10 @@ class TestPromptTemplateEngine: assert "== 234 != 123 ==" == result @mark.asyncio - async def test_it_allows_to_pass_escaped_values1_to_functions(self): + async def test_it_allows_to_pass_escaped_values1_to_functions(self, kernel: Kernel): # Arrange template = "== {{my.check123 'a\\'b'}} ==" - kernel = Kernel() - kernel.import_plugin_from_object(MyPlugin(), "my") + kernel.add_plugin(MyPlugin(), "my") # Act result = await KernelPromptTemplate( prompt_template_config=PromptTemplateConfig(name="test", description="test", template=template) @@ -125,11 +120,10 @@ class TestPromptTemplateEngine: assert "== a'b != 123 ==" == result @mark.asyncio - async def test_it_allows_to_pass_escaped_values2_to_functions(self): + async def test_it_allows_to_pass_escaped_values2_to_functions(self, kernel: Kernel): # Arrange template = '== {{my.check123 "a\\"b"}} ==' - kernel = Kernel() - kernel.import_plugin_from_object(MyPlugin(), "my") + kernel.add_plugin(MyPlugin(), "my") # Act result = await KernelPromptTemplate( @@ -141,10 +135,9 @@ class TestPromptTemplateEngine: @mark.asyncio @mark.parametrize("template,expected_result", [(t, r) for t, r in _get_template_language_tests()]) - async def test_it_handle_edge_cases(self, template: str, expected_result: str): + async def test_it_handle_edge_cases(self, kernel: Kernel, template: str, expected_result: str): # Arrange - kernel = Kernel() - kernel.import_plugin_from_object(MyPlugin(), "my_plugin") + kernel.add_plugin(MyPlugin(), "my_plugin") # Act if expected_result.startswith("ERROR"): 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 b11a8d46c1..03c01b3e0e 100644 --- a/python/tests/unit/template_engine/blocks/test_code_block.py +++ b/python/tests/unit/template_engine/blocks/test_code_block.py @@ -13,7 +13,6 @@ from semantic_kernel.functions.kernel_arguments import KernelArguments from semantic_kernel.functions.kernel_function_decorator import kernel_function from semantic_kernel.functions.kernel_function_from_method import KernelFunctionFromMethod from semantic_kernel.functions.kernel_plugin import KernelPlugin -from semantic_kernel.functions.kernel_plugin_collection import KernelPluginCollection from semantic_kernel.kernel import Kernel from semantic_kernel.template_engine.blocks.block_types import BlockTypes from semantic_kernel.template_engine.blocks.code_block import CodeBlock @@ -35,32 +34,27 @@ def test_init(): class TestCodeBlockRendering: - def setup_method(self): - self.kernel = Kernel() - @mark.asyncio - async def test_it_throws_if_a_plugins_are_empty(self): + async def test_it_throws_if_a_plugins_are_empty(self, kernel: Kernel): target = CodeBlock( content="functionName", ) assert target.tokens[0].type == BlockTypes.FUNCTION_ID with raises(CodeBlockRenderException, match="Function `functionName` not found"): - await target.render_code(self.kernel, KernelArguments()) + await target.render_code(kernel, KernelArguments()) @mark.asyncio - async def test_it_throws_if_a_function_doesnt_exist(self): + async def test_it_throws_if_a_function_doesnt_exist(self, kernel: Kernel): target = CodeBlock( content="functionName", ) assert target.tokens[0].type == BlockTypes.FUNCTION_ID - self.kernel.plugins = KernelPluginCollection() - dkp = KernelPlugin(name="test", functions=[]) - self.kernel.plugins.add(dkp) + kernel.add_plugin(KernelPlugin(name="test", functions=[])) with raises(CodeBlockRenderException, match="Function `functionName` not found"): - await target.render_code(self.kernel, KernelArguments()) + await target.render_code(kernel, KernelArguments()) @mark.asyncio - async def test_it_throws_if_a_function_call_throws(self): + async def test_it_throws_if_a_function_call_throws(self, kernel: Kernel): @kernel_function(name="funcName") def invoke(): raise Exception("error") @@ -70,11 +64,7 @@ class TestCodeBlockRendering: plugin_name="pluginName", ) - dkp = KernelPlugin(name="test", functions=[function]) - plugins = KernelPluginCollection() - plugins.add(dkp) - kernel = Kernel() - kernel.plugins = plugins + kernel.add_plugin(KernelPlugin(name="test", functions=[function])) target = CodeBlock( content="functionName", @@ -84,25 +74,25 @@ class TestCodeBlockRendering: await target.render_code(kernel, KernelArguments()) @mark.asyncio - async def test_it_renders_code_block_consisting_of_just_a_var_block1(self): + async def test_it_renders_code_block_consisting_of_just_a_var_block1(self, kernel: Kernel): code_block = CodeBlock( content="$var", ) - result = await code_block.render_code(self.kernel, KernelArguments(var="foo")) + result = await code_block.render_code(kernel, KernelArguments(var="foo")) assert result == "foo" @mark.asyncio - async def test_it_renders_code_block_consisting_of_just_a_val_block1(self): + async def test_it_renders_code_block_consisting_of_just_a_val_block1(self, kernel: Kernel): code_block = CodeBlock( content="'ciao'", ) - result = await code_block.render_code(self.kernel, KernelArguments()) + result = await code_block.render_code(kernel, KernelArguments()) assert result == "ciao" @mark.asyncio - async def test_it_invokes_function_cloning_all_variables(self): + async def test_it_invokes_function_cloning_all_variables(self, kernel: Kernel): # Set up initial context variables arguments = KernelArguments(input="zero", var1="uno", var2="due") @@ -131,9 +121,7 @@ class TestCodeBlockRendering: plugin_name="pluginName", ) - dkp = KernelPlugin(name="test", functions=[function]) - kernel = Kernel() - kernel.plugins.add(dkp) + kernel.add_plugin(KernelPlugin(name="test", functions=[function])) # Create a CodeBlock with the FunctionIdBlock and render it with the context code_block = CodeBlock( @@ -153,7 +141,7 @@ class TestCodeBlockRendering: assert arguments["var2"] == "due" @mark.asyncio - async def test_it_invokes_function_with_custom_variable(self): + async def test_it_invokes_function_with_custom_variable(self, kernel: Kernel): # Define custom variable name and value VAR_NAME = "varName" VAR_VALUE = "varValue" @@ -183,9 +171,7 @@ class TestCodeBlockRendering: plugin_name="pluginName", ) - dkp = KernelPlugin(name="test", functions=[function]) - kernel = Kernel() - kernel.plugins.add(dkp) + kernel.add_plugin(KernelPlugin(name="test", functions=[function])) # Create a CodeBlock with the FunctionIdBlock and VarBlock, # and render it with the context @@ -201,7 +187,7 @@ class TestCodeBlockRendering: assert canary == VAR_VALUE @mark.asyncio - async def test_it_invokes_function_with_custom_value(self): + async def test_it_invokes_function_with_custom_value(self, kernel: Kernel): # Define a value to be used in the test VALUE = "value" @@ -225,9 +211,7 @@ class TestCodeBlockRendering: plugin_name="pluginName", ) - dkp = KernelPlugin(name="test", functions=[function]) - kernel = Kernel() - kernel.plugins.add(dkp) + kernel.add_plugin(KernelPlugin(name="test", functions=[function])) # Create a CodeBlock with the FunctionIdBlock and ValBlock, # and render it with the context @@ -243,7 +227,7 @@ class TestCodeBlockRendering: assert canary == VALUE @mark.asyncio - async def test_it_invokes_function_with_multiple_arguments(self): + async def test_it_invokes_function_with_multiple_arguments(self, kernel: Kernel): # Define a value to be used in the test VALUE = "value" @@ -272,9 +256,7 @@ class TestCodeBlockRendering: plugin_name="pluginName", ) - dkp = KernelPlugin(name="test", functions=[function]) - kernel = Kernel() - kernel.plugins.add(dkp) + kernel.add_plugin(KernelPlugin(name="test", functions=[function])) # Create a CodeBlock with the FunctionIdBlock and ValBlock, # and render it with the context @@ -286,7 +268,7 @@ class TestCodeBlockRendering: assert canary == f"{VALUE} arg1 arg2" @mark.asyncio - async def test_it_invokes_function_with_only_named_arguments(self): + async def test_it_invokes_function_with_only_named_arguments(self, kernel: Kernel): code_block = CodeBlock( content=" ", tokens=[ @@ -311,9 +293,7 @@ class TestCodeBlockRendering: plugin_name="pluginName", ) - dkp = KernelPlugin(name="test", functions=[function]) - kernel = Kernel() - kernel.plugins.add(dkp) + kernel.add_plugin(KernelPlugin(name="test", functions=[function])) # Create a CodeBlock with the FunctionIdBlock and ValBlock, # and render it with the context @@ -325,7 +305,7 @@ class TestCodeBlockRendering: assert canary == "arg1 arg2" @mark.asyncio - async def test_it_fails_on_function_without_args(self): + async def test_it_fails_on_function_without_args(self, kernel: Kernel): code_block = CodeBlock( content=" ", tokens=[ @@ -345,9 +325,7 @@ class TestCodeBlockRendering: plugin_name="test", ) - dkp = KernelPlugin(name="test", functions=[function]) - kernel = Kernel() - kernel.plugins.add(dkp) + kernel.add_plugin(KernelPlugin(name="test", functions=[function])) # Create a CodeBlock with the FunctionIdBlock and ValBlock, # and render it with the context diff --git a/python/tests/unit/test_serialization.py b/python/tests/unit/test_serialization.py index 308f81bbd3..fa6062fc00 100644 --- a/python/tests/unit/test_serialization.py +++ b/python/tests/unit/test_serialization.py @@ -5,9 +5,7 @@ import typing_extensions as te from pydantic import Field, Json from semantic_kernel.contents.chat_history import ChatHistory -from semantic_kernel.core_plugins.conversation_summary_plugin import ( - ConversationSummaryPlugin, -) +from semantic_kernel.core_plugins.conversation_summary_plugin import ConversationSummaryPlugin from semantic_kernel.core_plugins.http_plugin import HttpPlugin from semantic_kernel.core_plugins.math_plugin import MathPlugin from semantic_kernel.core_plugins.text_memory_plugin import TextMemoryPlugin @@ -20,9 +18,6 @@ from semantic_kernel.functions.kernel_function import KernelFunction from semantic_kernel.functions.kernel_function_decorator import kernel_function from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata from semantic_kernel.functions.kernel_parameter_metadata import KernelParameterMetadata -from semantic_kernel.functions.kernel_plugin_collection import ( - KernelPluginCollection, -) from semantic_kernel.kernel_pydantic import KernelBaseModel from semantic_kernel.memory.null_memory import NullMemory from semantic_kernel.memory.semantic_text_memory_base import SemanticTextMemoryBase @@ -70,11 +65,6 @@ def kernel_factory() -> t.Callable[[t.Type[_Serializable]], _Serializable]: def create_chat_history() -> ChatHistory: return ChatHistory() - def create_plugin_collection() -> KernelPluginCollection: - """Return a plugin collection.""" - # TODO: Add a few plugins to this collection. - return KernelPluginCollection() - cls_obj_map = { Block: Block(content="foo"), CodeBlock: CodeBlock(content="foo"), @@ -100,7 +90,6 @@ def kernel_factory() -> t.Callable[[t.Type[_Serializable]], _Serializable]: is_asynchronous=False, ), ChatHistory: create_chat_history(), - KernelPluginCollection: create_plugin_collection(), NullMemory: NullMemory(), KernelFunction: create_kernel_function(), } @@ -145,7 +134,6 @@ PYDANTIC_MODELS = [ NamedArgBlock, KernelParameterMetadata, KernelFunctionMetadata, - KernelPluginCollection, ChatHistory, pytest.param( KernelFunction,