From 756937fb2c8998d4ddfdf6f2291d89bad58e0d3f Mon Sep 17 00:00:00 2001 From: Evan Mattson <35585003+moonbox3@users.noreply.github.com> Date: Fri, 5 Jan 2024 10:31:57 -0500 Subject: [PATCH 1/5] Python: Handle errors if no event loop running in current thread. (#4485) ### Motivation and Context If invoking multiple functions, there may not be an event loop running in the current thread. The check for the event loop can throw an error, which is seen when trying to run functions in a loop, as an example. ### Description Adding a try catch to handle an errors when checking for this event loop. ### 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 - [X] I didn't break anyone :smile: Co-authored-by: Evan Mattson --- python/semantic_kernel/orchestration/sk_function.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/python/semantic_kernel/orchestration/sk_function.py b/python/semantic_kernel/orchestration/sk_function.py index 64ce94f8bb30..5e8b7b0753b2 100644 --- a/python/semantic_kernel/orchestration/sk_function.py +++ b/python/semantic_kernel/orchestration/sk_function.py @@ -366,7 +366,10 @@ def invoke( if input is not None: context.variables.update(input) - loop = asyncio.get_running_loop() if asyncio.get_event_loop().is_running() else None + try: + loop = asyncio.get_running_loop() if asyncio.get_event_loop().is_running() else None + except RuntimeError: + loop = None if loop and loop.is_running(): coroutine_function = self._invoke_semantic_async if self.is_semantic else self._invoke_native_async From 8618cd32ef9111188c07536a974cb47902e1c42f Mon Sep 17 00:00:00 2001 From: Evan Mattson <35585003+moonbox3@users.noreply.github.com> Date: Fri, 5 Jan 2024 11:35:49 -0500 Subject: [PATCH 2/5] Python: Bump Python Package version for release. Update notebook refs to use AIRequestSettings. (#4478) ### Motivation and Context Bump Python Package version for release. Update notebook refs and request settings to use the new AIRequestSettings classes. Fix hugging face bug that requires generator input as text_inputs instead of prompt. Add a hugging face request settings unit test. ### Description Bump Python Package version for release. Update notebook refs to point to new package version. ### 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 - [X] I didn't break anyone :smile: --------- Co-authored-by: Evan Mattson --- python/notebooks/00-getting-started.ipynb | 4 +- .../01-basic-loading-the-kernel.ipynb | 25 +- .../02-running-prompts-from-file.ipynb | 13 +- .../03-semantic-function-inline.ipynb | 22 +- .../notebooks/04-context-variables-chat.ipynb | 20 +- python/notebooks/05-using-the-planner.ipynb | 44 +- .../notebooks/06-memory-and-embeddings.ipynb | 1074 +++++++++-------- .../07-hugging-face-for-skills.ipynb | 4 +- .../notebooks/08-native-function-inline.ipynb | 4 +- .../notebooks/09-groundedness-checking.ipynb | 4 +- .../10-multiple-results-per-prompt.ipynb | 119 +- .../notebooks/11-streaming-completions.ipynb | 94 +- .../weaviate-persistent-memory.ipynb | 1036 ++++++++-------- python/pyproject.toml | 2 +- .../openai_logit_bias.py | 33 +- .../ai/hugging_face/hf_request_settings.py | 2 +- .../services/hf_text_completion.py | 2 +- .../open_ai_request_settings.py | 2 +- 18 files changed, 1319 insertions(+), 1185 deletions(-) diff --git a/python/notebooks/00-getting-started.ipynb b/python/notebooks/00-getting-started.ipynb index 8fe6a3374e71..db08ad662d99 100644 --- a/python/notebooks/00-getting-started.ipynb +++ b/python/notebooks/00-getting-started.ipynb @@ -16,7 +16,7 @@ "metadata": {}, "outputs": [], "source": [ - "!python -m pip install semantic-kernel==0.4.3.dev0" + "!python -m pip install semantic-kernel==0.4.4.dev0" ] }, { @@ -57,7 +57,7 @@ "\n", "api_key, org_id = sk.openai_settings_from_dot_env()\n", "\n", - "kernel.add_chat_service(\"chat-gpt\", OpenAIChatCompletion(ai_model_id=\"gpt-3.5-turbo\", api_key=api_key, org_id=org_id))" + "kernel.add_chat_service(\"chat-gpt\", OpenAIChatCompletion(ai_model_id=\"gpt-3.5-turbo-1106\", api_key=api_key, org_id=org_id))" ] }, { diff --git a/python/notebooks/01-basic-loading-the-kernel.ipynb b/python/notebooks/01-basic-loading-the-kernel.ipynb index da00f754ea09..87dbadb87fcf 100644 --- a/python/notebooks/01-basic-loading-the-kernel.ipynb +++ b/python/notebooks/01-basic-loading-the-kernel.ipynb @@ -25,17 +25,20 @@ "metadata": {}, "outputs": [], "source": [ - "!python -m pip install semantic-kernel==0.4.3.dev0" + "!python -m pip install semantic-kernel==0.4.4.dev0" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import semantic_kernel as sk\n", - "from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion, OpenAIChatCompletion" + "from semantic_kernel.connectors.ai.open_ai import (\n", + " AzureChatCompletion,\n", + " OpenAIChatCompletion,\n", + ")" ] }, { @@ -48,7 +51,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -85,13 +88,13 @@ " )\n", ")\n", "\n", - "kernel.add_chat_service( # We are adding a text service\n", - " \"OpenAI_chat_gpt\", # The alias we can use in prompt templates' config.json\n", + "kernel.add_chat_service( # We are adding a text service\n", + " \"OpenAI_chat_gpt\", # The alias we can use in prompt templates' config.json\n", " OpenAIChatCompletion(\n", - " ai_model_id=\"gpt-3.5-turbo\", # OpenAI Model Name\n", - " api_key=\"...your OpenAI API Key...\", # OpenAI API key\n", - " org_id=\"...your OpenAI Org ID...\" # *optional* OpenAI Organization ID\n", - " )\n", + " ai_model_id=\"gpt-3.5-turbo\", # OpenAI Model Name\n", + " api_key=\"...your OpenAI API Key...\", # OpenAI API key\n", + " org_id=\"...your OpenAI Org ID...\", # *optional* OpenAI Organization ID\n", + " ),\n", ")" ] }, @@ -143,7 +146,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.6" + "version": "3.10.12" }, "polyglot_notebook": { "kernelInfo": { diff --git a/python/notebooks/02-running-prompts-from-file.ipynb b/python/notebooks/02-running-prompts-from-file.ipynb index c15d38e16e7f..17519257d6c3 100644 --- a/python/notebooks/02-running-prompts-from-file.ipynb +++ b/python/notebooks/02-running-prompts-from-file.ipynb @@ -89,7 +89,7 @@ "metadata": {}, "outputs": [], "source": [ - "!python -m pip install semantic-kernel==0.4.3.dev0" + "!python -m pip install semantic-kernel==0.4.4.dev0" ] }, { @@ -100,7 +100,10 @@ "outputs": [], "source": [ "import semantic_kernel as sk\n", - "from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion, OpenAIChatCompletion\n", + "from semantic_kernel.connectors.ai.open_ai import (\n", + " AzureChatCompletion,\n", + " OpenAIChatCompletion,\n", + ")\n", "\n", "kernel = sk.Kernel()\n", "\n", @@ -109,7 +112,9 @@ "# Configure AI service used by the kernel\n", "if useAzureOpenAI:\n", " deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env()\n", - " azure_chat_service = AzureChatCompletion(deployment_name=\"turbo\", endpoint=endpoint, api_key=api_key) # set the deployment name to the value of your chat model\n", + " azure_chat_service = AzureChatCompletion(\n", + " deployment_name=\"turbo\", endpoint=endpoint, api_key=api_key\n", + " ) # set the deployment name to the value of your chat model\n", " kernel.add_chat_service(\"chat_completion\", azure_chat_service)\n", "else:\n", " api_key, org_id = sk.openai_settings_from_dot_env()\n", @@ -192,7 +197,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.6" + "version": "3.10.12" } }, "nbformat": 4, diff --git a/python/notebooks/03-semantic-function-inline.ipynb b/python/notebooks/03-semantic-function-inline.ipynb index fa633ef30a57..f5c90d8b1d2f 100644 --- a/python/notebooks/03-semantic-function-inline.ipynb +++ b/python/notebooks/03-semantic-function-inline.ipynb @@ -55,7 +55,7 @@ "metadata": {}, "outputs": [], "source": [ - "!python -m pip install semantic-kernel==0.4.3.dev0" + "!python -m pip install semantic-kernel==0.4.4.dev0" ] }, { @@ -66,7 +66,10 @@ "outputs": [], "source": [ "import semantic_kernel as sk\n", - "from semantic_kernel.connectors.ai.open_ai import AzureTextCompletion, OpenAITextCompletion\n", + "from semantic_kernel.connectors.ai.open_ai import (\n", + " AzureTextCompletion,\n", + " OpenAITextCompletion,\n", + ")\n", "\n", "kernel = sk.Kernel()\n", "\n", @@ -75,7 +78,9 @@ "# Configure AI service used by the kernel\n", "if useAzureOpenAI:\n", " deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env()\n", - " azure_text_service = AzureTextCompletion(deployment_name=\"text\", endpoint=endpoint, api_key=api_key) # set the deployment name to the value of your text model\n", + " azure_text_service = AzureTextCompletion(\n", + " deployment_name=\"text\", endpoint=endpoint, api_key=api_key\n", + " ) # set the deployment name to the value of your text model\n", " kernel.add_text_completion_service(\"dv\", azure_text_service)\n", "else:\n", " api_key, org_id = sk.openai_settings_from_dot_env()\n", @@ -196,7 +201,10 @@ "outputs": [], "source": [ "import semantic_kernel as sk\n", - "from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion, OpenAIChatCompletion\n", + "from semantic_kernel.connectors.ai.open_ai import (\n", + " AzureChatCompletion,\n", + " OpenAIChatCompletion,\n", + ")\n", "\n", "kernel = sk.Kernel()\n", "\n", @@ -205,7 +213,9 @@ "# Configure AI service used by the kernel\n", "if useAzureOpenAI:\n", " deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env()\n", - " azure_chat_service = AzureChatCompletion(deployment_name=\"turbo\", endpoint=endpoint, api_key=api_key) # set the deployment name to the value of your chat model\n", + " azure_chat_service = AzureChatCompletion(\n", + " deployment_name=\"turbo\", endpoint=endpoint, api_key=api_key\n", + " ) # set the deployment name to the value of your chat model\n", " kernel.add_chat_service(\"chat_completion\", azure_chat_service)\n", "else:\n", " api_key, org_id = sk.openai_settings_from_dot_env()\n", @@ -241,7 +251,7 @@ "\n", "summary = tldr_function(text)\n", "\n", - "print(f\"Output: {summary}\") # Output: Robots must not harm humans." + "print(f\"Output: {summary}\") # Output: Robots must not harm humans." ] } ], diff --git a/python/notebooks/04-context-variables-chat.ipynb b/python/notebooks/04-context-variables-chat.ipynb index baed381d49b1..8eff5b7f3838 100644 --- a/python/notebooks/04-context-variables-chat.ipynb +++ b/python/notebooks/04-context-variables-chat.ipynb @@ -26,7 +26,7 @@ "metadata": {}, "outputs": [], "source": [ - "!python -m pip install semantic-kernel==0.4.3.dev0" + "!python -m pip install semantic-kernel==0.4.4.dev0" ] }, { @@ -37,7 +37,10 @@ "outputs": [], "source": [ "import semantic_kernel as sk\n", - "from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion, OpenAIChatCompletion\n", + "from semantic_kernel.connectors.ai.open_ai import (\n", + " AzureChatCompletion,\n", + " OpenAIChatCompletion,\n", + ")\n", "\n", "kernel = sk.Kernel()\n", "\n", @@ -46,10 +49,16 @@ "# Configure AI service used by the kernel\n", "if useAzureOpenAI:\n", " deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env()\n", - " kernel.add_chat_service(\"chat_completion\", AzureChatCompletion(deployment_name=deployment, endpoint=endpoint, api_key=api_key))\n", + " kernel.add_chat_service(\n", + " \"chat_completion\",\n", + " AzureChatCompletion(deployment_name=deployment, endpoint=endpoint, api_key=api_key),\n", + " )\n", "else:\n", " api_key, org_id = sk.openai_settings_from_dot_env()\n", - " kernel.add_chat_service(\"gpt-3.5\", OpenAIChatCompletion(ai_model_id=\"gpt-3.5-turbo\", api_key=api_key, org_id=org_id))\n" + " kernel.add_chat_service(\n", + " \"gpt-3.5\",\n", + " OpenAIChatCompletion(ai_model_id=\"gpt-3.5-turbo\", api_key=api_key, org_id=org_id),\n", + " )" ] }, { @@ -98,7 +107,8 @@ " function_name=\"ChatBot\",\n", " max_tokens=2000,\n", " temperature=0.7,\n", - " top_p=0.5)" + " top_p=0.5,\n", + ")" ] }, { diff --git a/python/notebooks/05-using-the-planner.ipynb b/python/notebooks/05-using-the-planner.ipynb index a9f3be5bfc95..188c9dfa8464 100644 --- a/python/notebooks/05-using-the-planner.ipynb +++ b/python/notebooks/05-using-the-planner.ipynb @@ -23,7 +23,7 @@ "metadata": {}, "outputs": [], "source": [ - "!python -m pip install semantic-kernel==0.4.3.dev0" + "!python -m pip install semantic-kernel==0.4.4.dev0" ] }, { @@ -34,7 +34,10 @@ "outputs": [], "source": [ "import semantic_kernel as sk\n", - "from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion, AzureChatCompletion\n", + "from semantic_kernel.connectors.ai.open_ai import (\n", + " OpenAIChatCompletion,\n", + " AzureChatCompletion,\n", + ")\n", "\n", "kernel = sk.Kernel()\n", "\n", @@ -43,10 +46,16 @@ "# Configure AI backend used by the kernel\n", "if useAzureOpenAI:\n", " deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env()\n", - " kernel.add_chat_service(\"chat_completion\", AzureChatCompletion(deployment_name=deployment, endpoint=endpoint, api_key=api_key))\n", + " kernel.add_chat_service(\n", + " \"chat_completion\",\n", + " AzureChatCompletion(deployment_name=deployment, endpoint=endpoint, api_key=api_key),\n", + " )\n", "else:\n", " api_key, org_id = sk.openai_settings_from_dot_env()\n", - " kernel.add_chat_service(\"gpt-3.5\", OpenAIChatCompletion(ai_model_id=\"gpt-3.5-turbo\", api_key=api_key, org_id=org_id))" + " kernel.add_chat_service(\n", + " \"gpt-3.5\",\n", + " OpenAIChatCompletion(ai_model_id=\"gpt-3.5-turbo\", api_key=api_key, org_id=org_id),\n", + " )" ] }, { @@ -127,6 +136,7 @@ "outputs": [], "source": [ "from semantic_kernel.planning.basic_planner import BasicPlanner\n", + "\n", "planner = BasicPlanner()" ] }, @@ -185,7 +195,8 @@ " function_name=\"shakespeare\",\n", " skill_name=\"ShakespeareSkill\",\n", " max_tokens=2000,\n", - " temperature=0.8)" + " temperature=0.8,\n", + ")" ] }, { @@ -299,6 +310,7 @@ "outputs": [], "source": [ "from semantic_kernel.planning import SequentialPlanner\n", + "\n", "planner = SequentialPlanner(kernel)" ] }, @@ -383,6 +395,7 @@ "outputs": [], "source": [ "from semantic_kernel.planning import ActionPlanner\n", + "\n", "planner = ActionPlanner(kernel)" ] }, @@ -402,6 +415,7 @@ "outputs": [], "source": [ "from semantic_kernel.core_skills import FileIOSkill, MathSkill, TextSkill, TimeSkill\n", + "\n", "kernel.import_skill(MathSkill(), \"math\")\n", "kernel.import_skill(FileIOSkill(), \"fileIO\")\n", "kernel.import_skill(TimeSkill(), \"time\")\n", @@ -502,15 +516,17 @@ " \"\"\"\n", " A search engine skill.\n", " \"\"\"\n", + "\n", " from semantic_kernel.orchestration.sk_context import SKContext\n", - " from semantic_kernel.skill_definition import sk_function, sk_function_context_parameter\n", + " from semantic_kernel.skill_definition import (\n", + " sk_function,\n", + " sk_function_context_parameter,\n", + " )\n", "\n", " def __init__(self, connector) -> None:\n", " self._connector = connector\n", "\n", - " @sk_function(\n", - " description=\"Performs a web search for a given query\", name=\"searchAsync\"\n", - " )\n", + " @sk_function(description=\"Performs a web search for a given query\", name=\"searchAsync\")\n", " @sk_function_context_parameter(\n", " name=\"query\",\n", " description=\"The search query\",\n", @@ -564,9 +580,7 @@ "metadata": {}, "outputs": [], "source": [ - "planner = StepwisePlanner(\n", - " kernel, StepwisePlannerConfig(max_iterations=10, min_iteration_time_ms=1000)\n", - ")" + "planner = StepwisePlanner(kernel, StepwisePlannerConfig(max_iterations=10, min_iteration_time_ms=1000))" ] }, { @@ -626,10 +640,10 @@ "source": [ "for index, step in enumerate(plan._steps):\n", " print(\"Step:\", index)\n", - " print(\"Description:\",step.description)\n", + " print(\"Description:\", step.description)\n", " print(\"Function:\", step.skill_name + \".\" + step._function.name)\n", " if len(step._outputs) > 0:\n", - " print( \" Output:\\n\", str.replace(result[step._outputs[0]],\"\\n\", \"\\n \"))" + " print(\" Output:\\n\", str.replace(result[step._outputs[0]], \"\\n\", \"\\n \"))" ] } ], @@ -649,7 +663,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.6" + "version": "3.10.12" } }, "nbformat": 4, diff --git a/python/notebooks/06-memory-and-embeddings.ipynb b/python/notebooks/06-memory-and-embeddings.ipynb index 95e686b9ca95..0a89aceabaa2 100644 --- a/python/notebooks/06-memory-and-embeddings.ipynb +++ b/python/notebooks/06-memory-and-embeddings.ipynb @@ -1,538 +1,546 @@ { - "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-context-variables-chat.ipynb), we used `context variables` to pass in additional\n", - "text into prompts to enrich them with more context. This allowed us to create a basic chat experience. \n", - "\n", - "However, if you solely relied on context variables, 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. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a77bdf89", - "metadata": {}, - "outputs": [], - "source": [ - "!python -m pip install semantic-kernel==0.4.3.dev0" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "508ad44f", - "metadata": {}, - "outputs": [], - "source": [ - "from typing import Tuple\n", - "\n", - "import semantic_kernel as sk\n", - "from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion, OpenAITextEmbedding, AzureChatCompletion, AzureTextEmbedding" - ] - }, - { - "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." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8f8dcbc6", - "metadata": {}, - "outputs": [], - "source": [ - "kernel = sk.Kernel()\n", - "\n", - "useAzureOpenAI = False\n", - "\n", - "# Configure AI service used by the kernel\n", - "if useAzureOpenAI:\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(deployment_name=\"turbo\", endpoint=endpoint, api_key=api_key)\n", - " # next line assumes embeddings deployment name is \"text-embedding\", adjust the deployment name to the value of your chat model if needed \n", - " azure_text_embedding = AzureTextEmbedding(deployment_name=\"text-embedding\", endpoint=endpoint, api_key=api_key)\n", - " kernel.add_chat_service(\"chat_completion\", azure_chat_service)\n", - " kernel.add_text_embedding_generation_service(\"ada\", azure_text_embedding)\n", - "else:\n", - " api_key, org_id = sk.openai_settings_from_dot_env()\n", - " oai_chat_service = OpenAIChatCompletion(ai_model_id=\"gpt-3.5-turbo\", api_key=api_key, org_id=org_id)\n", - " oai_text_embedding = OpenAITextEmbedding(ai_model_id=\"text-embedding-ada-002\", api_key=api_key, org_id=org_id)\n", - " kernel.add_chat_service(\"chat-gpt\", oai_chat_service)\n", - " kernel.add_text_embedding_generation_service(\"ada\", oai_text_embedding)\n", - "\n", - "kernel.register_memory_store(memory_store=sk.memory.VolatileMemoryStore())\n", - "kernel.import_skill(sk.core_skills.TextMemorySkill())" - ] - }, - { - "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)." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "2a7e7ca4", - "metadata": {}, - "source": [ - "### Manually adding memories\n", - "Let's create some initial memories \"About Me\". We can add memories to our `VolatileMemoryStore` by using `SaveInformationAsync`" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d096504c", - "metadata": {}, - "outputs": [], - "source": [ - "async def populate_memory(kernel: sk.Kernel) -> None:\n", - " # Add some documents to the semantic memory\n", - " await kernel.memory.save_information_async(\n", - " collection=\"aboutMe\", id=\"info1\", text=\"My name is Andrea\"\n", - " )\n", - " await kernel.memory.save_information_async(\n", - " collection=\"aboutMe\", id=\"info2\", text=\"I currently work as a tour guide\"\n", - " )\n", - " await kernel.memory.save_information_async(\n", - " collection= \"aboutMe\", id=\"info3\", text=\"I've been living in Seattle since 2005\"\n", - " )\n", - " await kernel.memory.save_information_async(\n", - " collection=\"aboutMe\", id=\"info4\", text=\"I visited France and Italy five times since 2015\"\n", - " )\n", - " await kernel.memory.save_information_async(\n", - " collection=\"aboutMe\", id=\"info5\", text=\"My family is from New York\"\n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "await populate_memory(kernel)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "2calf857", - "metadata": {}, - "source": [ - "Let's try searching the memory:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "628c843e", - "metadata": {}, - "outputs": [], - "source": [ - "async def search_memory_examples(kernel: sk.Kernel) -> None:\n", - " questions = [\n", - " \"what's my name\",\n", - " \"where do I live?\",\n", - " \"where's my family from?\",\n", - " \"where have I traveled?\",\n", - " \"what do I do for work\",\n", - " ]\n", - "\n", - " for question in questions:\n", - " print(f\"Question: {question}\")\n", - " result = await kernel.memory.search_async(\"aboutMe\", question)\n", - " print(f\"Answer: {result[0].text}\\n\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "await search_memory_examples(kernel)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "e70c2b22", - "metadata": {}, - "source": [ - "Let's now revisit the our chat sample from the [previous notebook](04-context-variables-chat.ipynb).\n", - "If you remember, we used context variables to fill the prompt with a `history` that continuously got populated as we chatted with the bot. Let's add also memory to it!" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "1ed54a32", - "metadata": {}, - "source": [ - "This is done by using the `TextMemorySkill` 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. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fb8549b2", - "metadata": {}, - "outputs": [], - "source": [ - "async def setup_chat_with_memory(\n", - " kernel: sk.Kernel,\n", - ") -> Tuple[sk.SKFunctionBase, sk.SKContext]:\n", - " sk_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", - " - {{$fact1}} {{recall $fact1}}\n", - " - {{$fact2}} {{recall $fact2}}\n", - " - {{$fact3}} {{recall $fact3}}\n", - " - {{$fact4}} {{recall $fact4}}\n", - " - {{$fact5}} {{recall $fact5}}\n", - "\n", - " Chat:\n", - " {{$chat_history}}\n", - " User: {{$user_input}}\n", - " ChatBot: \"\"\".strip()\n", - "\n", - " chat_func = kernel.create_semantic_function(sk_prompt, max_tokens=200, temperature=0.8)\n", - "\n", - " context = kernel.create_new_context()\n", - " context[\"fact1\"] = \"what is my name?\"\n", - " context[\"fact2\"] = \"where do I live?\"\n", - " context[\"fact3\"] = \"where's my family from?\"\n", - " context[\"fact4\"] = \"where have I traveled?\"\n", - " context[\"fact5\"] = \"what do I do for work?\"\n", - "\n", - " context[sk.core_skills.TextMemorySkill.COLLECTION_PARAM] = \"aboutMe\"\n", - " context[sk.core_skills.TextMemorySkill.RELEVANCE_PARAM] = \"0.8\"\n", - "\n", - " context[\"chat_history\"] = \"\"\n", - "\n", - " return chat_func, context" - ] - }, - { - "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." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "645b55a1", - "metadata": {}, - "source": [ - "Now that we've included our memories, let's chat!" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "75267a2f", - "metadata": {}, - "outputs": [], - "source": [ - "async def chat(\n", - " kernel: sk.Kernel, chat_func: sk.SKFunctionBase, context: sk.SKContext\n", - ") -> bool:\n", - " try:\n", - " user_input = input(\"User:> \")\n", - " context[\"user_input\"] = user_input\n", - " print(f\"User:> {user_input}\")\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.run_async(chat_func, input_vars=context.variables)\n", - " context[\"chat_history\"] += f\"\\nUser:> {user_input}\\nChatBot:> {answer}\\n\"\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(kernel)\n", - "\n", - "print(\"Asking questions... (manually)\")\n", - "await search_memory_examples(kernel)\n", - "\n", - "print(\"Setting up a chat (with memory!)\")\n", - "chat_func, context = await setup_chat_with_memory(kernel)\n", - "\n", - "print(\"Begin chatting (type 'exit' to exit):\\n\")\n", - "chatting = True\n", - "while chatting:\n", - " chatting = await chat(kernel, chat_func, context)" - ] - }, - { - "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." - ] - }, - { - "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", - "github_files[\"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 skill 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", - "github_files[\"https://github.com/microsoft/semantic-kernel/tree/main/samples/skills/ChatSkill/ChatGPT\"] = \\\n", - " \"Sample demonstrating how to create a chat skill interfacing with ChatGPT\"\n", - "github_files[\"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." - ] - }, - { - "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 kernel.memory.save_reference_async(\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 kernel.memory.search_async(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, - "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", - "#text-embedding-ada-002 uses a 1536-dimensional embedding vector\n", - "kernel.register_memory_store(\n", - " 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" - ] - }, - { - "cell_type": "markdown", - "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" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "await populate_memory(kernel)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can see that our function created an \"About Me\" index and that our five pieces of information have been indexed (note that it can take a few minutes for the UI to reflect the document count and storage size)." - ] - }, - { - "attachments": { - "image.png": { - "image/png": "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" - } - }, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "![image.png](attachment:image.png)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "And we can see that embeddings have been conveniently created to allow for semantic search." - ] - }, - { - "attachments": { - "image.png": { - "image/png": "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zRXY884GzG7D5Z0G3fF/9ODpmoGJt+rzEnLTA9jk24DF8ji+cAnnz14Sx3ALJi/agB0xbqmakM8NQ+G/rtOvNvCXE1j7nUURld8/nxevWb5u8bPyZ3k5OP6sEBERJSwlKKjHRERERESO0+5XCSAiIiIiak8MrERERETkaAysRERERORoDKxERERE5GgMrERERETkaAysRERERORoDKxERERE5GgMrERERETkaAysRERERORoDKxERERE5GgMrERERETkaAysRERERORoDKxERERE5GgMrERERETkaAysRERERORoDKxERERE5GgMrERERETkaAysRERERORoDKxERERE5GgMrERERETkaAysRERERORoDKxERERE5GgMrERERETkaAysRERERORoDKxERERE5GgMrERERETkaAysRERERORoDKxERERE5GgMrERERETkaAysRERERORoDKxERERE5GhXH1jPH0V50SSMG5mNYUPFNHIM5m49rxZ2lv1YIve9aL9qdyUnHUtHaEB5vvqsQ9M0lH8MnFo/DcPyX8Qp9cx4tOfGeY9aW95l9i2yvfZF4hMP0447znJNxDayMX19g1qo2J/TFe9FJx1DW743jhJ6fxbhNctr0H9GluzTGklK/R4LTVG+xw7kzO9S+Hfm9PWvau+rk74bSfvzd4X2L4ry+1bTHX5u2+DjFzFd+9mW/37rP+/Ga3fsv7/Bq/H/KoNPjckKDnXnB7+/4WfBn1X8LPh62cLg4pfq1RM6y9vBxdniOBa+rdpdyUnH0hHqgy9OzQoufks1r1BDWX7c96i15V3h7YXic81eKD5hk7fE9129F7GWDxXfB8v7JedNXR9sUM3gyfXBaabnaK/dvFx4u8za7mhOOAZnkz/n+cEXT6qmRfv8jHQZ7ftoe21vrY/xWruW/JmbVtbZ/960jdN+lyXDe9ahtN/Jtt/Tkva9jzK/DZLnvY3/O8qJ//5KV1dhPVWDD88D4/5xA576h8mYPGUyZj+8As99K1M9gah7kH9xzm2Yhz01KzBRzdPcJb7vd4n/71uEuXsnY22U5SdWT8ae8jgVjJsewkOTxN/3jfpf/Q0NJzD0zokYpLV0Ex9+yNLuaE44Bkf7uAEN4t0YdJNqdycN9ajJvhMTza/trodQ2B1fayfQfpYy+ZPjGHd5IH4jY7+tknrqnbdRM8lj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4PLly2oOEREREVHHuKLASkRERETUWTiGlYiIiIgcjYGViIiIiByNgZWIiIiIHI2BlYiIiIgcjYGViIiIiByNgZWIiIiIHI2BlYiIiIgcjYGViIiIiByNgZWIiIiIHI2BlYiIiIgcjYGViIiIiByNgZWIiIiIHI2BlYiIiIgcjYGViIiIiByNgZWIiIiIHI2BlYiIiIgcjYGViIiIiByNgZWIiIiIHI2BlYiIiIgcjYGViIiIiByNgZWIiIiIHI2BlYiIiIgcjYGViIiIiBwtCQLru/ieuwA/rldNIiIiIupRrjCwNuLHBV/DSLdpKnhJzO0E+5d23r6IiIiIqMtdRYU1G0VvfIhjfn1anuHF937EGElERERE7avdhgRMGH836hpPqRbQ+KOCcPV16btqrlD/EmaHKrNL8Z4xz1I1lRXcyGEA2jYffwuo82KGWP97++VcOWTA2B6HDhARERF1N+0UWEXAfPkt3DX+dr25fylmvHc7thnVVyzCbK36KsLlve/i9lBldhkm6GskJOM7m3Hs2buBrGJt289MlMF2EfCssb3N+NZg9WQiIiIi6hauIrDWouzecKUUT8kAKeer8Prgt5GhPc9cfR2EQVm1ONVuIwcykCF20tjEoQhERERE3VW7jGFdfpcIry+buv2FfY8bYVZMshu/sVFE2Qx8a/MKQC3Tu/SvzoRlO3D7e9O07elVXCIiIiLqTtplSMCEZStw176XTONHrSdkadNmo+J6O57R5sng2h5jTmUIltvTg2t7hGAiIiIico52GsMqQuizGSj7gTxxKgMTJkA9jsc0PGBwBjLq3sV7Krw2/mgpyur0x4nj8AAiIiKi7qidAqsw8Tsoghczlr6rnRwlL3Mlz+Q3hgVEntE/De9O2KHGvd6Obz8sQq4aE/s9fBtFWXJ+FMZ+tG1arwe7GCuw6TvGyFkiIiIi6g5SgoJ6TERERETkOO1XYSUiIiIi6gAMrERERETkaAysRERERORoDKxERERE5GgMrERERETkaAysRERERORoDKxERERE5GhXdB3WM2fOqEdERERERB3rigNr//43oqWlRc3pWrXvH8ZWZOIHt96o5nS8Q4cOY+zYMarVMf5w6BU04FaknjyFL31rMq5X83HuMH71/MdIfewBpMuZJ/bgl68fxefvmouvjk3Tn6M5gdrvvY8vGs9T5HZrT96E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" - } - }, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "![image.png](attachment:image.png)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's now try to query from Azure AI Search!" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "await search_memory_examples(kernel)" - ] - }, - { - "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." - ] + "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-context-variables-chat.ipynb), we used `context variables` to pass in additional\n", + "text into prompts to enrich them with more context. This allowed us to create a basic chat experience. \n", + "\n", + "However, if you solely relied on context variables, 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. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a77bdf89", + "metadata": {}, + "outputs": [], + "source": [ + "!python -m pip install semantic-kernel==0.4.4.dev0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "508ad44f", + "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", + " AzureChatCompletion,\n", + " AzureTextEmbedding,\n", + ")" + ] + }, + { + "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." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8f8dcbc6", + "metadata": {}, + "outputs": [], + "source": [ + "kernel = sk.Kernel()\n", + "\n", + "useAzureOpenAI = False\n", + "\n", + "# Configure AI service used by the kernel\n", + "if useAzureOpenAI:\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(deployment_name=\"turbo\", endpoint=endpoint, api_key=api_key)\n", + " # next line assumes embeddings deployment name is \"text-embedding\", adjust the deployment name to the value of your chat model if needed\n", + " azure_text_embedding = AzureTextEmbedding(deployment_name=\"text-embedding\", endpoint=endpoint, api_key=api_key)\n", + " kernel.add_chat_service(\"chat_completion\", azure_chat_service)\n", + " kernel.add_text_embedding_generation_service(\"ada\", azure_text_embedding)\n", + "else:\n", + " api_key, org_id = sk.openai_settings_from_dot_env()\n", + " oai_chat_service = OpenAIChatCompletion(ai_model_id=\"gpt-3.5-turbo\", api_key=api_key, org_id=org_id)\n", + " oai_text_embedding = OpenAITextEmbedding(ai_model_id=\"text-embedding-ada-002\", api_key=api_key, org_id=org_id)\n", + " kernel.add_chat_service(\"chat-gpt\", oai_chat_service)\n", + " kernel.add_text_embedding_generation_service(\"ada\", oai_text_embedding)\n", + "\n", + "kernel.register_memory_store(memory_store=sk.memory.VolatileMemoryStore())\n", + "kernel.import_skill(sk.core_skills.TextMemorySkill())" + ] + }, + { + "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)." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "2a7e7ca4", + "metadata": {}, + "source": [ + "### Manually adding memories\n", + "Let's create some initial memories \"About Me\". We can add memories to our `VolatileMemoryStore` by using `SaveInformationAsync`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d096504c", + "metadata": {}, + "outputs": [], + "source": [ + "async def populate_memory(kernel: sk.Kernel) -> None:\n", + " # Add some documents to the semantic memory\n", + " await kernel.memory.save_information_async(collection=\"aboutMe\", id=\"info1\", text=\"My name is Andrea\")\n", + " await kernel.memory.save_information_async(\n", + " collection=\"aboutMe\", id=\"info2\", text=\"I currently work as a tour guide\"\n", + " )\n", + " await kernel.memory.save_information_async(\n", + " collection=\"aboutMe\", id=\"info3\", text=\"I've been living in Seattle since 2005\"\n", + " )\n", + " await kernel.memory.save_information_async(\n", + " collection=\"aboutMe\",\n", + " id=\"info4\",\n", + " text=\"I visited France and Italy five times since 2015\",\n", + " )\n", + " await kernel.memory.save_information_async(collection=\"aboutMe\", id=\"info5\", text=\"My family is from New York\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "await populate_memory(kernel)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "2calf857", + "metadata": {}, + "source": [ + "Let's try searching the memory:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "628c843e", + "metadata": {}, + "outputs": [], + "source": [ + "async def search_memory_examples(kernel: sk.Kernel) -> None:\n", + " questions = [\n", + " \"what's my name\",\n", + " \"where do I live?\",\n", + " \"where's my family from?\",\n", + " \"where have I traveled?\",\n", + " \"what do I do for work\",\n", + " ]\n", + "\n", + " for question in questions:\n", + " print(f\"Question: {question}\")\n", + " result = await kernel.memory.search_async(\"aboutMe\", question)\n", + " print(f\"Answer: {result[0].text}\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "await search_memory_examples(kernel)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "e70c2b22", + "metadata": {}, + "source": [ + "Let's now revisit the our chat sample from the [previous notebook](04-context-variables-chat.ipynb).\n", + "If you remember, we used context variables to fill the prompt with a `history` that continuously got populated as we chatted with the bot. Let's add also memory to it!" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "1ed54a32", + "metadata": {}, + "source": [ + "This is done by using the `TextMemorySkill` 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. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fb8549b2", + "metadata": {}, + "outputs": [], + "source": [ + "async def setup_chat_with_memory(\n", + " kernel: sk.Kernel,\n", + ") -> Tuple[sk.SKFunctionBase, sk.SKContext]:\n", + " sk_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", + " - {{$fact1}} {{recall $fact1}}\n", + " - {{$fact2}} {{recall $fact2}}\n", + " - {{$fact3}} {{recall $fact3}}\n", + " - {{$fact4}} {{recall $fact4}}\n", + " - {{$fact5}} {{recall $fact5}}\n", + "\n", + " Chat:\n", + " {{$chat_history}}\n", + " User: {{$user_input}}\n", + " ChatBot: \"\"\".strip()\n", + "\n", + " chat_func = kernel.create_semantic_function(sk_prompt, max_tokens=200, temperature=0.8)\n", + "\n", + " context = kernel.create_new_context()\n", + " context[\"fact1\"] = \"what is my name?\"\n", + " context[\"fact2\"] = \"where do I live?\"\n", + " context[\"fact3\"] = \"where's my family from?\"\n", + " context[\"fact4\"] = \"where have I traveled?\"\n", + " context[\"fact5\"] = \"what do I do for work?\"\n", + "\n", + " context[sk.core_skills.TextMemorySkill.COLLECTION_PARAM] = \"aboutMe\"\n", + " context[sk.core_skills.TextMemorySkill.RELEVANCE_PARAM] = \"0.8\"\n", + "\n", + " context[\"chat_history\"] = \"\"\n", + "\n", + " return chat_func, context" + ] + }, + { + "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." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "645b55a1", + "metadata": {}, + "source": [ + "Now that we've included our memories, let's chat!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "75267a2f", + "metadata": {}, + "outputs": [], + "source": [ + "async def chat(kernel: sk.Kernel, chat_func: sk.SKFunctionBase, context: sk.SKContext) -> bool:\n", + " try:\n", + " user_input = input(\"User:> \")\n", + " context[\"user_input\"] = user_input\n", + " print(f\"User:> {user_input}\")\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.run_async(chat_func, input_vars=context.variables)\n", + " context[\"chat_history\"] += f\"\\nUser:> {user_input}\\nChatBot:> {answer}\\n\"\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(kernel)\n", + "\n", + "print(\"Asking questions... (manually)\")\n", + "await search_memory_examples(kernel)\n", + "\n", + "print(\"Setting up a chat (with memory!)\")\n", + "chat_func, context = await setup_chat_with_memory(kernel)\n", + "\n", + "print(\"Begin chatting (type 'exit' to exit):\\n\")\n", + "chatting = True\n", + "while chatting:\n", + " chatting = await chat(kernel, chat_func, context)" + ] + }, + { + "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." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c3d5a1b9", + "metadata": {}, + "outputs": [], + "source": [ + "github_files = {}\n", + "github_files[\n", + " \"https://github.com/microsoft/semantic-kernel/blob/main/README.md\"\n", + "] = \"README: Installation, getting started, and how to contribute\"\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 skill or function\"\n", + "github_files[\n", + " \"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", + "github_files[\n", + " \"https://github.com/microsoft/semantic-kernel/tree/main/samples/skills/ChatSkill/ChatGPT\"\n", + "] = \"Sample demonstrating how to create a chat skill interfacing with ChatGPT\"\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." + ] + }, + { + "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 kernel.memory.save_reference_async(\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 kernel.memory.search_async(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, + "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", + "# text-embedding-ada-002 uses a 1536-dimensional embedding vector\n", + "kernel.register_memory_store(\n", + " memory_store=AzureCognitiveSearchMemoryStore(\n", + " vector_size=1536,\n", + " search_endpoint=azure_ai_search_url,\n", + " admin_key=azure_ai_search_api_key,\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "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" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "await populate_memory(kernel)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can see that our function created an \"About Me\" index and that our five pieces of information have been indexed (note that it can take a few minutes for the UI to reflect the document count and storage size)." + ] + }, + { + "attachments": { + "image.png": { + "image/png": "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" } - ], - "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" + }, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![image.png](attachment:image.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And we can see that embeddings have been conveniently created to allow for semantic search." + ] + }, + { + "attachments": { + "image.png": { + "image/png": "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zRXY884GzG7D5Z0G3fF/9ODpmoGJt+rzEnLTA9jk24DF8ji+cAnnz14Sx3ALJi/agB0xbqmakM8NQ+G/rtOvNvCXE1j7nUURld8/nxevWb5u8bPyZ3k5OP6sEBERJSwlKKjHRERERESO0+5XCSAiIiIiak8MrERERETkaAysRERERORoDKxERERE5GgMrERERETkaAysRERERORoDKxERERE5GgMrERERETkaAysRERERORoDKxERERE5GgMrERERETkaAysRERERORoDKxERERE5GgMrERERETkaAysRERERORoDKxERERE5GgMrERERETkaAysRERERORoDKxERERE5GgMrERERETkaAysRERERORoDKxERERE5GgMrERERETkaAysRERERORoDKxERERE5GgMrERERETkaAysRERERORoDKxERERE5GgMrERERETkaAysRERERORoDKxERERE5GhXH1jPH0V50SSMG5mNYUPFNHIM5m49rxZ2lv1YIve9aL9qdyUnHUtHaEB5vvqsQ9M0lH8MnFo/DcPyX8Qp9cx4tOfGeY9aW95l9i2yvfZF4hMP0447znJNxDayMX19g1qo2J/TFe9FJx1DW743jhJ6fxbhNctr0H9GluzTGklK/R4LTVG+xw7kzO9S+Hfm9PWvau+rk74bSfvzd4X2L4ry+1bTHX5u2+DjFzFd+9mW/37rP+/Ga3fsv7/Bq/H/KoNPjckKDnXnB7+/4WfBn1X8LPh62cLg4pfq1RM6y9vBxdniOBa+rdpdyUnH0hHqgy9OzQoufks1r1BDWX7c96i15V3h7YXic81eKD5hk7fE9129F7GWDxXfB8v7JedNXR9sUM3gyfXBaabnaK/dvFx4u8za7mhOOAZnkz/n+cEXT6qmRfv8jHQZ7ftoe21vrY/xWruW/JmbVtbZ/960jdN+lyXDe9ahtN/Jtt/Tkva9jzK/DZLnvY3/O8qJ//5KV1dhPVWDD88D4/5xA576h8mYPGUyZj+8As99K1M9gah7kH9xzm2Yhz01KzBRzdPcJb7vd4n/71uEuXsnY22U5SdWT8ae8jgVjJsewkOTxN/3jfpf/Q0NJzD0zokYpLV0Ex9+yNLuaE44Bkf7uAEN4t0YdJNqdycN9ajJvhMTza/trodQ2B1fayfQfpYy+ZPjGHd5IH4jY7+tknrqnbdRM8lj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4PLly2oOEREREVHHuKLASkRERETUWTiGlYiIiIgcjYGViIiIiByNgZWIiIiIHI2BlYiIiIgcjYGViIiIiByNgZWIiIiIHI2BlYiIiIgcjYGViIiIiByNgZWIiIiIHI2BlYiIiIgcjYGViIiIiByNgZWIiIiIHI2BlYiIiIgcjYGViIiIiByNgZWIiIiIHI2BlYiIiIgcjYGViIiIiByNgZWIiIiIHI2BlYiIiIgcjYGViIiIiByNgZWIiIiIHI2BlYiIiIgcjYGViIiIiBwtCQLru/ieuwA/rldNIiIiIupRrjCwNuLHBV/DSLdpKnhJzO0E+5d23r6IiIiIqMtdRYU1G0VvfIhjfn1anuHF937EGElERERE7avdhgRMGH836hpPqRbQ+KOCcPV16btqrlD/EmaHKrNL8Z4xz1I1lRXcyGEA2jYffwuo82KGWP97++VcOWTA2B6HDhARERF1N+0UWEXAfPkt3DX+dr25fylmvHc7thnVVyzCbK36KsLlve/i9lBldhkm6GskJOM7m3Hs2buBrGJt289MlMF2EfCssb3N+NZg9WQiIiIi6hauIrDWouzecKUUT8kAKeer8Prgt5GhPc9cfR2EQVm1ONVuIwcykCF20tjEoQhERERE3VW7jGFdfpcIry+buv2FfY8bYVZMshu/sVFE2Qx8a/MKQC3Tu/SvzoRlO3D7e9O07elVXCIiIiLqTtplSMCEZStw176XTONHrSdkadNmo+J6O57R5sng2h5jTmUIltvTg2t7hGAiIiIico52GsMqQuizGSj7gTxxKgMTJkA9jsc0PGBwBjLq3sV7Krw2/mgpyur0x4nj8AAiIiKi7qidAqsw8Tsoghczlr6rnRwlL3Mlz+Q3hgVEntE/De9O2KHGvd6Obz8sQq4aE/s9fBtFWXJ+FMZ+tG1arwe7GCuw6TvGyFkiIiIi6g5SgoJ6TERERETkOO1XYSUiIiIi6gAMrERERETkaAysRERERORoDKxERERE5GgMrERERETkaAysRERERORoDKxERERE5GhXdB3WM2fOqEdERERERB3rigNr//43oqWlRc3pWrXvH8ZWZOIHt96o5nS8Q4cOY+zYMarVMf5w6BU04FaknjyFL31rMq5X83HuMH71/MdIfewBpMuZJ/bgl68fxefvmouvjk3Tn6M5gdrvvY8vGs9T5HZrT96E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" } + }, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![image.png](attachment:image.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's now try to query from Azure AI Search!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "await search_memory_examples(kernel)" + ] + }, + { + "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." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" }, - "nbformat": 4, - "nbformat_minor": 5 + "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-skills.ipynb b/python/notebooks/07-hugging-face-for-skills.ipynb index d49f171b583e..c4ce3fb2d49a 100644 --- a/python/notebooks/07-hugging-face-for-skills.ipynb +++ b/python/notebooks/07-hugging-face-for-skills.ipynb @@ -20,7 +20,7 @@ "metadata": {}, "outputs": [], "source": [ - "!python -m pip install semantic-kernel==0.4.3.dev0\n", + "!python -m pip install semantic-kernel==0.4.4.dev0\n", "\n", "# Note that additional dependencies are required for the Hugging Face connectors:\n", "!python -m pip install torch==2.0.0\n", @@ -184,7 +184,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.6" + "version": "3.10.12" } }, "nbformat": 4, diff --git a/python/notebooks/08-native-function-inline.ipynb b/python/notebooks/08-native-function-inline.ipynb index ced7bd6f9317..588cde44659a 100644 --- a/python/notebooks/08-native-function-inline.ipynb +++ b/python/notebooks/08-native-function-inline.ipynb @@ -46,7 +46,7 @@ "metadata": {}, "outputs": [], "source": [ - "!python -m pip install semantic-kernel==0.4.3.dev0" + "!python -m pip install semantic-kernel==0.4.4.dev0" ] }, { @@ -576,7 +576,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.6" + "version": "3.10.12" } }, "nbformat": 4, diff --git a/python/notebooks/09-groundedness-checking.ipynb b/python/notebooks/09-groundedness-checking.ipynb index 30a0c6ca80f0..cf9cbeed18c2 100644 --- a/python/notebooks/09-groundedness-checking.ipynb +++ b/python/notebooks/09-groundedness-checking.ipynb @@ -81,7 +81,7 @@ "metadata": {}, "outputs": [], "source": [ - "!python -m pip install semantic-kernel==0.4.3.dev0" + "!python -m pip install semantic-kernel==0.4.4.dev0" ] }, { @@ -352,7 +352,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.3" + "version": "3.10.12" } }, "nbformat": 4, diff --git a/python/notebooks/10-multiple-results-per-prompt.ipynb b/python/notebooks/10-multiple-results-per-prompt.ipynb index 082078b62cc2..5ad8b068e24e 100644 --- a/python/notebooks/10-multiple-results-per-prompt.ipynb +++ b/python/notebooks/10-multiple-results-per-prompt.ipynb @@ -25,7 +25,7 @@ "metadata": {}, "outputs": [], "source": [ - "!python -m pip install semantic-kernel==0.4.3.dev0" + "!python -m pip install semantic-kernel==0.4.4.dev0" ] }, { @@ -36,7 +36,13 @@ "outputs": [], "source": [ "import semantic_kernel as sk\n", - "from semantic_kernel.connectors.ai import ChatRequestSettings, CompleteRequestSettings\n", + "from semantic_kernel.connectors.ai.open_ai.request_settings.open_ai_request_settings import (\n", + " OpenAITextRequestSettings,\n", + " OpenAIChatRequestSettings,\n", + ")\n", + "from semantic_kernel.connectors.ai.open_ai.request_settings.azure_chat_request_settings import (\n", + " AzureChatRequestSettings\n", + ")\n", "from semantic_kernel.connectors.ai.open_ai import AzureTextCompletion, AzureChatCompletion, OpenAITextCompletion, OpenAIChatCompletion\n", "from semantic_kernel.connectors.ai.hugging_face import HuggingFaceTextCompletion" ] @@ -61,9 +67,8 @@ "\n", "# Configure Azure LLM service\n", "deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env()\n", - "azure_text_service = AzureTextCompletion(deployment_name=\"text\", endpoint=endpoint, api_key=api_key) # set the deployment name to the value of your text model\n", - "azure_chat_service = AzureChatCompletion(deployment_name=\"turbo\", endpoint=endpoint, api_key=api_key) # set the deployment name to the value of your chat model\n", - "\n", + "azure_text_service = AzureTextCompletion(deployment_name=\"text-davinci-003\", endpoint=endpoint, api_key=api_key) # set the deployment name to the value of your text model\n", + "azure_chat_service = AzureChatCompletion(deployment_name=\"gpt-35-turbo\", endpoint=endpoint, api_key=api_key) # set the deployment name to the value of your chat model\n", "\n", "# Configure OpenAI service\n", "api_key, org_id = sk.openai_settings_from_dot_env()\n", @@ -90,14 +95,7 @@ "metadata": {}, "outputs": [], "source": [ - "request_settings = CompleteRequestSettings(\n", - " max_tokens=80,\n", - " temperature=0.7,\n", - " top_p=1,\n", - " frequency_penalty=0.5,\n", - " presence_penalty=0.5,\n", - " number_of_responses=3\n", - ")" + "oai_text_request_settings = OpenAITextRequestSettings(extension_data = { \"max_tokens\": 80, \"temperature\": 0.7, \"top_p\": 1, \"frequency_penalty\": 0.5, \"presence_penalty\": 0.5, \"number_of_responses\": 3})" ] }, { @@ -117,7 +115,7 @@ "outputs": [], "source": [ "prompt = \"what is the purpose of a rubber duck?\"\n", - "results = await oai_text_service.complete_async(prompt=prompt, settings=request_settings)\n", + "results = await oai_text_service.complete_async(prompt=prompt, settings=oai_text_request_settings)\n", "i = 1\n", "for result in results:\n", " print(f\"Result {i}: {result}\")\n", @@ -141,7 +139,7 @@ "outputs": [], "source": [ "prompt = \"provide me a list of possible meanings for the acronym 'ORLD'\"\n", - "results = await azure_text_service.complete_async(prompt=prompt, settings=request_settings)\n", + "results = await azure_text_service.complete_async(prompt=prompt, settings=oai_text_request_settings)\n", "i = 1\n", "for result in results:\n", " print(f\"Result {i}: {result}\")\n", @@ -157,6 +155,20 @@ "## Multiple Hugging Face Text Completions" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "4a148709", + "metadata": {}, + "outputs": [], + "source": [ + "from semantic_kernel.connectors.ai.hugging_face.hf_request_settings import (\n", + " HuggingFaceRequestSettings,\n", + ")\n", + "\n", + "hf_request_settings = HuggingFaceRequestSettings(extension_data = { \"max_new_tokens\": 80, \"temperature\": 0.7, \"top_p\": 1})" + ] + }, { "cell_type": "code", "execution_count": null, @@ -165,11 +177,8 @@ "outputs": [], "source": [ "prompt = \"The purpose of a rubber duck is\"\n", - "results = await hf_text_service.complete_async(prompt=prompt, request_settings=request_settings)\n", - "i = 1\n", - "for result in results:\n", - " print(f\"Result {i}: {result}\")\n", - " i += 1" + "results = await hf_text_service.complete_async(prompt=prompt, request_settings=hf_request_settings)\n", + "print(\"\".join(results))" ] }, { @@ -188,7 +197,7 @@ "metadata": {}, "outputs": [], "source": [ - "chat_request_settings = ChatRequestSettings(\n", + "oai_chat_request_settings = OpenAIChatRequestSettings(\n", " max_tokens=80,\n", " temperature=0.7,\n", " top_p=1,\n", @@ -217,10 +226,10 @@ "role = \"user\"\n", "content = \"It's a beautiful day outside, birds are singing, flowers are blooming. On days like these, kids like you...\"\n", "message = { \"role\":role, \"content\":content }\n", - "results = await oai_chat_service.complete_chat_async(messages=[message], settings=chat_request_settings)\n", + "results = await oai_chat_service.complete_chat_async(messages=[message], settings=oai_chat_request_settings)\n", "i = 0\n", "for result in results:\n", - " print(f\"Result {i}: {result}\")\n", + " print(f\"Result {i}: {result[0]}\")\n", " i += 1" ] }, @@ -233,6 +242,23 @@ "## Multiple Azure OpenAI Chat Completions" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "66ba4767", + "metadata": {}, + "outputs": [], + "source": [ + "az_oai_request_settings = AzureChatRequestSettings(\n", + " max_tokens=80,\n", + " temperature=0.7,\n", + " top_p=1,\n", + " frequency_penalty=0.5,\n", + " presence_penalty=0.5,\n", + " number_of_responses=3\n", + ")" + ] + }, { "cell_type": "code", "execution_count": null, @@ -243,10 +269,10 @@ "role = \"user\"\n", "content = \"Tomorow is going to be a great day, I can feel it. I'm going to wake up early, go for a run, and then...\"\n", "message = { \"role\":role, \"content\":content }\n", - "results = await azure_chat_service.complete_chat_async(messages=[message], settings=chat_request_settings)\n", + "results = await azure_chat_service.complete_chat_async(messages=[message], settings=az_oai_request_settings)\n", "i = 0\n", "for result in results:\n", - " print(f\"Result {i}: {result}\")\n", + " print(f\"Result {i}: {result[0]}\")\n", " i += 1" ] }, @@ -270,24 +296,39 @@ "source": [ "import os\n", "from IPython.display import clear_output\n", + "import time\n", "\n", - "if os.name == \"nt\":\n", - " clear = \"cls\"\n", - "else:\n", - " clear = \"clear\"\n", + "# Determine the clear command based on OS\n", + "clear_command = \"cls\" if os.name == \"nt\" else \"clear\"\n", "\n", "prompt = \"what is the purpose of a rubber duck?\"\n", - "stream = oai_text_service.complete_stream_async(prompt=prompt, settings=request_settings)\n", - "texts = [\"\"] * request_settings.number_of_responses\n", + "stream = oai_text_service.complete_stream_async(prompt=prompt, settings=oai_text_request_settings)\n", + "number_of_responses = oai_text_request_settings.number_of_responses\n", + "texts = [\"\"] * number_of_responses\n", + "\n", + "last_clear_time = time.time()\n", + "clear_interval = 0.5 # seconds\n", + "\n", + "# Note: there are some quirks with displaying the output, which sometimes flashes and disappears.\n", + "# This could be influenced by a few factors specific to Jupyter notebooks and asynchronous processing.\n", + "# The following code attempts to buffer the results to avoid the output flashing on/off the screen.\n", + "\n", "async for results in stream:\n", - " i = 1\n", - " clear_output(wait=True)\n", - " for result in results:\n", - " texts[i - 1] += result\n", - " print(f\"Result {i}: {texts[i - 1]}\")\n", - " i += 1\n", + " current_time = time.time()\n", + "\n", + " # Update texts with new results\n", + " for idx, result in enumerate(results):\n", + " if idx < number_of_responses:\n", + " texts[idx] += result\n", + "\n", + " # Clear and display output at intervals\n", + " if current_time - last_clear_time > clear_interval:\n", + " clear_output(wait=True)\n", + " for idx, text in enumerate(texts):\n", + " print(f\"Result {idx + 1}: {text}\")\n", + " last_clear_time = current_time\n", "\n", - "print(\"----------------------------------------\")\n" + "print(\"----------------------------------------\")" ] } ], @@ -307,7 +348,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.6" + "version": "3.10.12" } }, "nbformat": 4, diff --git a/python/notebooks/11-streaming-completions.ipynb b/python/notebooks/11-streaming-completions.ipynb index 36cd540cba7e..3a9c409e1d08 100644 --- a/python/notebooks/11-streaming-completions.ipynb +++ b/python/notebooks/11-streaming-completions.ipynb @@ -16,7 +16,7 @@ "metadata": {}, "outputs": [], "source": [ - "!python -m pip install semantic-kernel==0.4.3.dev0" + "!python -m pip install semantic-kernel==0.4.4.dev0" ] }, { @@ -27,8 +27,19 @@ "outputs": [], "source": [ "import semantic_kernel as sk\n", - "from semantic_kernel.connectors.ai import ChatRequestSettings, CompleteRequestSettings\n", - "from semantic_kernel.connectors.ai.open_ai import AzureTextCompletion, AzureChatCompletion, OpenAITextCompletion, OpenAIChatCompletion\n", + "from semantic_kernel.connectors.ai.open_ai.request_settings.open_ai_request_settings import (\n", + " OpenAITextRequestSettings,\n", + " OpenAIChatRequestSettings,\n", + ")\n", + "from semantic_kernel.connectors.ai.open_ai.request_settings.azure_chat_request_settings import (\n", + " AzureChatRequestSettings,\n", + ")\n", + "from semantic_kernel.connectors.ai.open_ai import (\n", + " AzureTextCompletion,\n", + " AzureChatCompletion,\n", + " OpenAITextCompletion,\n", + " OpenAIChatCompletion,\n", + ")\n", "from semantic_kernel.connectors.ai.hugging_face import HuggingFaceTextCompletion" ] }, @@ -52,8 +63,12 @@ "\n", "# Configure Azure LLM service\n", "deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env()\n", - "azure_text_service = AzureTextCompletion(deployment_name=\"text\", endpoint=endpoint, api_key=api_key) # set the deployment name to the value of your text model\n", - "azure_chat_service = AzureChatCompletion(deployment_name=\"turbo\", endpoint=endpoint, api_key=api_key) # set the deployment name to the value of your chat model\n", + "azure_text_service = AzureTextCompletion(\n", + " deployment_name=\"text\", endpoint=endpoint, api_key=api_key\n", + ") # set the deployment name to the value of your text model\n", + "azure_chat_service = AzureChatCompletion(\n", + " deployment_name=\"turbo\", endpoint=endpoint, api_key=api_key\n", + ") # set the deployment name to the value of your chat model\n", "\n", "# Configure OpenAI service\n", "api_key, org_id = sk.openai_settings_from_dot_env()\n", @@ -80,12 +95,12 @@ "metadata": {}, "outputs": [], "source": [ - "request_settings = CompleteRequestSettings(\n", + "oai_request_settings = OpenAITextRequestSettings(\n", " max_tokens=150,\n", " temperature=0.7,\n", " top_p=1,\n", " frequency_penalty=0.5,\n", - " presence_penalty=0.5\n", + " presence_penalty=0.5,\n", ")" ] }, @@ -106,9 +121,9 @@ "outputs": [], "source": [ "prompt = \"what is the purpose of a rubber duck?\"\n", - "stream = oai_text_service.complete_stream_async(prompt=prompt, settings=request_settings)\n", + "stream = oai_text_service.complete_stream_async(prompt=prompt, settings=oai_request_settings)\n", "async for text in stream:\n", - " print(text, end = \"\") # end = \"\" to avoid newlines" + " print(text, end=\"\") # end = \"\" to avoid newlines" ] }, { @@ -128,9 +143,9 @@ "outputs": [], "source": [ "prompt = \"provide me a list of possible meanings for the acronym 'ORLD'\"\n", - "stream = azure_text_service.complete_stream_async(prompt=prompt, settings=request_settings)\n", + "stream = azure_text_service.complete_stream_async(prompt=prompt, settings=oai_request_settings)\n", "async for text in stream:\n", - " print(text, end = \"\") # end = \"\" to avoid newlines" + " print(text, end=\"\") # end = \"\" to avoid newlines" ] }, { @@ -142,6 +157,27 @@ "## Streaming Hugging Face Text Completion" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "be7b1c2e", + "metadata": {}, + "outputs": [], + "source": [ + "from semantic_kernel.connectors.ai.hugging_face.hf_request_settings import (\n", + " HuggingFaceRequestSettings,\n", + ")\n", + "\n", + "hf_request_settings = HuggingFaceRequestSettings(\n", + " extension_data={\n", + " \"max_new_tokens\": 80,\n", + " \"top_p\": 1,\n", + " \"eos_token_id\": 11,\n", + " \"pad_token_id\": 0,\n", + " }\n", + ")" + ] + }, { "cell_type": "code", "execution_count": null, @@ -150,9 +186,9 @@ "outputs": [], "source": [ "prompt = \"The purpose of a rubber duck is\"\n", - "stream = hf_text_service.complete_stream_async(prompt=prompt, request_settings=request_settings)\n", + "stream = hf_text_service.complete_stream_async(prompt=prompt, request_settings=hf_request_settings)\n", "async for text in stream:\n", - " print(text, end = \"\") # end = \"\" to avoid newlines" + " print(text, end=\"\") # end = \"\" to avoid newlines" ] }, { @@ -171,7 +207,7 @@ "metadata": {}, "outputs": [], "source": [ - "chat_request_settings = ChatRequestSettings(\n", + "oai_chat_request_settings = OpenAIChatRequestSettings(\n", " max_tokens=150,\n", " temperature=0.7,\n", " top_p=1,\n", @@ -198,10 +234,10 @@ "source": [ "role = \"system\"\n", "content = \"You are an AI assistant that helps people find information.\"\n", - "message = { \"role\":role, \"content\":content }\n", - "stream = oai_chat_service.complete_chat_stream_async(messages=[message], settings=chat_request_settings)\n", + "message = {\"role\": role, \"content\": content}\n", + "stream = oai_chat_service.complete_chat_stream_async(messages=[message], settings=oai_chat_request_settings)\n", "async for text in stream:\n", - " print(text, end = \"\") # end = \"\" to avoid newlines" + " print(text, end=\"\") # end = \"\" to avoid newlines" ] }, { @@ -213,6 +249,22 @@ "## Streaming Azure OpenAI Chat Completion" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "da1e9f59", + "metadata": {}, + "outputs": [], + "source": [ + "az_oai_chat_request_settings = AzureChatRequestSettings(\n", + " max_tokens=150,\n", + " temperature=0.7,\n", + " top_p=1,\n", + " frequency_penalty=0.5,\n", + " presence_penalty=0.5,\n", + ")" + ] + }, { "cell_type": "code", "execution_count": null, @@ -222,10 +274,10 @@ "source": [ "role = \"system\"\n", "content = \"You are an AI assistant that helps people find information.\"\n", - "message = { \"role\":role, \"content\":content }\n", - "stream = azure_chat_service.complete_chat_stream_async(messages=[message], settings=chat_request_settings)\n", + "message = {\"role\": role, \"content\": content}\n", + "stream = azure_chat_service.complete_chat_stream_async(messages=[message], settings=az_oai_chat_request_settings)\n", "async for text in stream:\n", - " print(text, end = \"\") # end = \"\" to avoid newlines" + " print(text, end=\"\") # end = \"\" to avoid newlines" ] } ], @@ -245,7 +297,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.6" + "version": "3.10.12" } }, "nbformat": 4, diff --git a/python/notebooks/third_party/weaviate-persistent-memory.ipynb b/python/notebooks/third_party/weaviate-persistent-memory.ipynb index 90ef285bb7c4..da708da4ddb0 100644 --- a/python/notebooks/third_party/weaviate-persistent-memory.ipynb +++ b/python/notebooks/third_party/weaviate-persistent-memory.ipynb @@ -1,530 +1,510 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Introduction" - ] - }, - { - "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." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# About Weaviate" - ] - }, - { - "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)." - ] - }, - { - "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." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Setup" - ] - }, - { - "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", - "* 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" - ] - }, - { - "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" - ] - }, - { - "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", - "* from a Docker instance\n", - "* from WCS\n", - "* directly from the client (embedded Weaviate), which works on Linux only at the moment" - ] - }, - { - "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:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kernel = sk.Kernel()\n", - "\n", - "api_key, org_id = sk.openai_settings_from_dot_env()\n", - "kernel.add_chat_service(\n", - " \"chat-gpt\", OpenAIChatCompletion(\"gpt-3.5-turbo\", api_key, org_id)\n", - ")\n", - "kernel.add_text_embedding_generation_service(\n", - " \"ada\", OpenAITextEmbedding(\"text-embedding-ada-002\", api_key, org_id)\n", - ")\n", - "\n", - "kernel.register_memory_store(memory_store=store)\n", - "kernel.import_skill(sk.core_skills.TextMemorySkill())" - ] - }, - { - "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_async`" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "COLLECTION = \"AboutMe\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "async def populate_memory(kernel: sk.Kernel) -> None:\n", - " # Add some documents to the semantic memory\n", - " await kernel.memory.save_information_async(\n", - " COLLECTION, id=\"info1\", text=\"My name is Andrea\"\n", - " )\n", - " await kernel.memory.save_information_async(\n", - " COLLECTION, id=\"info2\", text=\"I currently work as a tour guide\"\n", - " )\n", - " await kernel.memory.save_information_async(\n", - " COLLECTION, id=\"info3\", text=\"I've been living in Seattle since 2005\"\n", - " )\n", - " await kernel.memory.save_information_async(\n", - " COLLECTION, id=\"info4\", text=\"I visited France and Italy five times since 2015\"\n", - " )\n", - " await kernel.memory.save_information_async(\n", - " COLLECTION, id=\"info5\", text=\"My family is from New York\"\n", - " )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Searching is done through `search_async`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "async def search_memory_examples(kernel: sk.Kernel) -> None:\n", - " questions = [\n", - " \"what's my name\",\n", - " \"where do I live?\",\n", - " \"where's my family from?\",\n", - " \"where have I traveled?\",\n", - " \"what do I do for work\",\n", - " ]\n", - "\n", - " for question in questions:\n", - " print(f\"Question: {question}\")\n", - " result = await kernel.memory.search_async(COLLECTION, question)\n", - " print(f\"Answer: {result[0].text}\\n\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's see the results of the functions:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(\"Populating memory...\")\n", - "await populate_memory(kernel)\n", - "\n", - "print(\"Asking questions... (manually)\")\n", - "await search_memory_examples(kernel)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Here's how to use the weaviate memory store in a chat application:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "async def setup_chat_with_memory(\n", - " kernel: sk.Kernel,\n", - ") -> Tuple[sk.SKFunctionBase, sk.SKContext]:\n", - " sk_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", - " - {{$fact1}} {{recall $fact1}}\n", - " - {{$fact2}} {{recall $fact2}}\n", - " - {{$fact3}} {{recall $fact3}}\n", - " - {{$fact4}} {{recall $fact4}}\n", - " - {{$fact5}} {{recall $fact5}}\n", - "\n", - " Chat:\n", - " {{$chat_history}}\n", - " User: {{$user_input}}\n", - " ChatBot: \"\"\".strip()\n", - "\n", - " chat_func = kernel.create_semantic_function(\n", - " sk_prompt, max_tokens=200, temperature=0.8\n", - " )\n", - "\n", - " context = kernel.create_new_context()\n", - " context[\"fact1\"] = \"what is my name?\"\n", - " context[\"fact2\"] = \"where do I live?\"\n", - " context[\"fact3\"] = \"where's my family from?\"\n", - " context[\"fact4\"] = \"where have I traveled?\"\n", - " context[\"fact5\"] = \"what do I do for work?\"\n", - "\n", - " context[sk.core_skills.TextMemorySkill.COLLECTION_PARAM] = COLLECTION\n", - " context[sk.core_skills.TextMemorySkill.RELEVANCE_PARAM] = 0.8\n", - "\n", - " context[\"chat_history\"] = \"\"\n", - "\n", - " return chat_func, context" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "async def chat(\n", - " kernel: sk.Kernel, chat_func: sk.SKFunctionBase, context: sk.SKContext\n", - ") -> bool:\n", - " try:\n", - " user_input = input(\"User:> \")\n", - " context[\"user_input\"] = user_input\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.run_async(chat_func, input_vars=context.variables)\n", - " context[\"chat_history\"] += f\"\\nUser:> {user_input}\\nChatBot:> {answer}\\n\"\n", - "\n", - " print(f\"ChatBot:> {answer}\")\n", - " return True" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(\"Setting up a chat (with memory!)\")\n", - "chat_func, context = await setup_chat_with_memory(kernel)\n", - "\n", - "print(\"Begin chatting (type 'exit' to exit):\\n\")\n", - "chatting = True\n", - "while chatting:\n", - " chatting = await chat(kernel, chat_func, context)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Adding documents to your memory" - ] - }, - { - "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:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "github_files = {}\n", - "github_files[\n", - " \"https://github.com/microsoft/semantic-kernel/blob/main/README.md\"\n", - "] = \"README: Installation, getting started, and how to contribute\"\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 skill or function\"\n", - "github_files[\n", - " \"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", - "github_files[\n", - " \"https://github.com/microsoft/semantic-kernel/tree/main/samples/skills/ChatSkill/ChatGPT\"\n", - "] = \"Sample demonstrating how to create a chat skill interfacing with ChatGPT\"\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_async` to save the file:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "COLLECTION = \"SKGitHub\"\n", - "\n", - "print(\n", - " \"Adding some GitHub file URLs and their descriptions to a volatile Semantic Memory.\"\n", - ")\n", - "i = 0\n", - "for entry, value in github_files.items():\n", - " await kernel.memory.save_reference_async(\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_async` to ask a question:" - ] - }, - { - "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 kernel.memory.search_async(\n", - " COLLECTION, ask, limit=5, min_relevance_score=0.77\n", - ")\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" + ] + }, + { + "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." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# About Weaviate" + ] + }, + { + "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)." + ] + }, + { + "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." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Setup" + ] + }, + { + "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", + "* 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" + ] + }, + { + "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" + ] + }, + { + "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", + "* from a Docker instance\n", + "* from WCS\n", + "* directly from the client (embedded Weaviate), which works on Linux only at the moment" + ] + }, + { + "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:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "kernel = sk.Kernel()\n", + "\n", + "api_key, org_id = sk.openai_settings_from_dot_env()\n", + "kernel.add_chat_service(\"chat-gpt\", OpenAIChatCompletion(\"gpt-3.5-turbo\", api_key, org_id))\n", + "kernel.add_text_embedding_generation_service(\"ada\", OpenAITextEmbedding(\"text-embedding-ada-002\", api_key, org_id))\n", + "\n", + "kernel.register_memory_store(memory_store=store)\n", + "kernel.import_skill(sk.core_skills.TextMemorySkill())" + ] + }, + { + "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_async`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "COLLECTION = \"AboutMe\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "async def populate_memory(kernel: sk.Kernel) -> None:\n", + " # Add some documents to the semantic memory\n", + " await kernel.memory.save_information_async(COLLECTION, id=\"info1\", text=\"My name is Andrea\")\n", + " await kernel.memory.save_information_async(COLLECTION, id=\"info2\", text=\"I currently work as a tour guide\")\n", + " await kernel.memory.save_information_async(COLLECTION, id=\"info3\", text=\"I've been living in Seattle since 2005\")\n", + " await kernel.memory.save_information_async(\n", + " COLLECTION, id=\"info4\", text=\"I visited France and Italy five times since 2015\"\n", + " )\n", + " await kernel.memory.save_information_async(COLLECTION, id=\"info5\", text=\"My family is from New York\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Searching is done through `search_async`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "async def search_memory_examples(kernel: sk.Kernel) -> None:\n", + " questions = [\n", + " \"what's my name\",\n", + " \"where do I live?\",\n", + " \"where's my family from?\",\n", + " \"where have I traveled?\",\n", + " \"what do I do for work\",\n", + " ]\n", + "\n", + " for question in questions:\n", + " print(f\"Question: {question}\")\n", + " result = await kernel.memory.search_async(COLLECTION, question)\n", + " print(f\"Answer: {result[0].text}\\n\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's see the results of the functions:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Populating memory...\")\n", + "await populate_memory(kernel)\n", + "\n", + "print(\"Asking questions... (manually)\")\n", + "await search_memory_examples(kernel)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here's how to use the weaviate memory store in a chat application:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "async def setup_chat_with_memory(\n", + " kernel: sk.Kernel,\n", + ") -> Tuple[sk.SKFunctionBase, sk.SKContext]:\n", + " sk_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", + " - {{$fact1}} {{recall $fact1}}\n", + " - {{$fact2}} {{recall $fact2}}\n", + " - {{$fact3}} {{recall $fact3}}\n", + " - {{$fact4}} {{recall $fact4}}\n", + " - {{$fact5}} {{recall $fact5}}\n", + "\n", + " Chat:\n", + " {{$chat_history}}\n", + " User: {{$user_input}}\n", + " ChatBot: \"\"\".strip()\n", + "\n", + " chat_func = kernel.create_semantic_function(sk_prompt, max_tokens=200, temperature=0.8)\n", + "\n", + " context = kernel.create_new_context()\n", + " context[\"fact1\"] = \"what is my name?\"\n", + " context[\"fact2\"] = \"where do I live?\"\n", + " context[\"fact3\"] = \"where's my family from?\"\n", + " context[\"fact4\"] = \"where have I traveled?\"\n", + " context[\"fact5\"] = \"what do I do for work?\"\n", + "\n", + " context[sk.core_skills.TextMemorySkill.COLLECTION_PARAM] = COLLECTION\n", + " context[sk.core_skills.TextMemorySkill.RELEVANCE_PARAM] = 0.8\n", + "\n", + " context[\"chat_history\"] = \"\"\n", + "\n", + " return chat_func, context" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "async def chat(kernel: sk.Kernel, chat_func: sk.SKFunctionBase, context: sk.SKContext) -> bool:\n", + " try:\n", + " user_input = input(\"User:> \")\n", + " context[\"user_input\"] = user_input\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.run_async(chat_func, input_vars=context.variables)\n", + " context[\"chat_history\"] += f\"\\nUser:> {user_input}\\nChatBot:> {answer}\\n\"\n", + "\n", + " print(f\"ChatBot:> {answer}\")\n", + " return True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Setting up a chat (with memory!)\")\n", + "chat_func, context = await setup_chat_with_memory(kernel)\n", + "\n", + "print(\"Begin chatting (type 'exit' to exit):\\n\")\n", + "chatting = True\n", + "while chatting:\n", + " chatting = await chat(kernel, chat_func, context)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Adding documents to your memory" + ] + }, + { + "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:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "github_files = {}\n", + "github_files[\n", + " \"https://github.com/microsoft/semantic-kernel/blob/main/README.md\"\n", + "] = \"README: Installation, getting started, and how to contribute\"\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 skill or function\"\n", + "github_files[\n", + " \"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", + "github_files[\n", + " \"https://github.com/microsoft/semantic-kernel/tree/main/samples/skills/ChatSkill/ChatGPT\"\n", + "] = \"Sample demonstrating how to create a chat skill interfacing with ChatGPT\"\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_async` to save the file:" + ] + }, + { + "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 kernel.memory.save_reference_async(\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_async` to ask a question:" + ] + }, + { + "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 kernel.memory.search_async(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/pyproject.toml b/python/pyproject.toml index efcdda603260..cc420dd549e7 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "semantic-kernel" -version = "0.4.3.dev" +version = "0.4.4.dev" description = "Semantic Kernel Python SDK" authors = ["Microsoft "] readme = "pip/README.md" diff --git a/python/samples/kernel-syntax-examples/openai_logit_bias.py b/python/samples/kernel-syntax-examples/openai_logit_bias.py index ea736bdcc653..d049debabb6a 100644 --- a/python/samples/kernel-syntax-examples/openai_logit_bias.py +++ b/python/samples/kernel-syntax-examples/openai_logit_bias.py @@ -1,9 +1,17 @@ # Copyright (c) Microsoft. All rights reserved. import asyncio +from typing import Any, Dict import semantic_kernel as sk import semantic_kernel.connectors.ai.open_ai as sk_oai +from semantic_kernel.connectors.ai.ai_request_settings import AIRequestSettings +from semantic_kernel.connectors.ai.chat_completion_client_base import ( + ChatCompletionClientBase, +) +from semantic_kernel.connectors.ai.text_completion_client_base import ( + TextCompletionClientBase, +) """ Logit bias enables prioritizing certain tokens within a given output. @@ -13,13 +21,11 @@ """ -def _config_ban_tokens(settings_type, keys): - settings = sk_oai.OpenAIRequestSettings() - +def _config_ban_tokens(settings: AIRequestSettings, keys: Dict[Any, Any]): # Map each token in the keys list to a bias value from -100 (a potential ban) to 100 (exclusive selection) for k in keys: # -100 to potentially ban all tokens in the list - settings.token_selection_biases[k] = -100 + settings.logit_bias[k] = -100 return settings @@ -59,7 +65,8 @@ async def chat_request_example(kernel, api_key, org_id): ] # Model will try its best to avoid using any of the above words - settings = _config_ban_tokens("chat", keys) + settings = kernel.get_request_settings_from_service(ChatCompletionClientBase, "chat_service") + settings = _config_ban_tokens(settings, keys) prompt_config = sk.PromptTemplateConfig.from_completion_parameters(max_tokens=2000, temperature=0.7, top_p=0.8) prompt_template = sk.ChatPromptTemplate("{{$user_input}}", kernel.prompt_template_engine, prompt_config) @@ -71,15 +78,18 @@ async def chat_request_example(kernel, api_key, org_id): function_config = sk.SemanticFunctionConfig(prompt_config, prompt_template) kernel.register_semantic_function("ChatBot", "Chat", function_config) - chat_messages = list() + chat_messages = [] + messages = [{"role": "user", "content": user_mssg}] + chat_messages.append(("user", user_mssg)) - answer = await openai_chat_completion.complete_chat_async(chat_messages, settings) - chat_messages.append(("assistant", str(answer))) + answer = await openai_chat_completion.complete_chat_async(messages=messages, settings=settings) + chat_messages.append(("assistant", str(answer[0]))) user_mssg = "What are his best all-time stats?" + messages = [{"role": "user", "content": user_mssg}] chat_messages.append(("user", user_mssg)) - answer = await openai_chat_completion.complete_chat_async(chat_messages, settings) - chat_messages.append(("assistant", str(answer))) + answer = await openai_chat_completion.complete_chat_async(messages=messages, settings=settings) + chat_messages.append(("assistant", str(answer[0]))) context_vars = sk.ContextVariables() context_vars["chat_history"] = "" @@ -140,7 +150,8 @@ 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 = _config_ban_tokens("complete", keys) + settings = kernel.get_request_settings_from_service(TextCompletionClientBase, "text_service") + settings = _config_ban_tokens(settings, keys) user_mssg = "The best pie flavor to have in autumn is" answer = await openai_text_completion.complete_async(user_mssg, settings) diff --git a/python/semantic_kernel/connectors/ai/hugging_face/hf_request_settings.py b/python/semantic_kernel/connectors/ai/hugging_face/hf_request_settings.py index b20948014d41..3928649af400 100644 --- a/python/semantic_kernel/connectors/ai/hugging_face/hf_request_settings.py +++ b/python/semantic_kernel/connectors/ai/hugging_face/hf_request_settings.py @@ -28,7 +28,7 @@ def prepare_settings_dict(self, **kwargs) -> Dict[str, Any]: gen_config = self.get_generation_config() if "prompt" in kwargs and kwargs["prompt"] is not None: return { - "prompt": kwargs["prompt"], + "text_inputs": kwargs["prompt"], "generation_config": gen_config, "num_return_sequences": self.num_return_sequences, "do_sample": self.do_sample, diff --git a/python/semantic_kernel/connectors/ai/hugging_face/services/hf_text_completion.py b/python/semantic_kernel/connectors/ai/hugging_face/services/hf_text_completion.py index 5502ed6f1053..6ccbd7566e15 100644 --- a/python/semantic_kernel/connectors/ai/hugging_face/services/hf_text_completion.py +++ b/python/semantic_kernel/connectors/ai/hugging_face/services/hf_text_completion.py @@ -81,7 +81,7 @@ async def complete_async( if kwargs.get("logger"): logger.warning("The `logger` parameter is deprecated. Please use the `logging` module instead.") try: - results = self.generator(**request_settings.prepare_settings_dict(prompt)) + results = self.generator(**request_settings.prepare_settings_dict(prompt=prompt)) result_field_name = "summary_text" if self.task == "summarization" else "generated_text" if len(results) == 1: return results[0][result_field_name] diff --git a/python/semantic_kernel/connectors/ai/open_ai/request_settings/open_ai_request_settings.py b/python/semantic_kernel/connectors/ai/open_ai/request_settings/open_ai_request_settings.py index e48ead848046..3e5eca639dc5 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/request_settings/open_ai_request_settings.py +++ b/python/semantic_kernel/connectors/ai/open_ai/request_settings/open_ai_request_settings.py @@ -14,7 +14,7 @@ class OpenAIRequestSettings(AIRequestSettings): ai_model_id: Optional[str] = Field(None, serialization_alias="model") frequency_penalty: float = Field(0.0, ge=-2.0, le=2.0) - logit_bias: Dict[str, float] = Field(default_factory=dict) + logit_bias: Dict[Union[str, int], float] = Field(default_factory=dict) max_tokens: int = Field(256, gt=0) number_of_responses: int = Field(1, ge=1, le=128, serialization_alias="n") presence_penalty: float = Field(0.0, ge=-2.0, le=2.0) From 1c2d0fe4b3350224184230608f6b5bef025ed3fc Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Fri, 5 Jan 2024 09:51:56 -0800 Subject: [PATCH 3/5] .Net: Avoid causing first-chance exceptions in CodeTokenizer (#4471) ### Motivation and Context The CodeTokenizer routinely causes first-chance (caught) exceptions to determine whether tokens belong to function names of named argument blocks. This is inefficient but also can be startling depending on how one has set one's configuration for breaking on exceptions. See #4418 ### Description CodeTokenizer now uses a new method from NamedArgBlock to determine whether a string could result in a NamedArgBlock instead of resorting to causing exceptions to gather the same information. ### Contribution Checklist - [ ] The code builds clean without any errors or warnings - [ ] 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 - [ ] All unit tests pass, and I have added new tests where possible - [ ] I didn't break anyone :smile: --- .../TemplateEngine/Blocks/NamedArgBlock.cs | 42 ++++++++++++++----- .../TemplateEngine/CodeTokenizer.cs | 11 +++-- .../Blocks/NamedArgBlockTests.cs | 6 +-- .../TemplateEngine/CodeTokenizerTests.cs | 8 ++-- 4 files changed, 44 insertions(+), 23 deletions(-) diff --git a/dotnet/src/SemanticKernel.Core/TemplateEngine/Blocks/NamedArgBlock.cs b/dotnet/src/SemanticKernel.Core/TemplateEngine/Blocks/NamedArgBlock.cs index 2ab6b23b4a99..2da0df2dd1b2 100644 --- a/dotnet/src/SemanticKernel.Core/TemplateEngine/Blocks/NamedArgBlock.cs +++ b/dotnet/src/SemanticKernel.Core/TemplateEngine/Blocks/NamedArgBlock.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; @@ -35,21 +36,14 @@ internal sealed class NamedArgBlock : Block, ITextRendering public NamedArgBlock(string? text, ILoggerFactory? logger = null) : base(NamedArgBlock.TrimWhitespace(text), logger) { - var argParts = this.Content.Split(Symbols.NamedArgBlockSeparator); - if (argParts.Length != 2) + if (!TryGetNameAndValue(this.Content, out string argName, out string argValue)) { this.Logger.LogError("Invalid named argument `{Text}`", text); throw new KernelException($"A function named argument must contain a name and value separated by a '{Symbols.NamedArgBlockSeparator}' character."); } - this.Name = argParts[0]; - this._argNameAsVarBlock = new VarBlock($"{Symbols.VarPrefix}{argParts[0]}"); - var argValue = argParts[1]; - if (argValue.Length == 0) - { - this.Logger.LogError("Invalid named argument `{Text}`", text); - throw new KernelException($"A function named argument must contain a quoted value or variable after the '{Symbols.NamedArgBlockSeparator}' character."); - } + this.Name = argName; + this._argNameAsVarBlock = new VarBlock($"{Symbols.VarPrefix}{argName}"); if (argValue[0] == Symbols.VarPrefix) { @@ -61,6 +55,34 @@ public NamedArgBlock(string? text, ILoggerFactory? logger = null) } } + /// + /// Attempts to extract the name and value of a named argument block from a string + /// + /// String from which to extract a name and value + /// Name extracted from argument block, when successful. Empty string otherwise. + /// Value extracted from argument block, when successful. Empty string otherwise. + /// true when a name and value are successfully extracted from the given text, false otherwise + internal static bool TryGetNameAndValue(string? text, out string name, out string value) + { + name = string.Empty; + value = string.Empty; + + if (!string.IsNullOrEmpty(text)) + { + string[] argBlockParts = text!.Split(new char[] { Symbols.NamedArgBlockSeparator }, StringSplitOptions.RemoveEmptyEntries); + + if (argBlockParts.Length == 2) + { + name = argBlockParts[0]; + value = argBlockParts[1]; + + return true; + } + } + + return false; + } + /// /// Gets the rendered value of the function argument. If the value is a , the value stays the same. /// If the value is a , the value of the variable is determined by the arguments passed in. diff --git a/dotnet/src/SemanticKernel.Core/TemplateEngine/CodeTokenizer.cs b/dotnet/src/SemanticKernel.Core/TemplateEngine/CodeTokenizer.cs index ed11e57b59f9..44206060aaf0 100644 --- a/dotnet/src/SemanticKernel.Core/TemplateEngine/CodeTokenizer.cs +++ b/dotnet/src/SemanticKernel.Core/TemplateEngine/CodeTokenizer.cs @@ -322,14 +322,13 @@ private static bool CanBeEscaped(char c) Justification = "Does not throw an exception by design.")] private static bool IsValidNamedArg(string tokenContent) { - try + if (NamedArgBlock.TryGetNameAndValue(tokenContent, out string _, out string _)) { var tokenContentAsNamedArg = new NamedArgBlock(tokenContent); - return tokenContentAsNamedArg.IsValid(out var error); - } - catch - { - return false; + + return tokenContentAsNamedArg.IsValid(out string _); } + + return false; } } diff --git a/dotnet/src/SemanticKernel.UnitTests/TemplateEngine/Blocks/NamedArgBlockTests.cs b/dotnet/src/SemanticKernel.UnitTests/TemplateEngine/Blocks/NamedArgBlockTests.cs index 9353b87e8fcd..2e6fb7052ecf 100644 --- a/dotnet/src/SemanticKernel.UnitTests/TemplateEngine/Blocks/NamedArgBlockTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/TemplateEngine/Blocks/NamedArgBlockTests.cs @@ -115,11 +115,11 @@ public void ArgValueNeedsQuoteOrDollarSignPrefix() public void ArgNameShouldBeNonEmpty() { // Arrange - var target = new NamedArgBlock("='b'"); + static NamedArgBlock funcToTest() => new("='b'"); // Act + Assert - Assert.False(target.IsValid(out var error)); - Assert.Equal("A named argument must have a name", error); + KernelException exception = Assert.Throws(funcToTest); + Assert.Equal("A function named argument must contain a name and value separated by a '=' character.", exception.Message); } [Fact] diff --git a/dotnet/src/SemanticKernel.UnitTests/TemplateEngine/CodeTokenizerTests.cs b/dotnet/src/SemanticKernel.UnitTests/TemplateEngine/CodeTokenizerTests.cs index 62d90dbfe5b2..d6c185386547 100644 --- a/dotnet/src/SemanticKernel.UnitTests/TemplateEngine/CodeTokenizerTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/TemplateEngine/CodeTokenizerTests.cs @@ -209,12 +209,12 @@ public void ItThrowsWhenSeparatorsAreMissing(string template) } [Theory] - [InlineData("f a =", "A function named argument must contain a quoted value or variable after the '=' character.")] - [InlineData("f a='b' arg2", "A function named argument must contain a name and value separated by a '=' character.")] - public void ItThrowsWhenArgValueIsMissing(string template, string expectedErrorMessage) + [InlineData("f a =")] + [InlineData("f a='b' arg2")] + public void ItThrowsWhenArgValueIsMissing(string template) { // Act & Assert var exception = Assert.Throws(() => this._target.Tokenize(template)); - Assert.Equal(expectedErrorMessage, exception.Message); + Assert.Equal("A function named argument must contain a name and value separated by a '=' character.", exception.Message); } } From 86cb05fde595d49503fad45fd7f525f7d68e3213 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jan 2024 07:53:45 +0000 Subject: [PATCH 4/5] .Net: Bump xunit.analyzers from 1.7.0 to 1.8.0 in /dotnet (#4426) Bumps [xunit.analyzers](https://github.com/xunit/xunit.analyzers) from 1.7.0 to 1.8.0.
Commits
  • 559d488 v1.8.0
  • 7b8a772 Support Roslyn 3.11, 4.2, 4.4, 4.6, and 4.8
  • 8edaa4a Update build scripts for the new .NET SDK 8 requirement
  • 8aa39e4 xunit/xunit#2849: Update xUnit1030 to handle ConfigureAwaitOptions
  • 49afdc9 Restructure DoNotUseConfigureAwaitFixerTests
  • a041234 Restructure DoNotUseConfigureAwaitTests
  • a34764a Update dependencies, and upgrade builder/test projects to .NET 8
  • bc3fd98 Shift xUnit1041 to be a Warning instead of an Error
  • 4db8815 xunit/xunit#2846: Optional parameters should not trigger xUnit1041
  • 9ca8812 Turn off CA1014
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=xunit.analyzers&package-manager=nuget&previous-version=1.7.0&new-version=1.8.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dotnet/Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index f512f324f306..657e599a5382 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -91,7 +91,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 9db47e56155cf8cf4da3d4d1b95bde4833d80108 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jan 2024 07:54:30 +0000 Subject: [PATCH 5/5] .Net: Bump xunit from 2.6.3 to 2.6.4 in /dotnet (#4425) Bumps [xunit](https://github.com/xunit/xunit) from 2.6.3 to 2.6.4.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=xunit&package-manager=nuget&previous-version=2.6.3&new-version=2.6.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dotnet/Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 657e599a5382..dde6071102d7 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -46,7 +46,7 @@ - +