diff --git a/python/samples/concepts/chat_completion/chat_gpt_api.py b/python/samples/concepts/chat_completion/chat_gpt_api.py index cb231a4d0365..66a3839800b8 100644 --- a/python/samples/concepts/chat_completion/chat_gpt_api.py +++ b/python/samples/concepts/chat_completion/chat_gpt_api.py @@ -19,7 +19,7 @@ kernel = Kernel() service_id = "chat-gpt" -kernel.add_service(OpenAIChatCompletion(service_id=service_id)) +kernel.add_service(OpenAIChatCompletion(service_id=service_id, ai_model_id="gpt-3.5-turbo")) settings = kernel.get_prompt_execution_settings_from_service_id(service_id) settings.max_tokens = 2000 diff --git a/python/samples/concepts/memory/azure_cognitive_search_memory.py b/python/samples/concepts/memory/azure_cognitive_search_memory.py index 0580125185dc..2c75525c1d79 100644 --- a/python/samples/concepts/memory/azure_cognitive_search_memory.py +++ b/python/samples/concepts/memory/azure_cognitive_search_memory.py @@ -5,7 +5,6 @@ from semantic_kernel import Kernel from semantic_kernel.connectors.ai.open_ai import AzureTextCompletion, AzureTextEmbedding from semantic_kernel.connectors.memory.azure_cognitive_search import AzureCognitiveSearchMemoryStore -from semantic_kernel.connectors.memory.azure_cognitive_search.azure_ai_search_settings import AzureAISearchSettings from semantic_kernel.core_plugins import TextMemoryPlugin from semantic_kernel.memory import SemanticTextMemory @@ -43,41 +42,19 @@ async def search_acs_memory_questions(memory: SemanticTextMemory) -> None: async def main() -> None: kernel = Kernel() - azure_ai_search_settings = AzureAISearchSettings() - vector_size = 1536 # Setting up OpenAI services for text completion and text embedding - text_complete_service_id = "dv" - kernel.add_service( - AzureTextCompletion( - service_id=text_complete_service_id, - ), - ) - embedding_service_id = "ada" - embedding_gen = AzureTextEmbedding( - service_id=embedding_service_id, - ) - kernel.add_service( - embedding_gen, - ) - - acs_connector = AzureCognitiveSearchMemoryStore( - vector_size=vector_size, - search_endpoint=azure_ai_search_settings.endpoint, - admin_key=azure_ai_search_settings.api_key, - ) - - memory = SemanticTextMemory(storage=acs_connector, embeddings_generator=embedding_gen) - kernel.add_plugin(TextMemoryPlugin(memory), "TextMemoryPlugin") - - print("Populating memory...") - await populate_memory(memory) + kernel.add_service(AzureTextCompletion(service_id="dv")) + async with AzureCognitiveSearchMemoryStore(vector_size=vector_size) as acs_connector: + memory = SemanticTextMemory(storage=acs_connector, embeddings_generator=AzureTextEmbedding(service_id="ada")) + kernel.add_plugin(TextMemoryPlugin(memory), "TextMemoryPlugin") - print("Asking questions... (manually)") - await search_acs_memory_questions(memory) + print("Populating memory...") + await populate_memory(memory) - await acs_connector.close() + print("Asking questions... (manually)") + await search_acs_memory_questions(memory) if __name__ == "__main__": diff --git a/python/samples/concepts/on_your_data/azure_chat_gpt_with_data_api.py b/python/samples/concepts/on_your_data/azure_chat_gpt_with_data_api.py index 92a6d0c6ec23..7419eaa73ed4 100644 --- a/python/samples/concepts/on_your_data/azure_chat_gpt_with_data_api.py +++ b/python/samples/concepts/on_your_data/azure_chat_gpt_with_data_api.py @@ -35,16 +35,14 @@ # } # Create the data source settings -azure_ai_search_settings = AzureAISearchSettings.create() +azure_ai_search_settings = AzureAISearchSettings.create(env_file_path=".env") -az_source = AzureAISearchDataSource(parameters=azure_ai_search_settings.model_dump()) +az_source = AzureAISearchDataSource.from_azure_ai_search_settings(azure_ai_search_settings=azure_ai_search_settings) extra = ExtraBody(data_sources=[az_source]) req_settings = AzureChatPromptExecutionSettings(service_id="default", extra_body=extra) # When using data, use the 2024-02-15-preview API version. -chat_service = AzureChatCompletion( - service_id="chat-gpt", -) +chat_service = AzureChatCompletion(service_id="chat-gpt") kernel.add_service(chat_service) prompt_template_config = PromptTemplateConfig( diff --git a/python/samples/getting_started/06-memory-and-embeddings.ipynb b/python/samples/getting_started/06-memory-and-embeddings.ipynb index 0e03cbb5850a..7de080e7d270 100644 --- a/python/samples/getting_started/06-memory-and-embeddings.ipynb +++ b/python/samples/getting_started/06-memory-and-embeddings.ipynb @@ -1,509 +1,509 @@ { - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "id": "68e1c158", - "metadata": {}, - "source": [ - "# Building Semantic Memory with Embeddings\n", - "\n", - "So far, we've mostly been treating the kernel as a stateless orchestration engine.\n", - "We send text into a model API and receive text out.\n", - "\n", - "In a [previous notebook](04-kernel-arguments-chat.ipynb), we used `kernel arguments` to pass in additional\n", - "text into prompts to enrich them with more data. This allowed us to create a basic chat experience.\n", - "\n", - "However, if you solely relied on kernel arguments, you would quickly realize that eventually your prompt\n", - "would grow so large that you would run into the model's token limit. What we need is a way to persist state\n", - "and build both short-term and long-term memory to empower even more intelligent applications.\n", - "\n", - "To do this, we dive into the key concept of `Semantic Memory` in the Semantic Kernel.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a77bdf89", - "metadata": {}, - "outputs": [], - "source": [ - "!python -m pip install semantic-kernel==1.0.3\n", - "!python -m pip install azure-core==1.30.1\n", - "!python -m pip install azure-search-documents==11.4.0" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1b95af24", - "metadata": {}, - "outputs": [], - "source": [ - "from services import Service\n", - "\n", - "# Select a service to use for this notebook (available services: OpenAI, AzureOpenAI, HuggingFace)\n", - "selectedService = Service.OpenAI" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "d8ddffc1", - "metadata": {}, - "source": [ - "In order to use memory, we need to instantiate the Kernel with a Memory Storage\n", - "and an Embedding service. In this example, we make use of the `VolatileMemoryStore` which can be thought of as a temporary in-memory storage. This memory is not written to disk and is only available during the app session.\n", - "\n", - "When developing your app you will have the option to plug in persistent storage like Azure AI Search, Azure Cosmos Db, PostgreSQL, SQLite, etc. Semantic Memory allows also to index external data sources, without duplicating all the information as you will see further down in this notebook.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8f8dcbc6", - "metadata": {}, - "outputs": [], - "source": [ - "from semantic_kernel.connectors.ai.open_ai.services.azure_chat_completion import (\n", - " AzureChatCompletion,\n", - ")\n", - "from semantic_kernel.connectors.ai.open_ai.services.azure_text_embedding import (\n", - " AzureTextEmbedding,\n", - ")\n", - "from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion import (\n", - " OpenAIChatCompletion,\n", - ")\n", - "from semantic_kernel.connectors.ai.open_ai.services.open_ai_text_embedding import (\n", - " OpenAITextEmbedding,\n", - ")\n", - "from semantic_kernel.core_plugins.text_memory_plugin import TextMemoryPlugin\n", - "from semantic_kernel.kernel import Kernel\n", - "from semantic_kernel.memory.semantic_text_memory import SemanticTextMemory\n", - "from semantic_kernel.memory.volatile_memory_store import VolatileMemoryStore\n", - "\n", - "kernel = Kernel()\n", - "\n", - "chat_service_id = \"chat\"\n", - "\n", - "# Configure AI service used by the kernel\n", - "if selectedService == Service.AzureOpenAI:\n", - " azure_chat_service = AzureChatCompletion(service_id=chat_service_id)\n", - " # next line assumes embeddings deployment name is \"text-embedding\", adjust the deployment name to the value of your chat model if needed\n", - " embedding_gen = AzureTextEmbedding(deployment_name=\"text-embedding\")\n", - " kernel.add_service(azure_chat_service)\n", - " kernel.add_service(embedding_gen)\n", - "elif selectedService == Service.OpenAI:\n", - " oai_chat_service = OpenAIChatCompletion(service_id=chat_service_id, ai_model_id=\"gpt-3.5-turbo\")\n", - " embedding_gen = OpenAITextEmbedding(ai_model_id=\"text-embedding-ada-002\")\n", - " kernel.add_service(oai_chat_service)\n", - " kernel.add_service(embedding_gen)\n", - "\n", - "memory = SemanticTextMemory(storage=VolatileMemoryStore(), embeddings_generator=embedding_gen)\n", - "kernel.add_plugin(TextMemoryPlugin(memory), \"TextMemoryPlugin\")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "e7fefb6a", - "metadata": {}, - "source": [ - "At its core, Semantic Memory is a set of data structures that allow you to store the meaning of text that come from different data sources, and optionally to store the source text too. These texts can be from the web, e-mail providers, chats, a database, or from your local directory, and are hooked up to the Semantic Kernel through data source connectors.\n", - "\n", - "The texts are embedded or compressed into a vector of floats representing mathematically the texts' contents and meaning. You can read more about embeddings [here](https://aka.ms/sk/embeddings).\n" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "2a7e7ca4", - "metadata": {}, - "source": [ - "### Manually adding memories\n", - "\n", - "Let's create some initial memories \"About Me\". We can add memories to our `VolatileMemoryStore` by using `SaveInformationAsync`\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d096504c", - "metadata": {}, - "outputs": [], - "source": [ - "collection_id = \"generic\"\n", - "\n", - "\n", - "async def populate_memory(memory: SemanticTextMemory) -> None:\n", - " # Add some documents to the semantic memory\n", - " await memory.save_information(collection=collection_id, id=\"info1\", text=\"Your budget for 2024 is $100,000\")\n", - " await memory.save_information(collection=collection_id, id=\"info2\", text=\"Your savings from 2023 are $50,000\")\n", - " await memory.save_information(collection=collection_id, id=\"info3\", text=\"Your investments are $80,000\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5338d3ac", - "metadata": {}, - "outputs": [], - "source": [ - "await populate_memory(memory)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "2calf857", - "metadata": {}, - "source": [ - "Let's try searching the memory:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "628c843e", - "metadata": {}, - "outputs": [], - "source": [ - "async def search_memory_examples(memory: SemanticTextMemory) -> None:\n", - " questions = [\n", - " \"What is my budget for 2024?\",\n", - " \"What are my savings from 2023?\",\n", - " \"What are my investments?\",\n", - " ]\n", - "\n", - " for question in questions:\n", - " print(f\"Question: {question}\")\n", - " result = await memory.search(collection_id, question)\n", - " print(f\"Answer: {result[0].text}\\n\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "24764c48", - "metadata": {}, - "outputs": [], - "source": [ - "await search_memory_examples(memory)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "e70c2b22", - "metadata": {}, - "source": [ - "Let's now revisit the our chat sample from the [previous notebook](04-kernel-arguments-chat.ipynb).\n", - "If you remember, we used kernel arguments to fill the prompt with a `history` that continuously got populated as we chatted with the bot. Let's add also memory to it!\n" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "1ed54a32", - "metadata": {}, - "source": [ - "This is done by using the `TextMemoryPlugin` which exposes the `recall` native function.\n", - "\n", - "`recall` takes an input ask and performs a similarity search on the contents that have\n", - "been embedded in the Memory Store and returns the most relevant memory.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fb8549b2", - "metadata": {}, - "outputs": [], - "source": [ - "from semantic_kernel.functions import KernelFunction\n", - "from semantic_kernel.prompt_template import PromptTemplateConfig\n", - "\n", - "\n", - "async def setup_chat_with_memory(\n", - " kernel: Kernel,\n", - " service_id: str,\n", - ") -> KernelFunction:\n", - " prompt = \"\"\"\n", - " ChatBot can have a conversation with you about any topic.\n", - " It can give explicit instructions or say 'I don't know' if\n", - " it does not have an answer.\n", - "\n", - " Information about me, from previous conversations:\n", - " - {{recall 'budget by year'}} What is my budget for 2024?\n", - " - {{recall 'savings from previous year'}} What are my savings from 2023?\n", - " - {{recall 'investments'}} What are my investments?\n", - "\n", - " {{$request}}\n", - " \"\"\".strip()\n", - "\n", - " prompt_template_config = PromptTemplateConfig(\n", - " template=prompt,\n", - " execution_settings={\n", - " service_id: kernel.get_service(service_id).get_prompt_execution_settings_class()(service_id=service_id)\n", - " },\n", - " )\n", - "\n", - " chat_func = kernel.add_function(\n", - " function_name=\"chat_with_memory\",\n", - " plugin_name=\"chat\",\n", - " prompt_template_config=prompt_template_config,\n", - " )\n", - "\n", - " return chat_func" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "1ac62457", - "metadata": {}, - "source": [ - "The `RelevanceParam` is used in memory search and is a measure of the relevance score from 0.0 to 1.0, where 1.0 means a perfect match. We encourage users to experiment with different values.\n" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "645b55a1", - "metadata": {}, - "source": [ - "Now that we've included our memories, let's chat!\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e3875a34", - "metadata": {}, - "outputs": [], - "source": [ - "print(\"Populating memory...\")\n", - "await populate_memory(memory)\n", - "\n", - "print(\"Asking questions... (manually)\")\n", - "await search_memory_examples(memory)\n", - "\n", - "print(\"Setting up a chat (with memory!)\")\n", - "chat_func = await setup_chat_with_memory(kernel, chat_service_id)\n", - "\n", - "print(\"Begin chatting (type 'exit' to exit):\\n\")\n", - "print(\n", - " \"Welcome to the chat bot!\\\n", - " \\n Type 'exit' to exit.\\\n", - " \\n Try asking a question about your finances (i.e. \\\"talk to me about my finances\\\").\"\n", - ")\n", - "\n", - "\n", - "async def chat(user_input: str):\n", - " print(f\"User: {user_input}\")\n", - " answer = await kernel.invoke(chat_func, request=user_input)\n", - " print(f\"ChatBot:> {answer}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6b55f64f", - "metadata": {}, - "outputs": [], - "source": [ - "await chat(\"What is my budget for 2024?\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "243f9eb2", - "metadata": {}, - "outputs": [], - "source": [ - "await chat(\"talk to me about my finances\")" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "0a51542b", - "metadata": {}, - "source": [ - "### Adding documents to your memory\n", - "\n", - "Many times in your applications you'll want to bring in external documents into your memory. Let's see how we can do this using our VolatileMemoryStore.\n", - "\n", - "Let's first get some data using some of the links in the Semantic Kernel repo.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c3d5a1b9", - "metadata": {}, - "outputs": [], - "source": [ - "github_files = {}\n", - "github_files[\"https://github.com/microsoft/semantic-kernel/blob/main/README.md\"] = (\n", - " \"README: Installation, getting started, and how to contribute\"\n", - ")\n", - "github_files[\n", - " \"https://github.com/microsoft/semantic-kernel/blob/main/dotnet/notebooks/02-running-prompts-from-file.ipynb\"\n", - "] = \"Jupyter notebook describing how to pass prompts from a file to a semantic plugin or function\"\n", - "github_files[\"https://github.com/microsoft/semantic-kernel/blob/main/dotnet/notebooks/00-getting-started.ipynb\"] = (\n", - " \"Jupyter notebook describing how to get started with the Semantic Kernel\"\n", - ")\n", - "github_files[\"https://github.com/microsoft/semantic-kernel/tree/main/samples/plugins/ChatPlugin/ChatGPT\"] = (\n", - " \"Sample demonstrating how to create a chat plugin interfacing with ChatGPT\"\n", - ")\n", - "github_files[\n", - " \"https://github.com/microsoft/semantic-kernel/blob/main/dotnet/src/SemanticKernel/Memory/Volatile/VolatileMemoryStore.cs\"\n", - "] = \"C# class that defines a volatile embedding store\"" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "75f3ea5e", - "metadata": {}, - "source": [ - "Now let's add these files to our VolatileMemoryStore using `SaveReferenceAsync`. We'll separate these memories from the chat memories by putting them in a different collection.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "170e7142", - "metadata": {}, - "outputs": [], - "source": [ - "memory_collection_name = \"SKGitHub\"\n", - "print(\"Adding some GitHub file URLs and their descriptions to a volatile Semantic Memory.\")\n", - "i = 0\n", - "for entry, value in github_files.items():\n", - " await memory.save_reference(\n", - " collection=memory_collection_name,\n", - " description=value,\n", - " text=value,\n", - " external_id=entry,\n", - " external_source_name=\"GitHub\",\n", - " )\n", - " i += 1\n", - " print(\" URL {} saved\".format(i))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "143911c3", - "metadata": {}, - "outputs": [], - "source": [ - "ask = \"I love Jupyter notebooks, how should I get started?\"\n", - "print(\"===========================\\n\" + \"Query: \" + ask + \"\\n\")\n", - "\n", - "memories = await memory.search(memory_collection_name, ask, limit=5, min_relevance_score=0.77)\n", - "\n", - "i = 0\n", - "for memory in memories:\n", - " i += 1\n", - " print(f\"Result {i}:\")\n", - " print(\" URL: : \" + memory.id)\n", - " print(\" Title : \" + memory.description)\n", - " print(\" Relevance: \" + str(memory.relevance))\n", - " print()" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "59294dac", - "metadata": {}, - "source": [ - "Now you might be wondering what happens if you have so much data that it doesn't fit into your RAM? That's where you want to make use of an external Vector Database made specifically for storing and retrieving embeddings. Fortunately, semantic kernel makes this easy thanks to an extensive list of available connectors. In the following section, we will connect to an existing Azure AI Search service that we will use as an external Vector Database to store and retrieve embeddings.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "77fdfa86", - "metadata": {}, - "outputs": [], - "source": [ - "from semantic_kernel.connectors.memory.azure_cognitive_search import AzureCognitiveSearchMemoryStore\n", - "\n", - "acs_memory_store = AzureCognitiveSearchMemoryStore(vector_size=1536)\n", - "\n", - "memory = SemanticTextMemory(storage=acs_memory_store, embeddings_generator=embedding_gen)\n", - "kernel.add_plugin(TextMemoryPlugin(memory), \"TextMemoryPluginACS\")" - ] - }, - { - "cell_type": "markdown", - "id": "94f9e83b", - "metadata": {}, - "source": [ - "The implementation of Semantic Kernel allows to easily swap memory store for another. Here, we will re-use the functions we initially created for `VolatileMemoryStore` with our new external Vector Store leveraging Azure AI Search\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fc3da7e1", - "metadata": {}, - "outputs": [], - "source": [ - "await populate_memory(memory)" - ] - }, - { - "cell_type": "markdown", - "id": "b0bbe830", - "metadata": {}, - "source": [ - "Let's now try to query from Azure AI Search!\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1a09d0ca", - "metadata": {}, - "outputs": [], - "source": [ - "await search_memory_examples(memory)" - ] - }, - { - "cell_type": "markdown", - "id": "3d33dcdc", - "metadata": {}, - "source": [ - "We have laid the foundation which will allow us to store an arbitrary amount of data in an external Vector Store above and beyond what could fit in memory at the expense of a little more latency.\n" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "id": "68e1c158", + "metadata": {}, + "source": [ + "# Building Semantic Memory with Embeddings\n", + "\n", + "So far, we've mostly been treating the kernel as a stateless orchestration engine.\n", + "We send text into a model API and receive text out.\n", + "\n", + "In a [previous notebook](04-kernel-arguments-chat.ipynb), we used `kernel arguments` to pass in additional\n", + "text into prompts to enrich them with more data. This allowed us to create a basic chat experience.\n", + "\n", + "However, if you solely relied on kernel arguments, you would quickly realize that eventually your prompt\n", + "would grow so large that you would run into the model's token limit. What we need is a way to persist state\n", + "and build both short-term and long-term memory to empower even more intelligent applications.\n", + "\n", + "To do this, we dive into the key concept of `Semantic Memory` in the Semantic Kernel.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a77bdf89", + "metadata": {}, + "outputs": [], + "source": [ + "!python -m pip install semantic-kernel==1.0.3\n", + "!python -m pip install azure-core==1.30.1\n", + "!python -m pip install azure-search-documents==11.4.0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1b95af24", + "metadata": {}, + "outputs": [], + "source": [ + "from services import Service\n", + "\n", + "# Select a service to use for this notebook (available services: OpenAI, AzureOpenAI, HuggingFace)\n", + "selectedService = Service.OpenAI" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "d8ddffc1", + "metadata": {}, + "source": [ + "In order to use memory, we need to instantiate the Kernel with a Memory Storage\n", + "and an Embedding service. In this example, we make use of the `VolatileMemoryStore` which can be thought of as a temporary in-memory storage. This memory is not written to disk and is only available during the app session.\n", + "\n", + "When developing your app you will have the option to plug in persistent storage like Azure AI Search, Azure Cosmos Db, PostgreSQL, SQLite, etc. Semantic Memory allows also to index external data sources, without duplicating all the information as you will see further down in this notebook.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8f8dcbc6", + "metadata": {}, + "outputs": [], + "source": [ + "from semantic_kernel.connectors.ai.open_ai.services.azure_chat_completion import (\n", + " AzureChatCompletion,\n", + ")\n", + "from semantic_kernel.connectors.ai.open_ai.services.azure_text_embedding import (\n", + " AzureTextEmbedding,\n", + ")\n", + "from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion import (\n", + " OpenAIChatCompletion,\n", + ")\n", + "from semantic_kernel.connectors.ai.open_ai.services.open_ai_text_embedding import (\n", + " OpenAITextEmbedding,\n", + ")\n", + "from semantic_kernel.core_plugins.text_memory_plugin import TextMemoryPlugin\n", + "from semantic_kernel.kernel import Kernel\n", + "from semantic_kernel.memory.semantic_text_memory import SemanticTextMemory\n", + "from semantic_kernel.memory.volatile_memory_store import VolatileMemoryStore\n", + "\n", + "kernel = Kernel()\n", + "\n", + "chat_service_id = \"chat\"\n", + "\n", + "# Configure AI service used by the kernel\n", + "if selectedService == Service.AzureOpenAI:\n", + " azure_chat_service = AzureChatCompletion(service_id=chat_service_id)\n", + " # next line assumes embeddings deployment name is \"text-embedding\", adjust the deployment name to the value of your chat model if needed\n", + " embedding_gen = AzureTextEmbedding(deployment_name=\"text-embedding\")\n", + " kernel.add_service(azure_chat_service)\n", + " kernel.add_service(embedding_gen)\n", + "elif selectedService == Service.OpenAI:\n", + " oai_chat_service = OpenAIChatCompletion(service_id=chat_service_id, ai_model_id=\"gpt-3.5-turbo\")\n", + " embedding_gen = OpenAITextEmbedding(ai_model_id=\"text-embedding-ada-002\")\n", + " kernel.add_service(oai_chat_service)\n", + " kernel.add_service(embedding_gen)\n", + "\n", + "memory = SemanticTextMemory(storage=VolatileMemoryStore(), embeddings_generator=embedding_gen)\n", + "kernel.add_plugin(TextMemoryPlugin(memory), \"TextMemoryPlugin\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "e7fefb6a", + "metadata": {}, + "source": [ + "At its core, Semantic Memory is a set of data structures that allow you to store the meaning of text that come from different data sources, and optionally to store the source text too. These texts can be from the web, e-mail providers, chats, a database, or from your local directory, and are hooked up to the Semantic Kernel through data source connectors.\n", + "\n", + "The texts are embedded or compressed into a vector of floats representing mathematically the texts' contents and meaning. You can read more about embeddings [here](https://aka.ms/sk/embeddings).\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "2a7e7ca4", + "metadata": {}, + "source": [ + "### Manually adding memories\n", + "\n", + "Let's create some initial memories \"About Me\". We can add memories to our `VolatileMemoryStore` by using `SaveInformationAsync`\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d096504c", + "metadata": {}, + "outputs": [], + "source": [ + "collection_id = \"generic\"\n", + "\n", + "\n", + "async def populate_memory(memory: SemanticTextMemory) -> None:\n", + " # Add some documents to the semantic memory\n", + " await memory.save_information(collection=collection_id, id=\"info1\", text=\"Your budget for 2024 is $100,000\")\n", + " await memory.save_information(collection=collection_id, id=\"info2\", text=\"Your savings from 2023 are $50,000\")\n", + " await memory.save_information(collection=collection_id, id=\"info3\", text=\"Your investments are $80,000\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5338d3ac", + "metadata": {}, + "outputs": [], + "source": [ + "await populate_memory(memory)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "2calf857", + "metadata": {}, + "source": [ + "Let's try searching the memory:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "628c843e", + "metadata": {}, + "outputs": [], + "source": [ + "async def search_memory_examples(memory: SemanticTextMemory) -> None:\n", + " questions = [\n", + " \"What is my budget for 2024?\",\n", + " \"What are my savings from 2023?\",\n", + " \"What are my investments?\",\n", + " ]\n", + "\n", + " for question in questions:\n", + " print(f\"Question: {question}\")\n", + " result = await memory.search(collection_id, question)\n", + " print(f\"Answer: {result[0].text}\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24764c48", + "metadata": {}, + "outputs": [], + "source": [ + "await search_memory_examples(memory)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "e70c2b22", + "metadata": {}, + "source": [ + "Let's now revisit the our chat sample from the [previous notebook](04-kernel-arguments-chat.ipynb).\n", + "If you remember, we used kernel arguments to fill the prompt with a `history` that continuously got populated as we chatted with the bot. Let's add also memory to it!\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "1ed54a32", + "metadata": {}, + "source": [ + "This is done by using the `TextMemoryPlugin` which exposes the `recall` native function.\n", + "\n", + "`recall` takes an input ask and performs a similarity search on the contents that have\n", + "been embedded in the Memory Store and returns the most relevant memory.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fb8549b2", + "metadata": {}, + "outputs": [], + "source": [ + "from semantic_kernel.functions import KernelFunction\n", + "from semantic_kernel.prompt_template import PromptTemplateConfig\n", + "\n", + "\n", + "async def setup_chat_with_memory(\n", + " kernel: Kernel,\n", + " service_id: str,\n", + ") -> KernelFunction:\n", + " prompt = \"\"\"\n", + " ChatBot can have a conversation with you about any topic.\n", + " It can give explicit instructions or say 'I don't know' if\n", + " it does not have an answer.\n", + "\n", + " Information about me, from previous conversations:\n", + " - {{recall 'budget by year'}} What is my budget for 2024?\n", + " - {{recall 'savings from previous year'}} What are my savings from 2023?\n", + " - {{recall 'investments'}} What are my investments?\n", + "\n", + " {{$request}}\n", + " \"\"\".strip()\n", + "\n", + " prompt_template_config = PromptTemplateConfig(\n", + " template=prompt,\n", + " execution_settings={\n", + " service_id: kernel.get_service(service_id).get_prompt_execution_settings_class()(service_id=service_id)\n", + " },\n", + " )\n", + "\n", + " chat_func = kernel.add_function(\n", + " function_name=\"chat_with_memory\",\n", + " plugin_name=\"chat\",\n", + " prompt_template_config=prompt_template_config,\n", + " )\n", + "\n", + " return chat_func" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "1ac62457", + "metadata": {}, + "source": [ + "The `RelevanceParam` is used in memory search and is a measure of the relevance score from 0.0 to 1.0, where 1.0 means a perfect match. We encourage users to experiment with different values.\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "645b55a1", + "metadata": {}, + "source": [ + "Now that we've included our memories, let's chat!\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e3875a34", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Populating memory...\")\n", + "await populate_memory(memory)\n", + "\n", + "print(\"Asking questions... (manually)\")\n", + "await search_memory_examples(memory)\n", + "\n", + "print(\"Setting up a chat (with memory!)\")\n", + "chat_func = await setup_chat_with_memory(kernel, chat_service_id)\n", + "\n", + "print(\"Begin chatting (type 'exit' to exit):\\n\")\n", + "print(\n", + " \"Welcome to the chat bot!\\\n", + " \\n Type 'exit' to exit.\\\n", + " \\n Try asking a question about your finances (i.e. \\\"talk to me about my finances\\\").\"\n", + ")\n", + "\n", + "\n", + "async def chat(user_input: str):\n", + " print(f\"User: {user_input}\")\n", + " answer = await kernel.invoke(chat_func, request=user_input)\n", + " print(f\"ChatBot:> {answer}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6b55f64f", + "metadata": {}, + "outputs": [], + "source": [ + "await chat(\"What is my budget for 2024?\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "243f9eb2", + "metadata": {}, + "outputs": [], + "source": [ + "await chat(\"talk to me about my finances\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "0a51542b", + "metadata": {}, + "source": [ + "### Adding documents to your memory\n", + "\n", + "Many times in your applications you'll want to bring in external documents into your memory. Let's see how we can do this using our VolatileMemoryStore.\n", + "\n", + "Let's first get some data using some of the links in the Semantic Kernel repo.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c3d5a1b9", + "metadata": {}, + "outputs": [], + "source": [ + "github_files = {}\n", + "github_files[\"https://github.com/microsoft/semantic-kernel/blob/main/README.md\"] = (\n", + " \"README: Installation, getting started, and how to contribute\"\n", + ")\n", + "github_files[\n", + " \"https://github.com/microsoft/semantic-kernel/blob/main/dotnet/notebooks/02-running-prompts-from-file.ipynb\"\n", + "] = \"Jupyter notebook describing how to pass prompts from a file to a semantic plugin or function\"\n", + "github_files[\"https://github.com/microsoft/semantic-kernel/blob/main/dotnet/notebooks/00-getting-started.ipynb\"] = (\n", + " \"Jupyter notebook describing how to get started with the Semantic Kernel\"\n", + ")\n", + "github_files[\"https://github.com/microsoft/semantic-kernel/tree/main/samples/plugins/ChatPlugin/ChatGPT\"] = (\n", + " \"Sample demonstrating how to create a chat plugin interfacing with ChatGPT\"\n", + ")\n", + "github_files[\n", + " \"https://github.com/microsoft/semantic-kernel/blob/main/dotnet/src/SemanticKernel/Memory/Volatile/VolatileMemoryStore.cs\"\n", + "] = \"C# class that defines a volatile embedding store\"" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "75f3ea5e", + "metadata": {}, + "source": [ + "Now let's add these files to our VolatileMemoryStore using `SaveReferenceAsync`. We'll separate these memories from the chat memories by putting them in a different collection.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "170e7142", + "metadata": {}, + "outputs": [], + "source": [ + "memory_collection_name = \"SKGitHub\"\n", + "print(\"Adding some GitHub file URLs and their descriptions to a volatile Semantic Memory.\")\n", + "i = 0\n", + "for entry, value in github_files.items():\n", + " await memory.save_reference(\n", + " collection=memory_collection_name,\n", + " description=value,\n", + " text=value,\n", + " external_id=entry,\n", + " external_source_name=\"GitHub\",\n", + " )\n", + " i += 1\n", + " print(\" URL {} saved\".format(i))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "143911c3", + "metadata": {}, + "outputs": [], + "source": [ + "ask = \"I love Jupyter notebooks, how should I get started?\"\n", + "print(\"===========================\\n\" + \"Query: \" + ask + \"\\n\")\n", + "\n", + "memories = await memory.search(memory_collection_name, ask, limit=5, min_relevance_score=0.77)\n", + "\n", + "i = 0\n", + "for memory in memories:\n", + " i += 1\n", + " print(f\"Result {i}:\")\n", + " print(\" URL: : \" + memory.id)\n", + " print(\" Title : \" + memory.description)\n", + " print(\" Relevance: \" + str(memory.relevance))\n", + " print()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "59294dac", + "metadata": {}, + "source": [ + "Now you might be wondering what happens if you have so much data that it doesn't fit into your RAM? That's where you want to make use of an external Vector Database made specifically for storing and retrieving embeddings. Fortunately, semantic kernel makes this easy thanks to an extensive list of available connectors. In the following section, we will connect to an existing Azure AI Search service that we will use as an external Vector Database to store and retrieve embeddings.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "77fdfa86", + "metadata": {}, + "outputs": [], + "source": [ + "from semantic_kernel.connectors.memory.azure_cognitive_search import AzureCognitiveSearchMemoryStore\n", + "\n", + "acs_memory_store = AzureCognitiveSearchMemoryStore(vector_size=1536)\n", + "\n", + "memory = SemanticTextMemory(storage=acs_memory_store, embeddings_generator=embedding_gen)\n", + "kernel.add_plugin(TextMemoryPlugin(memory), \"TextMemoryPluginACS\")" + ] + }, + { + "cell_type": "markdown", + "id": "94f9e83b", + "metadata": {}, + "source": [ + "The implementation of Semantic Kernel allows to easily swap memory store for another. Here, we will re-use the functions we initially created for `VolatileMemoryStore` with our new external Vector Store leveraging Azure AI Search\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fc3da7e1", + "metadata": {}, + "outputs": [], + "source": [ + "await populate_memory(memory)" + ] + }, + { + "cell_type": "markdown", + "id": "b0bbe830", + "metadata": {}, + "source": [ + "Let's now try to query from Azure AI Search!\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1a09d0ca", + "metadata": {}, + "outputs": [], + "source": [ + "await search_memory_examples(memory)" + ] + }, + { + "cell_type": "markdown", + "id": "3d33dcdc", + "metadata": {}, + "source": [ + "We have laid the foundation which will allow us to store an arbitrary amount of data in an external Vector Store above and beyond what could fit in memory at the expense of a little more latency.\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.12" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/python/semantic_kernel/connectors/ai/google_palm/services/gp_chat_completion.py b/python/semantic_kernel/connectors/ai/google_palm/services/gp_chat_completion.py index 20ff3e853553..a38b95064827 100644 --- a/python/semantic_kernel/connectors/ai/google_palm/services/gp_chat_completion.py +++ b/python/semantic_kernel/connectors/ai/google_palm/services/gp_chat_completion.py @@ -19,7 +19,7 @@ from semantic_kernel.contents.chat_history import ChatHistory from semantic_kernel.contents.chat_message_content import ChatMessageContent from semantic_kernel.contents.text_content import TextContent -from semantic_kernel.exceptions import ServiceInvalidRequestError, ServiceResponseException +from semantic_kernel.exceptions import ServiceInitializationError, ServiceInvalidRequestError, ServiceResponseException logger: logging.Logger = logging.getLogger(__name__) @@ -37,6 +37,7 @@ def __init__( api_key: str | None = None, message_history: ChatHistory | None = None, env_file_path: str | None = None, + env_file_encoding: str | None = None, ): """Initializes a new instance of the GooglePalmChatCompletion class. @@ -48,25 +49,27 @@ def __init__( message_history (ChatHistory | None): The message history to use for context. (Optional) env_file_path (str | None): Use the environment settings file as a fallback to environment variables. (Optional) + env_file_encoding (str | None): The encoding of the environment settings file. (Optional) + + Raises: + ServiceInitializationError: When any of the required settings are missing. """ - google_palm_settings = None try: - google_palm_settings = GooglePalmSettings.create(env_file_path=env_file_path) - except ValidationError as e: - logger.warning(f"Error loading Google Palm pydantic settings: {e}") - - api_key = api_key or ( - google_palm_settings.api_key.get_secret_value() - if google_palm_settings and google_palm_settings.api_key - else None - ) - ai_model_id = ai_model_id or ( - google_palm_settings.chat_model_id if google_palm_settings and google_palm_settings.chat_model_id else None - ) + google_palm_settings = GooglePalmSettings.create( + api_key=api_key, + chat_model_id=ai_model_id, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + except ValidationError as ex: + raise ServiceInitializationError("Failed to create Google Palm settings", ex) from ex + + if not google_palm_settings.chat_model_id: + raise ServiceInitializationError("The chat model ID is required for a Chat Completion Model.") super().__init__( - ai_model_id=ai_model_id, - api_key=api_key, + ai_model_id=google_palm_settings.chat_model_id, + api_key=google_palm_settings.api_key.get_secret_value(), ) self._message_history = message_history diff --git a/python/semantic_kernel/connectors/ai/google_palm/services/gp_text_completion.py b/python/semantic_kernel/connectors/ai/google_palm/services/gp_text_completion.py index ecb4117d0f67..2bc1a9e19c32 100644 --- a/python/semantic_kernel/connectors/ai/google_palm/services/gp_text_completion.py +++ b/python/semantic_kernel/connectors/ai/google_palm/services/gp_text_completion.py @@ -13,7 +13,7 @@ from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings from semantic_kernel.connectors.ai.text_completion_client_base import TextCompletionClientBase from semantic_kernel.contents.text_content import TextContent -from semantic_kernel.exceptions import ServiceResponseException +from semantic_kernel.exceptions import ServiceInitializationError, ServiceResponseException logger: logging.Logger = logging.getLogger(__name__) @@ -21,7 +21,13 @@ class GooglePalmTextCompletion(TextCompletionClientBase): api_key: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)] - def __init__(self, ai_model_id: str, api_key: str | None = None, env_file_path: str | None = None): + def __init__( + self, + ai_model_id: str, + api_key: str | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + ): """Initializes a new instance of the GooglePalmTextCompletion class. Args: @@ -31,23 +37,28 @@ def __init__(self, ai_model_id: str, api_key: str | None = None, env_file_path: read from either the env vars or the .env settings file. env_file_path (str | None): Use the environment settings file as a fallback to environment variables. (Optional) + env_file_encoding (str | None): The encoding of the environment settings file. (Optional) + + Raises: + ServiceInitializationError: When the Google Palm settings cannot be read. """ try: - google_palm_settings = GooglePalmSettings.create(env_file_path=env_file_path) - except ValidationError as e: - logger.warning(f"Error loading Google Palm pydantic settings: {e}") - - api_key = api_key or ( - google_palm_settings.api_key.get_secret_value() - if google_palm_settings and google_palm_settings.api_key - else None - ) - ai_model_id = ai_model_id or ( - google_palm_settings.text_model_id if google_palm_settings and google_palm_settings.text_model_id else None + google_palm_settings = GooglePalmSettings.create( + api_key=api_key, + text_model_id=ai_model_id, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + except ValidationError as ex: + raise ServiceInitializationError("Failed to create Google Palm settings.", ex) from ex + if not google_palm_settings.text_model_id: + raise ServiceInitializationError("The Google Palm text model ID is required.") + + super().__init__( + ai_model_id=google_palm_settings.text_model_id, + api_key=google_palm_settings.api_key.get_secret_value() if google_palm_settings.api_key else None, ) - super().__init__(ai_model_id=ai_model_id, api_key=api_key) - async def get_text_contents( self, prompt: str, settings: GooglePalmTextPromptExecutionSettings ) -> list[TextContent]: diff --git a/python/semantic_kernel/connectors/ai/google_palm/services/gp_text_embedding.py b/python/semantic_kernel/connectors/ai/google_palm/services/gp_text_embedding.py index 5678a79ae514..25f9e0f79b5d 100644 --- a/python/semantic_kernel/connectors/ai/google_palm/services/gp_text_embedding.py +++ b/python/semantic_kernel/connectors/ai/google_palm/services/gp_text_embedding.py @@ -9,7 +9,7 @@ from semantic_kernel.connectors.ai.embeddings.embedding_generator_base import EmbeddingGeneratorBase from semantic_kernel.connectors.ai.google_palm.settings.google_palm_settings import GooglePalmSettings -from semantic_kernel.exceptions import ServiceInvalidAuthError, ServiceResponseException +from semantic_kernel.exceptions import ServiceInitializationError, ServiceInvalidAuthError, ServiceResponseException from semantic_kernel.utils.experimental_decorator import experimental_class logger: logging.Logger = logging.getLogger(__name__) @@ -19,7 +19,13 @@ class GooglePalmTextEmbedding(EmbeddingGeneratorBase): api_key: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)] - def __init__(self, ai_model_id: str, api_key: str | None = None, env_file_path: str | None = None) -> None: + def __init__( + self, + ai_model_id: str, + api_key: str | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + ) -> None: """Initializes a new instance of the GooglePalmTextEmbedding class. Args: @@ -29,23 +35,28 @@ def __init__(self, ai_model_id: str, api_key: str | None = None, env_file_path: read from either the env vars or the .env settings file. env_file_path (str | None): Use the environment settings file as a fallback to environment variables. (Optional) + env_file_encoding (str | None): The encoding of the environment settings file. (Optional) + + Raises: + ServiceInitializationError: When the Google Palm settings cannot be read. + """ try: - google_palm_settings = GooglePalmSettings.create(env_file_path=env_file_path) - except ValidationError as e: - logger.error(f"Error loading Google Palm pydantic settings: {e}") + google_palm_settings = GooglePalmSettings.create( + api_key=api_key, + embedding_model_id=ai_model_id, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + except ValidationError as ex: + raise ServiceInitializationError("Failed to create Google Palm settings.", ex) from ex + if not google_palm_settings.embedding_model_id: + raise ServiceInitializationError("The Google Palm embedding model ID is required.") - api_key = api_key or ( - google_palm_settings.api_key.get_secret_value() - if google_palm_settings and google_palm_settings.api_key - else None - ) - ai_model_id = ai_model_id or ( - google_palm_settings.embedding_model_id - if google_palm_settings and google_palm_settings.embedding_model_id - else None + super().__init__( + ai_model_id=google_palm_settings.embedding_model_id, + api_key=google_palm_settings.api_key.get_secret_value() if google_palm_settings.api_key else None, ) - super().__init__(ai_model_id=ai_model_id, api_key=api_key) async def generate_embeddings(self, texts: list[str], **kwargs: Any) -> ndarray: """Generates embeddings for the given list of texts.""" diff --git a/python/semantic_kernel/connectors/ai/google_palm/settings/google_palm_settings.py b/python/semantic_kernel/connectors/ai/google_palm/settings/google_palm_settings.py index 586d83d48823..f4eb33d72258 100644 --- a/python/semantic_kernel/connectors/ai/google_palm/settings/google_palm_settings.py +++ b/python/semantic_kernel/connectors/ai/google_palm/settings/google_palm_settings.py @@ -1,10 +1,13 @@ # Copyright (c) Microsoft. All rights reserved. +from typing import ClassVar + from pydantic import SecretStr -from pydantic_settings import BaseSettings + +from semantic_kernel.kernel_pydantic import KernelBaseSettings -class GooglePalmSettings(BaseSettings): +class GooglePalmSettings(KernelBaseSettings): """Google Palm model settings. The settings are first loaded from environment variables with the prefix 'GOOGLE_PALM_'. If the @@ -24,26 +27,9 @@ class GooglePalmSettings(BaseSettings): (Env var GOOGLE_PALM_EMBEDDING_MODEL_ID) """ - env_file_path: str | None = None - api_key: SecretStr | None = None + env_prefix: ClassVar[str] = "GOOGLE_PALM_" + + api_key: SecretStr chat_model_id: str | None = None text_model_id: str | None = None embedding_model_id: str | None = None - - class Config: - """Pydantic configuration settings.""" - - env_prefix = "GOOGLE_PALM_" - env_file = None - env_file_encoding = "utf-8" - extra = "ignore" - case_sensitive = False - - @classmethod - def create(cls, **kwargs): - """Create the settings object.""" - if "env_file_path" in kwargs and kwargs["env_file_path"]: - cls.Config.env_file = kwargs["env_file_path"] - else: - cls.Config.env_file = None - return cls(**kwargs) diff --git a/python/semantic_kernel/connectors/ai/open_ai/prompt_execution_settings/azure_chat_prompt_execution_settings.py b/python/semantic_kernel/connectors/ai/open_ai/prompt_execution_settings/azure_chat_prompt_execution_settings.py index a8812bae8ab8..fb01110eec37 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/prompt_execution_settings/azure_chat_prompt_execution_settings.py +++ b/python/semantic_kernel/connectors/ai/open_ai/prompt_execution_settings/azure_chat_prompt_execution_settings.py @@ -8,6 +8,7 @@ from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.open_ai_prompt_execution_settings import ( OpenAIChatPromptExecutionSettings, ) +from semantic_kernel.connectors.memory.azure_cognitive_search.azure_ai_search_settings import AzureAISearchSettings from semantic_kernel.kernel_pydantic import KernelBaseModel logger = logging.getLogger(__name__) @@ -82,6 +83,18 @@ class AzureAISearchDataSource(AzureChatRequestBase): type: Literal["azure_search"] = "azure_search" parameters: Annotated[dict, AzureAISearchDataSourceParameters] + @classmethod + def from_azure_ai_search_settings(cls, azure_ai_search_settings: AzureAISearchSettings, **kwargs: Any): + """Create an instance from Azure AI Search settings.""" + kwargs["parameters"] = { + "endpoint": str(azure_ai_search_settings.endpoint), + "index_name": azure_ai_search_settings.index_name, + "authentication": { + "key": azure_ai_search_settings.api_key.get_secret_value() if azure_ai_search_settings.api_key else None + }, + } + return cls(**kwargs) + DataSource = Annotated[Union[AzureAISearchDataSource, AzureCosmosDBDataSource], Field(discriminator="type")] diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/azure_chat_completion.py b/python/semantic_kernel/connectors/ai/open_ai/services/azure_chat_completion.py index 586a911027ad..cdcb9f874c68 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/services/azure_chat_completion.py +++ b/python/semantic_kernel/connectors/ai/open_ai/services/azure_chat_completion.py @@ -13,7 +13,6 @@ from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice from pydantic import ValidationError -from semantic_kernel.connectors.ai.open_ai.const import DEFAULT_AZURE_API_VERSION from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.azure_chat_prompt_execution_settings import ( AzureChatPromptExecutionSettings, ) @@ -51,6 +50,7 @@ def __init__( default_headers: Mapping[str, str] | None = None, async_client: AsyncAzureOpenAI | None = None, env_file_path: str | None = None, + env_file_encoding: str | None = None, ) -> None: """Initialize an AzureChatCompletion service. @@ -72,46 +72,41 @@ def __init__( string values for HTTP requests. (Optional) async_client (AsyncAzureOpenAI | None): An existing client to use. (Optional) env_file_path (str | None): Use the environment settings file as a fallback to using env vars. + env_file_encoding (str | None): The encoding of the environment settings file, defaults to 'utf-8'. """ - azure_openai_settings = None try: - azure_openai_settings = AzureOpenAISettings.create(env_file_path=env_file_path) - except ValidationError as e: - logger.warning(f"Failed to load AzureOpenAI pydantic settings: {e}") + azure_openai_settings = AzureOpenAISettings.create( + api_key=api_key, + base_url=base_url, + endpoint=endpoint, + chat_deployment_name=deployment_name, + api_version=api_version, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + except ValidationError as exc: + raise ServiceInitializationError(f"Failed to validate settings: {exc}") from exc - base_url = base_url or ( - str(azure_openai_settings.base_url) if azure_openai_settings and azure_openai_settings.base_url else None - ) - endpoint = endpoint or ( - str(azure_openai_settings.endpoint) if azure_openai_settings and azure_openai_settings.endpoint else None - ) - deployment_name = deployment_name or ( - azure_openai_settings.chat_deployment_name if azure_openai_settings else None - ) - api_version = api_version or (azure_openai_settings.api_version if azure_openai_settings else None) - api_key = api_key or ( - azure_openai_settings.api_key.get_secret_value() - if azure_openai_settings and azure_openai_settings.api_key - else None - ) + if not azure_openai_settings.chat_deployment_name: + raise ServiceInitializationError("chat_deployment_name is required.") - if api_version is None: - api_version = DEFAULT_AZURE_API_VERSION + if not azure_openai_settings.api_key and not ad_token and not ad_token_provider: + raise ServiceInitializationError("Please provide either api_key, ad_token or ad_token_provider") - if not base_url and not endpoint: + if not azure_openai_settings.base_url and not azure_openai_settings.endpoint: raise ServiceInitializationError("At least one of base_url or endpoint must be provided.") - if base_url and isinstance(base_url, str): - base_url = HttpsUrl(base_url) - if endpoint and deployment_name: - base_url = HttpsUrl(f"{str(endpoint).rstrip('/')}/openai/deployments/{deployment_name}") + if azure_openai_settings.endpoint and azure_openai_settings.chat_deployment_name: + azure_openai_settings.base_url = HttpsUrl( + f"{str(azure_openai_settings.endpoint).rstrip('/')}/openai/deployments/{azure_openai_settings.chat_deployment_name}" + ) super().__init__( - deployment_name=deployment_name, - endpoint=endpoint if not isinstance(endpoint, str) else HttpsUrl(endpoint), - base_url=base_url, - api_version=api_version, + deployment_name=azure_openai_settings.chat_deployment_name, + endpoint=azure_openai_settings.endpoint, + base_url=azure_openai_settings.base_url, + api_version=azure_openai_settings.api_version, service_id=service_id, - api_key=api_key, + api_key=azure_openai_settings.api_key.get_secret_value() if azure_openai_settings.api_key else None, ad_token=ad_token, ad_token_provider=ad_token_provider, default_headers=default_headers, diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/azure_text_completion.py b/python/semantic_kernel/connectors/ai/open_ai/services/azure_text_completion.py index 15b3c01835db..e9767fc00f72 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/services/azure_text_completion.py +++ b/python/semantic_kernel/connectors/ai/open_ai/services/azure_text_completion.py @@ -7,7 +7,6 @@ from openai.lib.azure import AsyncAzureADTokenProvider from pydantic import ValidationError -from semantic_kernel.connectors.ai.open_ai.const import DEFAULT_AZURE_API_VERSION from semantic_kernel.connectors.ai.open_ai.services.azure_config_base import AzureOpenAIConfigBase from semantic_kernel.connectors.ai.open_ai.services.open_ai_handler import OpenAIModelTypes from semantic_kernel.connectors.ai.open_ai.services.open_ai_text_completion_base import OpenAITextCompletionBase @@ -57,46 +56,32 @@ def __init__( env_file_path (str | None): Use the environment settings file as a fallback to environment variables. (Optional) """ - azure_openai_settings = None try: - azure_openai_settings = AzureOpenAISettings.create(env_file_path=env_file_path) - except ValidationError as e: - logger.warning(f"Failed to load AzureOpenAI pydantic settings: {e}") - - base_url = base_url or ( - str(azure_openai_settings.base_url) if azure_openai_settings and azure_openai_settings.base_url else None - ) - endpoint = endpoint or ( - str(azure_openai_settings.endpoint) if azure_openai_settings and azure_openai_settings.endpoint else None - ) - deployment_name = deployment_name or ( - azure_openai_settings.text_deployment_name if azure_openai_settings else None - ) - api_version = api_version or (azure_openai_settings.api_version if azure_openai_settings else None) - api_key = api_key or ( - azure_openai_settings.api_key.get_secret_value() - if azure_openai_settings and azure_openai_settings.api_key - else None - ) - - if api_version is None: - api_version = DEFAULT_AZURE_API_VERSION - - if not base_url and not endpoint: + azure_openai_settings = AzureOpenAISettings.create( + env_file_path=env_file_path, + text_deployment_name=deployment_name, + endpoint=endpoint, + base_url=base_url, + api_key=api_key, + api_version=api_version, + ) + except ValidationError as ex: + raise ServiceInitializationError(f"Invalid settings: {ex}") from ex + if not azure_openai_settings.text_deployment_name: + raise ServiceInitializationError("The Azure Text deployment name is required.") + if not azure_openai_settings.base_url and not azure_openai_settings.endpoint: raise ServiceInitializationError("At least one of base_url or endpoint must be provided.") - - if base_url and isinstance(base_url, str): - base_url = HttpsUrl(base_url) - if endpoint and deployment_name: - base_url = HttpsUrl(f"{str(endpoint).rstrip('/')}/openai/deployments/{deployment_name}") - + if azure_openai_settings.endpoint and azure_openai_settings.text_deployment_name: + azure_openai_settings.base_url = HttpsUrl( + f"{str(azure_openai_settings.endpoint).rstrip('/')}/openai/deployments/{azure_openai_settings.text_deployment_name}" + ) super().__init__( - deployment_name=deployment_name, - endpoint=endpoint if not isinstance(endpoint, str) else HttpsUrl(endpoint), - base_url=base_url, - api_version=api_version, + deployment_name=azure_openai_settings.text_deployment_name, + endpoint=azure_openai_settings.endpoint, + base_url=azure_openai_settings.base_url, + api_version=azure_openai_settings.api_version, service_id=service_id, - api_key=api_key, + api_key=azure_openai_settings.api_key.get_secret_value() if azure_openai_settings.api_key else None, ad_token=ad_token, ad_token_provider=ad_token_provider, default_headers=default_headers, diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/azure_text_embedding.py b/python/semantic_kernel/connectors/ai/open_ai/services/azure_text_embedding.py index bce92f2be560..6f6559908053 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/services/azure_text_embedding.py +++ b/python/semantic_kernel/connectors/ai/open_ai/services/azure_text_embedding.py @@ -1,6 +1,5 @@ # Copyright (c) Microsoft. All rights reserved. - import logging from collections.abc import Mapping @@ -8,7 +7,6 @@ from openai.lib.azure import AsyncAzureADTokenProvider from pydantic import ValidationError -from semantic_kernel.connectors.ai.open_ai.const import DEFAULT_AZURE_API_VERSION from semantic_kernel.connectors.ai.open_ai.services.azure_config_base import AzureOpenAIConfigBase from semantic_kernel.connectors.ai.open_ai.services.open_ai_handler import OpenAIModelTypes from semantic_kernel.connectors.ai.open_ai.services.open_ai_text_embedding_base import OpenAITextEmbeddingBase @@ -60,46 +58,35 @@ def __init__( env_file_path (str | None): Use the environment settings file as a fallback to environment variables. (Optional) """ - azure_openai_settings = None try: - azure_openai_settings = AzureOpenAISettings.create(env_file_path=env_file_path) - except ValidationError as e: - logger.warning(f"Failed to load AzureOpenAI pydantic settings: {e}") - - base_url = base_url or ( - str(azure_openai_settings.base_url) if azure_openai_settings and azure_openai_settings.base_url else None - ) - endpoint = endpoint or ( - str(azure_openai_settings.endpoint) if azure_openai_settings and azure_openai_settings.endpoint else None - ) - deployment_name = deployment_name or ( - azure_openai_settings.embedding_deployment_name if azure_openai_settings else None - ) - api_version = api_version or (azure_openai_settings.api_version if azure_openai_settings else None) - api_key = api_key or ( - azure_openai_settings.api_key.get_secret_value() - if azure_openai_settings and azure_openai_settings.api_key - else None - ) - - if api_version is None: - api_version = DEFAULT_AZURE_API_VERSION - - if not base_url and not endpoint: + azure_openai_settings = AzureOpenAISettings.create( + env_file_path=env_file_path, + api_key=api_key, + embedding_deployment_name=deployment_name, + endpoint=endpoint, + base_url=base_url, + api_version=api_version, + ) + except ValidationError as exc: + raise ServiceInitializationError(f"Invalid settings: {exc}") from exc + if not azure_openai_settings.embedding_deployment_name: + raise ServiceInitializationError("The Azure OpenAI embedding deployment name is required.") + + if not azure_openai_settings.base_url and not azure_openai_settings.endpoint: raise ServiceInitializationError("At least one of base_url or endpoint must be provided.") - if base_url and isinstance(base_url, str): - base_url = HttpsUrl(base_url) - if endpoint and deployment_name: - base_url = HttpsUrl(f"{str(endpoint).rstrip('/')}/openai/deployments/{deployment_name}") + if azure_openai_settings.endpoint and azure_openai_settings.embedding_deployment_name: + azure_openai_settings.base_url = HttpsUrl( + f"{str(azure_openai_settings.endpoint).rstrip('/')}/openai/deployments/{azure_openai_settings.embedding_deployment_name}" + ) super().__init__( - deployment_name=deployment_name, - endpoint=endpoint if not isinstance(endpoint, str) else HttpsUrl(endpoint), - base_url=base_url, - api_version=api_version, + deployment_name=azure_openai_settings.embedding_deployment_name, + endpoint=azure_openai_settings.endpoint, + base_url=azure_openai_settings.base_url, + api_version=azure_openai_settings.api_version, service_id=service_id, - api_key=api_key, + api_key=azure_openai_settings.api_key.get_secret_value() if azure_openai_settings.api_key else None, ad_token=ad_token, ad_token_provider=ad_token_provider, default_headers=default_headers, diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion.py b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion.py index c4ab84542d58..d808bdd5a8af 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion.py +++ b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion.py @@ -11,6 +11,7 @@ from semantic_kernel.connectors.ai.open_ai.services.open_ai_handler import OpenAIModelTypes from semantic_kernel.connectors.ai.open_ai.services.open_ai_text_completion_base import OpenAITextCompletionBase from semantic_kernel.connectors.ai.open_ai.settings.open_ai_settings import OpenAISettings +from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError logger: logging.Logger = logging.getLogger(__name__) @@ -27,6 +28,7 @@ def __init__( default_headers: Mapping[str, str] | None = None, async_client: AsyncOpenAI | None = None, env_file_path: str | None = None, + env_file_encoding: str | None = None, ) -> None: """Initialize an OpenAIChatCompletion service. @@ -43,25 +45,24 @@ def __init__( async_client (Optional[AsyncOpenAI]): An existing client to use. (Optional) env_file_path (str | None): Use the environment settings file as a fallback to environment variables. (Optional) + env_file_encoding (str | None): The encoding of the environment settings file. (Optional) """ - openai_settings = None try: - openai_settings = OpenAISettings.create(env_file_path=env_file_path) - except ValidationError as e: - logger.warning(f"Failed to load OpenAI pydantic settings: {e}") - - api_key = api_key or ( - openai_settings.api_key.get_secret_value() if openai_settings and openai_settings.api_key else None - ) - org_id = org_id or (openai_settings.org_id if openai_settings and openai_settings.org_id else None) - ai_model_id = ai_model_id or ( - openai_settings.chat_model_id if openai_settings and openai_settings.chat_model_id else None - ) - + openai_settings = OpenAISettings.create( + api_key=api_key, + org_id=org_id, + chat_model_id=ai_model_id, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + except ValidationError as ex: + raise ServiceInitializationError("Failed to create OpenAI settings.", ex) from ex + if not openai_settings.chat_model_id: + raise ServiceInitializationError("The OpenAI chat model ID is required.") super().__init__( - ai_model_id=ai_model_id, - api_key=api_key, - org_id=org_id, + ai_model_id=openai_settings.chat_model_id, + api_key=openai_settings.api_key.get_secret_value() if openai_settings.api_key else None, + org_id=openai_settings.org_id, service_id=service_id, ai_model_type=OpenAIModelTypes.CHAT, default_headers=default_headers, diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_text_completion.py b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_text_completion.py index 66bc45fbed87..cbaa102ee5de 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_text_completion.py +++ b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_text_completion.py @@ -11,6 +11,7 @@ from semantic_kernel.connectors.ai.open_ai.services.open_ai_handler import OpenAIModelTypes from semantic_kernel.connectors.ai.open_ai.services.open_ai_text_completion_base import OpenAITextCompletionBase from semantic_kernel.connectors.ai.open_ai.settings.open_ai_settings import OpenAISettings +from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError logger: logging.Logger = logging.getLogger(__name__) @@ -27,6 +28,7 @@ def __init__( default_headers: Mapping[str, str] | None = None, async_client: AsyncOpenAI | None = None, env_file_path: str | None = None, + env_file_encoding: str | None = None, ) -> None: """Initialize an OpenAITextCompletion service. @@ -43,25 +45,25 @@ def __init__( async_client (Optional[AsyncOpenAI]): An existing client to use. (Optional) env_file_path (str | None): Use the environment settings file as a fallback to environment variables. (Optional) + env_file_encoding (str | None): The encoding of the environment settings file. (Optional) """ try: - openai_settings = OpenAISettings.create(env_file_path=env_file_path) - except ValidationError as e: - logger.warning(f"Failed to load OpenAI pydantic settings: {e}") - - api_key = api_key or ( - openai_settings.api_key.get_secret_value() if openai_settings and openai_settings.api_key else None - ) - org_id = org_id or (openai_settings.org_id if openai_settings and openai_settings.org_id else None) - ai_model_id = ai_model_id or ( - openai_settings.text_model_id if openai_settings and openai_settings.text_model_id else None - ) - + openai_settings = OpenAISettings.create( + api_key=api_key, + org_id=org_id, + text_model_id=ai_model_id, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + except ValidationError as ex: + raise ServiceInitializationError("Failed to create OpenAI settings.", ex) from ex + if not openai_settings.text_model_id: + raise ServiceInitializationError("The OpenAI text model ID is required.") super().__init__( - ai_model_id=ai_model_id, - api_key=api_key, - org_id=org_id, + ai_model_id=openai_settings.text_model_id, service_id=service_id, + api_key=openai_settings.api_key.get_secret_value() if openai_settings.api_key else None, + org_id=openai_settings.org_id, ai_model_type=OpenAIModelTypes.TEXT, default_headers=default_headers, async_client=async_client, diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_text_embedding.py b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_text_embedding.py index 4529bb50e7ff..e85d743e5252 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_text_embedding.py +++ b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_text_embedding.py @@ -10,6 +10,7 @@ from semantic_kernel.connectors.ai.open_ai.services.open_ai_handler import OpenAIModelTypes from semantic_kernel.connectors.ai.open_ai.services.open_ai_text_embedding_base import OpenAITextEmbeddingBase from semantic_kernel.connectors.ai.open_ai.settings.open_ai_settings import OpenAISettings +from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError from semantic_kernel.utils.experimental_decorator import experimental_class logger: logging.Logger = logging.getLogger(__name__) @@ -28,6 +29,7 @@ def __init__( default_headers: Mapping[str, str] | None = None, async_client: AsyncOpenAI | None = None, env_file_path: str | None = None, + env_file_encoding: str | None = None, ) -> None: """Initializes a new instance of the OpenAITextCompletion class. @@ -44,25 +46,25 @@ def __init__( async_client (Optional[AsyncOpenAI]): An existing client to use. (Optional) env_file_path (str | None): Use the environment settings file as a fallback to environment variables. (Optional) + env_file_encoding (str | None): The encoding of the environment settings file. (Optional) """ try: - openai_settings = OpenAISettings.create(env_file_path=env_file_path) - except ValidationError as e: - logger.warning(f"Failed to load OpenAI pydantic settings: {e}") - - api_key = api_key or ( - openai_settings.api_key.get_secret_value() if openai_settings and openai_settings.api_key else None - ) - org_id = org_id or (openai_settings.org_id if openai_settings and openai_settings.org_id else None) - ai_model_id = ai_model_id or ( - openai_settings.embedding_model_id if openai_settings and openai_settings.embedding_model_id else None - ) - + openai_settings = OpenAISettings.create( + api_key=api_key, + org_id=org_id, + embedding_model_id=ai_model_id, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + except ValidationError as ex: + raise ServiceInitializationError("Failed to create OpenAI settings.", ex) from ex + if not openai_settings.embedding_model_id: + raise ServiceInitializationError("The OpenAI embedding model ID is required.") super().__init__( - ai_model_id=ai_model_id, - api_key=api_key, + ai_model_id=openai_settings.embedding_model_id, + api_key=openai_settings.api_key.get_secret_value() if openai_settings.api_key else None, ai_model_type=OpenAIModelTypes.EMBEDDING, - org_id=org_id, + org_id=openai_settings.org_id, service_id=service_id, default_headers=default_headers, async_client=async_client, diff --git a/python/semantic_kernel/connectors/ai/open_ai/settings/azure_open_ai_settings.py b/python/semantic_kernel/connectors/ai/open_ai/settings/azure_open_ai_settings.py index 3a59d707fa9e..891f275bf1f0 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/settings/azure_open_ai_settings.py +++ b/python/semantic_kernel/connectors/ai/open_ai/settings/azure_open_ai_settings.py @@ -1,13 +1,15 @@ # Copyright (c) Microsoft. All rights reserved. +from typing import ClassVar + from pydantic import SecretStr -from pydantic_settings import BaseSettings -from semantic_kernel.kernel_pydantic import HttpsUrl +from semantic_kernel.connectors.ai.open_ai.const import DEFAULT_AZURE_API_VERSION +from semantic_kernel.kernel_pydantic import HttpsUrl, KernelBaseSettings -class AzureOpenAISettings(BaseSettings): +class AzureOpenAISettings(KernelBaseSettings): """AzureOpenAI model settings. The settings are first loaded from environment variables with the prefix 'AZURE_OPENAI_'. @@ -54,29 +56,12 @@ class AzureOpenAISettings(BaseSettings): - env_file_path: str | None - if provided, the .env settings are read from this file path location """ - env_file_path: str | None = None + env_prefix: ClassVar[str] = "AZURE_OPENAI_" + chat_deployment_name: str | None = None text_deployment_name: str | None = None embedding_deployment_name: str | None = None endpoint: HttpsUrl | None = None base_url: HttpsUrl | None = None api_key: SecretStr | None = None - api_version: str | None = None - - class Config: - """Pydantic configuration settings.""" - - env_prefix = "AZURE_OPENAI_" - env_file = None - env_file_encoding = "utf-8" - extra = "ignore" - case_sensitive = False - - @classmethod - def create(cls, **kwargs): - """Create an instance of the class.""" - if "env_file_path" in kwargs and kwargs["env_file_path"]: - cls.Config.env_file = kwargs["env_file_path"] - else: - cls.Config.env_file = None - return cls(**kwargs) + api_version: str = DEFAULT_AZURE_API_VERSION diff --git a/python/semantic_kernel/connectors/ai/open_ai/settings/open_ai_settings.py b/python/semantic_kernel/connectors/ai/open_ai/settings/open_ai_settings.py index a4de3e11bae5..7facb7cbd89c 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/settings/open_ai_settings.py +++ b/python/semantic_kernel/connectors/ai/open_ai/settings/open_ai_settings.py @@ -1,10 +1,13 @@ # Copyright (c) Microsoft. All rights reserved. +from typing import ClassVar + from pydantic import SecretStr -from pydantic_settings import BaseSettings + +from semantic_kernel.kernel_pydantic import KernelBaseSettings -class OpenAISettings(BaseSettings): +class OpenAISettings(KernelBaseSettings): """OpenAI model settings. The settings are first loaded from environment variables with the prefix 'OPENAI_'. If the @@ -26,27 +29,10 @@ class OpenAISettings(BaseSettings): - env_file_path: str | None - if provided, the .env settings are read from this file path location """ - env_file_path: str | None = None + env_prefix: ClassVar[str] = "OPENAI_" + + api_key: SecretStr org_id: str | None = None - api_key: SecretStr | None = None chat_model_id: str | None = None text_model_id: str | None = None embedding_model_id: str | None = None - - class Config: - """Pydantic configuration settings.""" - - env_prefix = "OPENAI_" - env_file = None - env_file_encoding = "utf-8" - extra = "ignore" - case_sensitive = False - - @classmethod - def create(cls, **kwargs): - """Create an instance of the class.""" - if "env_file_path" in kwargs and kwargs["env_file_path"]: - cls.Config.env_file = kwargs["env_file_path"] - else: - cls.Config.env_file = None - return cls(**kwargs) diff --git a/python/semantic_kernel/connectors/memory/astradb/astradb_memory_store.py b/python/semantic_kernel/connectors/memory/astradb/astradb_memory_store.py index a48995a599f8..a6f74594fc19 100644 --- a/python/semantic_kernel/connectors/memory/astradb/astradb_memory_store.py +++ b/python/semantic_kernel/connectors/memory/astradb/astradb_memory_store.py @@ -2,17 +2,11 @@ import asyncio import logging -import sys import aiohttp from numpy import ndarray from pydantic import ValidationError -if sys.version_info >= (3, 12): - pass -else: - pass - from semantic_kernel.connectors.memory.astradb.astra_client import AstraClient from semantic_kernel.connectors.memory.astradb.astradb_settings import AstraDBSettings from semantic_kernel.connectors.memory.astradb.utils import build_payload, parse_payload @@ -45,6 +39,7 @@ def __init__( similarity: str, session: aiohttp.ClientSession | None = None, env_file_path: str | None = None, + env_file_encoding: str | None = None, ) -> None: """Initializes a new instance of the AstraDBMemoryStore class. @@ -58,32 +53,19 @@ def __init__( session: Optional session parameter env_file_path (str | None): Use the environment settings file as a fallback to environment variables. (Optional) + env_file_encoding (str | None): The encoding of the environment settings file. (Optional) """ - astradb_settings = None try: - astradb_settings = AstraDBSettings.create(env_file_path=env_file_path) - except ValidationError as e: - logger.warning(f"Failed to load AstraDB pydantic settings: {e}") - - # Load the settings and validate - astra_application_token = astra_application_token or ( - astradb_settings.app_token.get_secret_value() if astradb_settings and astradb_settings.app_token else None - ) - if astra_application_token is None: - raise ValueError("The astra_application_token cannot be None.") - astra_id = astra_id or (astradb_settings.db_id if astradb_settings and astradb_settings.db_id else None) - if astra_id is None: - raise ValueError("The astra_id cannot be None.") - astra_region = astra_region or ( - astradb_settings.region if astradb_settings and astradb_settings.region else None - ) - if astra_region is None: - raise ValueError("The astra_region cannot be None.") - keyspace_name = keyspace_name or ( - astradb_settings.keyspace if astradb_settings and astradb_settings.keyspace else None - ) - if keyspace_name is None: - raise ValueError("The keyspace_name cannot be None.") + astradb_settings = AstraDBSettings.create( + app_token=astra_application_token, + db_id=astra_id, + region=astra_region, + keyspace=keyspace_name, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + except ValidationError as ex: + raise MemoryConnectorInitializationError("Failed to create AstraDB settings.", ex) from ex self._embedding_dim = embedding_dim self._similarity = similarity @@ -96,10 +78,12 @@ def __init__( ) self._client = AstraClient( - astra_id=astra_id, - astra_region=astra_region, - astra_application_token=astra_application_token, - keyspace_name=keyspace_name, + astra_id=astradb_settings.db_id, + astra_region=astradb_settings.region, + astra_application_token=( + astradb_settings.app_token.get_secret_value() if astradb_settings.app_token else None + ), + keyspace_name=astradb_settings.keyspace, embedding_dim=embedding_dim, similarity_function=similarity, session=self._session, diff --git a/python/semantic_kernel/connectors/memory/astradb/astradb_settings.py b/python/semantic_kernel/connectors/memory/astradb/astradb_settings.py index 18fa062735e1..e3d190187f4c 100644 --- a/python/semantic_kernel/connectors/memory/astradb/astradb_settings.py +++ b/python/semantic_kernel/connectors/memory/astradb/astradb_settings.py @@ -1,32 +1,27 @@ # Copyright (c) Microsoft. All rights reserved. +from typing import ClassVar + from pydantic import SecretStr -from semantic_kernel.connectors.memory.memory_settings_base import BaseModelSettings +from semantic_kernel.kernel_pydantic import KernelBaseSettings from semantic_kernel.utils.experimental_decorator import experimental_class @experimental_class -class AstraDBSettings(BaseModelSettings): +class AstraDBSettings(KernelBaseSettings): """AstraDB model settings. - Optional: - - app_token: SecretStr | None - AstraDB token - (Env var ASTRADB_APP_TOKEN) - - db_id: str | None - AstraDB database ID - (Env var ASTRADB_DB_ID) - - region: str | None - AstraDB region - (Env var ASTRADB_REGION) - - keyspace: str | None - AstraDB keyspace - (Env var ASTRADB_KEYSPACE) + Settings for AstraDB connection: + - app_token: SecretStr | None - AstraDB token (Env var ASTRADB_APP_TOKEN) + - db_id: str | None - AstraDB database ID (Env var ASTRADB_DB_ID) + - region: str | None - AstraDB region (Env var ASTRADB_REGION) + - keyspace: str | None - AstraDB keyspace (Env var ASTRADB_KEYSPACE) """ + env_prefix: ClassVar[str] = "ASTRADB_" + app_token: SecretStr db_id: str region: str keyspace: str - - class Config(BaseModelSettings.Config): - """Pydantic configuration settings.""" - - env_prefix = "ASTRADB_" diff --git a/python/semantic_kernel/connectors/memory/azure_cognitive_search/azure_ai_search_settings.py b/python/semantic_kernel/connectors/memory/azure_cognitive_search/azure_ai_search_settings.py index 76edcd688c18..f04af95caf4c 100644 --- a/python/semantic_kernel/connectors/memory/azure_cognitive_search/azure_ai_search_settings.py +++ b/python/semantic_kernel/connectors/memory/azure_cognitive_search/azure_ai_search_settings.py @@ -1,14 +1,15 @@ # Copyright (c) Microsoft. All rights reserved. +from typing import ClassVar + from pydantic import SecretStr -from semantic_kernel.connectors.memory.memory_settings_base import BaseModelSettings -from semantic_kernel.kernel_pydantic import HttpsUrl +from semantic_kernel.kernel_pydantic import HttpsUrl, KernelBaseSettings from semantic_kernel.utils.experimental_decorator import experimental_class @experimental_class -class AzureAISearchSettings(BaseModelSettings): +class AzureAISearchSettings(KernelBaseSettings): """Azure AI Search model settings currently used by the AzureCognitiveSearchMemoryStore connector. Args: @@ -17,19 +18,8 @@ class AzureAISearchSettings(BaseModelSettings): - index_name: str - Azure AI Search index name (Env var AZURE_AI_SEARCH_INDEX_NAME) """ - api_key: SecretStr | None = None - endpoint: HttpsUrl | None = None - index_name: str | None = None - - class Config(BaseModelSettings.Config): - """Pydantic configuration settings.""" + env_prefix: ClassVar[str] = "AZURE_AI_SEARCH_" - env_prefix = "AZURE_AI_SEARCH_" - - def model_dump(self): - """Custom method to dump model data in the required format.""" - return { - "api_key": self.api_key.get_secret_value() if self.api_key else None, - "endpoint": str(self.endpoint), - "index_name": self.index_name, - } + api_key: SecretStr + endpoint: HttpsUrl + index_name: str | None = None diff --git a/python/semantic_kernel/connectors/memory/azure_cognitive_search/azure_cognitive_search_memory_store.py b/python/semantic_kernel/connectors/memory/azure_cognitive_search/azure_cognitive_search_memory_store.py index 79765332a900..cbb49fd09956 100644 --- a/python/semantic_kernel/connectors/memory/azure_cognitive_search/azure_cognitive_search_memory_store.py +++ b/python/semantic_kernel/connectors/memory/azure_cognitive_search/azure_cognitive_search_memory_store.py @@ -50,46 +50,47 @@ def __init__( azure_credentials: AzureKeyCredential | None = None, token_credentials: TokenCredential | None = None, env_file_path: str | None = None, + env_file_encoding: str | None = None, ) -> None: """Initializes a new instance of the AzureCognitiveSearchMemoryStore class. + Instantiate using Async Context Manager: + async with AzureCognitiveSearchMemoryStore(<...>) as memory: + await memory.<...> + Args: vector_size (int): Embedding vector size. search_endpoint (str | None): The endpoint of the Azure Cognitive Search service - (default: {None}). + (default: {None}). admin_key (str | None): Azure Cognitive Search API key (default: {None}). azure_credentials (AzureKeyCredential | None): Azure Cognitive Search credentials (default: {None}). token_credentials (TokenCredential | None): Azure Cognitive Search token credentials - (default: {None}). + (default: {None}). env_file_path (str | None): Use the environment settings file as a fallback - to environment variables + to environment variables + env_file_encoding (str | None): The encoding of the environment settings file - Instantiate using Async Context Manager: - async with AzureCognitiveSearchMemoryStore(<...>) as memory: - await memory.<...> """ - from semantic_kernel.connectors.memory.azure_cognitive_search import AzureAISearchSettings + from semantic_kernel.connectors.memory.azure_cognitive_search.azure_ai_search_settings import ( + AzureAISearchSettings, + ) - acs_memory_settings = None try: - acs_memory_settings = AzureAISearchSettings.create(env_file_path=env_file_path) - except ValidationError as e: - logger.warning(f"Failed to load AzureAISearch pydantic settings: {e}") - - admin_key = admin_key or ( - acs_memory_settings.api_key.get_secret_value() - if acs_memory_settings and acs_memory_settings.api_key - else None - ) - search_endpoint = search_endpoint or ( - acs_memory_settings.endpoint if acs_memory_settings and acs_memory_settings.endpoint else None - ) - if not search_endpoint: - raise ValueError("The ACS endpoint is required to connect to Azure Cognitive Search.") + acs_memory_settings = AzureAISearchSettings.create( + env_file_path=env_file_path, + endpoint=search_endpoint, + api_key=admin_key, + env_file_encoding=env_file_encoding, + ) + except ValidationError as exc: + raise MemoryConnectorInitializationError("Failed to create Azure Cognitive Search settings.") from exc self._vector_size = vector_size self._search_index_client = get_search_index_async_client( - str(search_endpoint), admin_key, azure_credentials, token_credentials + search_endpoint=str(acs_memory_settings.endpoint), + admin_key=acs_memory_settings.api_key.get_secret_value(), + azure_credential=azure_credentials, + token_credential=token_credentials, ) async def close(self): diff --git a/python/semantic_kernel/connectors/memory/azure_cosmosdb/azure_cosmos_db_memory_store.py b/python/semantic_kernel/connectors/memory/azure_cosmosdb/azure_cosmos_db_memory_store.py index c8eea3d9b775..40150463b40e 100644 --- a/python/semantic_kernel/connectors/memory/azure_cosmosdb/azure_cosmos_db_memory_store.py +++ b/python/semantic_kernel/connectors/memory/azure_cosmosdb/azure_cosmos_db_memory_store.py @@ -1,18 +1,18 @@ # Copyright (c) Microsoft. All rights reserved. import logging +from typing import Literal from numpy import ndarray -from pydantic import ValidationError +from pymongo import MongoClient from semantic_kernel.connectors.memory.azure_cosmosdb.azure_cosmos_db_store_api import AzureCosmosDBStoreApi from semantic_kernel.connectors.memory.azure_cosmosdb.azure_cosmosdb_settings import AzureCosmosDBSettings -from semantic_kernel.connectors.memory.azure_cosmosdb.cosmosdb_utils import ( +from semantic_kernel.connectors.memory.azure_cosmosdb.mongo_vcore_store_api import MongoStoreApi +from semantic_kernel.connectors.memory.azure_cosmosdb.utils import ( CosmosDBSimilarityType, CosmosDBVectorSearchType, - get_mongodb_search_client, ) -from semantic_kernel.connectors.memory.azure_cosmosdb.mongo_vcore_store_api import MongoStoreApi from semantic_kernel.exceptions import MemoryConnectorInitializationError from semantic_kernel.memory.memory_record import MemoryRecord from semantic_kernel.memory.memory_store_base import MemoryStoreBase @@ -75,40 +75,36 @@ def __init__( @staticmethod async def create( - cosmos_connstr, - application_name, - cosmos_api, - database_name, - collection_name, - index_name, - vector_dimensions, - num_lists, - similarity, - kind, - m, - ef_construction, - ef_search, + database_name: str, + collection_name: str, + vector_dimensions: int, + num_lists: int, + similarity: CosmosDBSimilarityType, + kind: CosmosDBVectorSearchType, + m: int, + ef_construction: int, + ef_search: int, + index_name: str | None = None, + cosmos_connstr: str | None = None, + application_name: str | None = None, + cosmos_api: Literal["mongo-vcore"] = "mongo-vcore", env_file_path: str | None = None, ) -> MemoryStoreBase: """Creates the underlying data store based on the API definition.""" # Right now this only supports Mongo, but set up to support more later. - apiStore: AzureCosmosDBStoreApi = None + api_store: AzureCosmosDBStoreApi = None if cosmos_api == "mongo-vcore": - cosmosdb_settings = None - try: - cosmosdb_settings = AzureCosmosDBSettings.create(env_file_path=env_file_path) - except ValidationError as e: - logger.warning(f"Failed to load AzureCosmosDB pydantic settings: {e}") - - cosmos_connstr = cosmos_connstr or ( - cosmosdb_settings.connection_string.get_secret_value() - if cosmosdb_settings and cosmosdb_settings.connection_string - else None + cosmosdb_settings = AzureCosmosDBSettings.create( + env_file_path=env_file_path, + connection_string=cosmos_connstr, ) - mongodb_client = get_mongodb_search_client(cosmos_connstr, application_name) + mongodb_client = MongoClient( + cosmosdb_settings.connection_string.get_secret_value() if cosmosdb_settings.connection_string else None, + appname=application_name, + ) database = mongodb_client[database_name] - apiStore = MongoStoreApi( + api_store = MongoStoreApi( collection_name=collection_name, index_name=index_name, vector_dimensions=vector_dimensions, @@ -124,7 +120,7 @@ async def create( raise MemoryConnectorInitializationError(f"API type {cosmos_api} is not supported.") store = AzureCosmosDBMemoryStore( - apiStore, + api_store, database_name, index_name, vector_dimensions, diff --git a/python/semantic_kernel/connectors/memory/azure_cosmosdb/azure_cosmos_db_store_api.py b/python/semantic_kernel/connectors/memory/azure_cosmosdb/azure_cosmos_db_store_api.py index fcacfdd5516e..68fdd93e272b 100644 --- a/python/semantic_kernel/connectors/memory/azure_cosmosdb/azure_cosmos_db_store_api.py +++ b/python/semantic_kernel/connectors/memory/azure_cosmosdb/azure_cosmos_db_store_api.py @@ -1,6 +1,5 @@ # Copyright (c) Microsoft. All rights reserved. - from abc import ABC, abstractmethod from numpy import ndarray diff --git a/python/semantic_kernel/connectors/memory/azure_cosmosdb/azure_cosmosdb_settings.py b/python/semantic_kernel/connectors/memory/azure_cosmosdb/azure_cosmosdb_settings.py index 0ee064b2d0d8..6cbd1ae049f2 100644 --- a/python/semantic_kernel/connectors/memory/azure_cosmosdb/azure_cosmosdb_settings.py +++ b/python/semantic_kernel/connectors/memory/azure_cosmosdb/azure_cosmosdb_settings.py @@ -1,24 +1,24 @@ # Copyright (c) Microsoft. All rights reserved. -from pydantic import SecretStr +from typing import ClassVar -from semantic_kernel.connectors.memory.memory_settings_base import BaseModelSettings +from pydantic import Field, SecretStr + +from semantic_kernel.kernel_pydantic import KernelBaseSettings from semantic_kernel.utils.experimental_decorator import experimental_class @experimental_class -class AzureCosmosDBSettings(BaseModelSettings): +class AzureCosmosDBSettings(KernelBaseSettings): """Azure CosmosDB model settings. - Args: + Optional: + - api: str - Azure CosmosDB API version (Env var COSMOSDB_API) - connection_string: str - Azure CosmosDB connection string (Env var COSMOSDB_CONNECTION_STRING) """ - api: str | None = None - connection_string: SecretStr | None = None - - class Config(BaseModelSettings.Config): - """Pydantic configuration settings.""" + env_prefix: ClassVar[str] = "COSMOSDB_" - env_prefix = "COSMOSDB_" + api: str | None = None + connection_string: SecretStr | None = Field(None, alias="AZCOSMOS_CONNSTR") diff --git a/python/semantic_kernel/connectors/memory/azure_cosmosdb/cosmosdb_utils.py b/python/semantic_kernel/connectors/memory/azure_cosmosdb/cosmosdb_utils.py deleted file mode 100644 index 0e28647fd569..000000000000 --- a/python/semantic_kernel/connectors/memory/azure_cosmosdb/cosmosdb_utils.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. -import os -from enum import Enum - -from dotenv import load_dotenv -from pymongo import MongoClient - -from semantic_kernel.connectors.telemetry import HTTP_USER_AGENT -from semantic_kernel.exceptions import ServiceInitializationError -from semantic_kernel.utils.experimental_decorator import experimental_function - - -@experimental_function -class CosmosDBSimilarityType(str, Enum): - """Cosmos DB Similarity Type as enumerator.""" - - COS = "COS" - """CosineSimilarity""" - IP = "IP" - """inner - product""" - L2 = "L2" - """Euclidean distance""" - - -@experimental_function -class CosmosDBVectorSearchType(str, Enum): - """Cosmos DB Vector Search Type as enumerator.""" - - VECTOR_IVF = "vector-ivf" - """IVF vector index""" - VECTOR_HNSW = "vector-hnsw" - """HNSW vector index""" - - -@experimental_function -def get_mongodb_search_client(connection_string: str, application_name: str): - """Returns a client for Azure Cosmos Mongo vCore Vector DB. - - Args: - connection_string (str): The connection string for the Azure Cosmos Mongo vCore Vector DB. - application_name (str): The name of the application. - - """ - ENV_VAR_COSMOS_CONN_STR = "AZCOSMOS_CONNSTR" - - load_dotenv() - - # Cosmos connection string - if connection_string: - cosmos_conn_str = connection_string - elif os.getenv(ENV_VAR_COSMOS_CONN_STR): - cosmos_conn_str = os.getenv(ENV_VAR_COSMOS_CONN_STR) - else: - raise ServiceInitializationError("Error: missing Azure Cosmos Mongo vCore Connection String") - - if cosmos_conn_str: - app_name = application_name if application_name is not None else HTTP_USER_AGENT - return MongoClient(cosmos_conn_str, appname=app_name) - - raise ServiceInitializationError("Error: unable to create Azure Cosmos Mongo vCore Vector DB client.") diff --git a/python/semantic_kernel/connectors/memory/azure_cosmosdb/mongo_vcore_store_api.py b/python/semantic_kernel/connectors/memory/azure_cosmosdb/mongo_vcore_store_api.py index 8e2db4ba8209..4f829839bf84 100644 --- a/python/semantic_kernel/connectors/memory/azure_cosmosdb/mongo_vcore_store_api.py +++ b/python/semantic_kernel/connectors/memory/azure_cosmosdb/mongo_vcore_store_api.py @@ -4,18 +4,15 @@ import sys from typing import Any -import numpy as np - -if sys.version_info >= (3, 12): +if sys.version >= "3.12": from typing import override else: from typing_extensions import override +import numpy as np + from semantic_kernel.connectors.memory.azure_cosmosdb.azure_cosmos_db_store_api import AzureCosmosDBStoreApi -from semantic_kernel.connectors.memory.azure_cosmosdb.cosmosdb_utils import ( - CosmosDBSimilarityType, - CosmosDBVectorSearchType, -) +from semantic_kernel.connectors.memory.azure_cosmosdb.utils import CosmosDBSimilarityType, CosmosDBVectorSearchType from semantic_kernel.memory.memory_record import MemoryRecord from semantic_kernel.utils.experimental_decorator import experimental_class diff --git a/python/semantic_kernel/connectors/memory/azure_cosmosdb/utils.py b/python/semantic_kernel/connectors/memory/azure_cosmosdb/utils.py new file mode 100644 index 000000000000..8c0cd782e1af --- /dev/null +++ b/python/semantic_kernel/connectors/memory/azure_cosmosdb/utils.py @@ -0,0 +1,27 @@ +# Copyright (c) Microsoft. All rights reserved. + +from enum import Enum + +from semantic_kernel.utils.experimental_decorator import experimental_function + + +@experimental_function +class CosmosDBSimilarityType(str, Enum): + """Cosmos DB Similarity Type as enumerator.""" + + COS = "COS" + """CosineSimilarity""" + IP = "IP" + """inner - product""" + L2 = "L2" + """Euclidean distance""" + + +@experimental_function +class CosmosDBVectorSearchType(str, Enum): + """Cosmos DB Vector Search Type as enumerator.""" + + VECTOR_IVF = "vector-ivf" + """IVF vector index""" + VECTOR_HNSW = "vector-hnsw" + """HNSW vector index""" diff --git a/python/semantic_kernel/connectors/memory/memory_settings_base.py b/python/semantic_kernel/connectors/memory/memory_settings_base.py deleted file mode 100644 index 084f82cd78ed..000000000000 --- a/python/semantic_kernel/connectors/memory/memory_settings_base.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -from pydantic_settings import BaseSettings - -from semantic_kernel.utils.experimental_decorator import experimental_class - - -@experimental_class -class BaseModelSettings(BaseSettings): - env_file_path: str | None = None - - class Config: - """Pydantic configuration settings.""" - - env_file = None - env_file_encoding = "utf-8" - extra = "ignore" - case_sensitive = False - - @classmethod - def create(cls, **kwargs): - """Create an instance of the class.""" - if "env_file_path" in kwargs and kwargs["env_file_path"]: - cls.Config.env_file = kwargs["env_file_path"] - else: - cls.Config.env_file = None - return cls(**kwargs) diff --git a/python/semantic_kernel/connectors/memory/mongodb_atlas/mongodb_atlas_memory_store.py b/python/semantic_kernel/connectors/memory/mongodb_atlas/mongodb_atlas_memory_store.py index 289d98e0f544..9c57c3341d28 100644 --- a/python/semantic_kernel/connectors/memory/mongodb_atlas/mongodb_atlas_memory_store.py +++ b/python/semantic_kernel/connectors/memory/mongodb_atlas/mongodb_atlas_memory_store.py @@ -12,8 +12,6 @@ from pymongo.driver_info import DriverInfo from semantic_kernel.connectors.memory.mongodb_atlas.utils import ( - DEFAULT_DB_NAME, - DEFAULT_SEARCH_INDEX_NAME, MONGODB_FIELD_EMBEDDING, MONGODB_FIELD_ID, NUM_CANDIDATES_SCALAR, @@ -21,6 +19,7 @@ memory_record_to_mongo_document, ) from semantic_kernel.exceptions import ServiceResourceNotFoundError +from semantic_kernel.exceptions.memory_connector_exceptions import MemoryConnectorInitializationError from semantic_kernel.memory.memory_record import MemoryRecord from semantic_kernel.memory.memory_store_base import MemoryStoreBase from semantic_kernel.utils.experimental_decorator import experimental_class @@ -32,12 +31,6 @@ class MongoDBAtlasMemoryStore(MemoryStoreBase): """Memory Store for MongoDB Atlas Vector Search Connections.""" - __slots__ = ("_mongo_client", "__database_name") - - _mongo_client: motor_asyncio.AsyncIOMotorClient - __database_name: str - __index_name: str - def __init__( self, index_name: str | None = None, @@ -45,44 +38,44 @@ def __init__( database_name: str | None = None, read_preference: ReadPreference | None = ReadPreference.PRIMARY, env_file_path: str | None = None, + env_file_encoding: str | None = None, ): - """Initializes a new instance of the MongoDBAtlasMemoryStore class.""" - from semantic_kernel.connectors.memory.mongodb_atlas import MongoDBAtlasSettings + """Create the MongoDB Atlas Memory Store. + + Args: + index_name (str): The name of the index. + connection_string (str): The connection string for the MongoDB Atlas instance. + database_name (str): The name of the database. + read_preference (ReadPreference): The read preference for the connection. + env_file_path (str): The path to the .env file containing the connection string. + env_file_encoding (str): The encoding of the .env file. + + """ + from semantic_kernel.connectors.memory.mongodb_atlas.mongodb_atlas_settings import MongoDBAtlasSettings - mongodb_settings = None try: - mongodb_settings = MongoDBAtlasSettings.create(env_file_path=env_file_path) - except ValidationError as e: - logger.warning(f"Failed to load the MongoDBAtlas pydantic settings: {e}") - - connection_string = connection_string or ( - mongodb_settings.connection_string.get_secret_value() - if mongodb_settings and mongodb_settings.connection_string - else None - ) + mongodb_settings = MongoDBAtlasSettings.create( + database_name=database_name, + index_name=index_name, + connection_string=connection_string, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + except ValidationError as ex: + raise MemoryConnectorInitializationError("Failed to create MongoDB Atlas settings.") from ex - self._mongo_client = motor_asyncio.AsyncIOMotorClient( - connection_string, + self.mongo_client: motor_asyncio.AsyncIOMotorClient = motor_asyncio.AsyncIOMotorClient( + mongodb_settings.connection_string.get_secret_value(), read_preference=read_preference, driver=DriverInfo("Microsoft Semantic Kernel", metadata.version("semantic-kernel")), ) - self.__database_name = database_name or DEFAULT_DB_NAME - self.__index_name = index_name or DEFAULT_SEARCH_INDEX_NAME - - @property - def database_name(self) -> str: - """The name of the database.""" - return self.__database_name + self.database_name: str = mongodb_settings.database_name + self.index_name: str = mongodb_settings.index_name @property def database(self) -> core.AgnosticDatabase: """The database object.""" - return self._mongo_client[self.database_name] - - @property - def index_name(self) -> str: - """The name of the index.""" - return self.__index_name + return self.mongo_client[self.database_name] @property def num_candidates(self) -> int: @@ -91,9 +84,9 @@ def num_candidates(self) -> int: async def close(self): """Async close connection, invoked by MemoryStoreBase.__aexit__().""" - if self._mongo_client: - self._mongo_client.close() - self._mongo_client = None + if self.mongo_client: + self.mongo_client.close() + del self.mongo_client async def create_collection(self, collection_name: str) -> None: """Creates a new collection in the data store. @@ -333,6 +326,3 @@ async def get_nearest_match( ) return matches[0] if matches else None - - -__all__ = ["MongoDBAtlasMemoryStore"] diff --git a/python/semantic_kernel/connectors/memory/mongodb_atlas/mongodb_atlas_settings.py b/python/semantic_kernel/connectors/memory/mongodb_atlas/mongodb_atlas_settings.py index 9f1dda5bcb74..0eec5591d15f 100644 --- a/python/semantic_kernel/connectors/memory/mongodb_atlas/mongodb_atlas_settings.py +++ b/python/semantic_kernel/connectors/memory/mongodb_atlas/mongodb_atlas_settings.py @@ -1,13 +1,16 @@ # Copyright (c) Microsoft. All rights reserved. +from typing import ClassVar + from pydantic import SecretStr -from semantic_kernel.connectors.memory.memory_settings_base import BaseModelSettings +from semantic_kernel.connectors.memory.mongodb_atlas.utils import DEFAULT_DB_NAME, DEFAULT_SEARCH_INDEX_NAME +from semantic_kernel.kernel_pydantic import KernelBaseSettings from semantic_kernel.utils.experimental_decorator import experimental_class @experimental_class -class MongoDBAtlasSettings(BaseModelSettings): +class MongoDBAtlasSettings(KernelBaseSettings): """MongoDB Atlas model settings. Args: @@ -15,9 +18,8 @@ class MongoDBAtlasSettings(BaseModelSettings): (Env var MONGODB_ATLAS_CONNECTION_STRING) """ - connection_string: SecretStr | None = None - - class Config(BaseModelSettings.Config): - """Pydantic configuration settings.""" + env_prefix: ClassVar[str] = "MONGODB_ATLAS_" - env_prefix = "MONGODB_ATLAS_" + connection_string: SecretStr + database_name: str = DEFAULT_DB_NAME + index_name: str = DEFAULT_SEARCH_INDEX_NAME diff --git a/python/semantic_kernel/connectors/memory/pinecone/pinecone_memory_store.py b/python/semantic_kernel/connectors/memory/pinecone/pinecone_memory_store.py index 815bbdd4e1c6..7e191a8ebe0c 100644 --- a/python/semantic_kernel/connectors/memory/pinecone/pinecone_memory_store.py +++ b/python/semantic_kernel/connectors/memory/pinecone/pinecone_memory_store.py @@ -15,6 +15,7 @@ ServiceResourceNotFoundError, ServiceResponseException, ) +from semantic_kernel.exceptions.memory_connector_exceptions import MemoryConnectorInitializationError from semantic_kernel.memory.memory_record import MemoryRecord from semantic_kernel.memory.memory_store_base import MemoryStoreBase from semantic_kernel.utils.experimental_decorator import experimental_class @@ -34,7 +35,6 @@ class PineconeMemoryStore(MemoryStoreBase): """A memory store that uses Pinecone as the backend.""" - _pinecone_api_key: str _default_dimensionality: int DEFAULT_INDEX_SPEC: ServerlessSpec = ServerlessSpec( @@ -47,6 +47,7 @@ def __init__( api_key: str, default_dimensionality: int, env_file_path: str | None = None, + env_file_encoding: str | None = None, ) -> None: """Initializes a new instance of the PineconeMemoryStore class. @@ -55,29 +56,25 @@ def __init__( default_dimensionality (int): The default dimensionality to use for new collections. env_file_path (str | None): Use the environment settings file as a fallback to environment variables. (Optional) + env_file_encoding (str | None): The encoding of the environment settings file. (Optional) """ if default_dimensionality > MAX_DIMENSIONALITY: - raise ServiceInitializationError( + raise MemoryConnectorInitializationError( f"Dimensionality of {default_dimensionality} exceeds " + f"the maximum allowed value of {MAX_DIMENSIONALITY}." ) - - pinecone_settings = None try: - pinecone_settings = PineconeSettings.create(env_file_path=env_file_path) - except ValidationError as e: - logger.warning(f"Failed to load the Pinecone pydantic settings: {e}") - - api_key = api_key or ( - pinecone_settings.api_key.get_secret_value() if pinecone_settings and pinecone_settings.api_key else None - ) - if not api_key: - raise ValueError("The Pinecone api_key cannot be None.") + pinecone_settings = PineconeSettings.create( + api_key=api_key, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + except ValidationError as ex: + raise MemoryConnectorInitializationError("Failed to create Pinecone settings.", ex) from ex - self._pinecone_api_key = api_key self._default_dimensionality = default_dimensionality - self.pinecone = Pinecone(api_key=self._pinecone_api_key) + self.pinecone = Pinecone(api_key=pinecone_settings.api_key.get_secret_value()) self.collection_names_cache = set() async def create_collection( diff --git a/python/semantic_kernel/connectors/memory/pinecone/pinecone_settings.py b/python/semantic_kernel/connectors/memory/pinecone/pinecone_settings.py index efd5331548ed..db2cd99ef88b 100644 --- a/python/semantic_kernel/connectors/memory/pinecone/pinecone_settings.py +++ b/python/semantic_kernel/connectors/memory/pinecone/pinecone_settings.py @@ -1,13 +1,15 @@ # Copyright (c) Microsoft. All rights reserved. +from typing import ClassVar + from pydantic import SecretStr -from semantic_kernel.connectors.memory.memory_settings_base import BaseModelSettings +from semantic_kernel.kernel_pydantic import KernelBaseSettings from semantic_kernel.utils.experimental_decorator import experimental_class @experimental_class -class PineconeSettings(BaseModelSettings): +class PineconeSettings(KernelBaseSettings): """Pinecone model settings. Args: @@ -15,9 +17,6 @@ class PineconeSettings(BaseModelSettings): (Env var PINECONE_API_KEY) """ - api_key: SecretStr | None = None - - class Config(BaseModelSettings.Config): - """Config for Pinecone settings.""" + env_prefix: ClassVar[str] = "PINECONE_" - env_prefix = "PINECONE_" + api_key: SecretStr diff --git a/python/semantic_kernel/connectors/memory/postgres/postgres_memory_store.py b/python/semantic_kernel/connectors/memory/postgres/postgres_memory_store.py index 14e68cd6c1ec..05ccaaaafd1e 100644 --- a/python/semantic_kernel/connectors/memory/postgres/postgres_memory_store.py +++ b/python/semantic_kernel/connectors/memory/postgres/postgres_memory_store.py @@ -17,6 +17,7 @@ ServiceResourceNotFoundError, ServiceResponseException, ) +from semantic_kernel.exceptions.memory_connector_exceptions import MemoryConnectorInitializationError from semantic_kernel.memory.memory_record import MemoryRecord from semantic_kernel.memory.memory_store_base import MemoryStoreBase from semantic_kernel.utils.experimental_decorator import experimental_class @@ -45,6 +46,7 @@ def __init__( max_pool: int, schema: str = DEFAULT_SCHEMA, env_file_path: str | None = None, + env_file_encoding: str | None = None, ) -> None: """Initializes a new instance of the PostgresMemoryStore class. @@ -56,24 +58,23 @@ def __init__( schema (str): The schema to use. (default: {"public"}) env_file_path (str | None): Use the environment settings file as a fallback to environment variables. (Optional) + env_file_encoding (str | None): The encoding of the environment settings file. """ - postgres_settings = None try: - postgres_settings = PostgresSettings.create(env_file_path=env_file_path) - except ValidationError as e: - logger.warning(f"Failed to load Postgres pydantic settings: {e}") - - connection_string = connection_string or ( - postgres_settings.connection_string.get_secret_value() - if postgres_settings and postgres_settings.connection_string - else None - ) + postgres_settings = PostgresSettings.create( + connection_string=connection_string, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + except ValidationError as ex: + raise MemoryConnectorInitializationError("Failed to create Postgres settings.", ex) from ex self._check_dimensionality(default_dimensionality) - self._connection_string = connection_string self._default_dimensionality = default_dimensionality - self._connection_pool = ConnectionPool(self._connection_string, min_size=min_pool, max_size=max_pool) + self._connection_pool = ConnectionPool( + postgres_settings.connection_string.get_secret_value(), min_size=min_pool, max_size=max_pool + ) self._schema = schema atexit.register(self._connection_pool.close) diff --git a/python/semantic_kernel/connectors/memory/postgres/postgres_settings.py b/python/semantic_kernel/connectors/memory/postgres/postgres_settings.py index 207e2dcdcbdf..c53205e424ab 100644 --- a/python/semantic_kernel/connectors/memory/postgres/postgres_settings.py +++ b/python/semantic_kernel/connectors/memory/postgres/postgres_settings.py @@ -1,13 +1,15 @@ # Copyright (c) Microsoft. All rights reserved. +from typing import ClassVar + from pydantic import SecretStr -from semantic_kernel.connectors.memory.memory_settings_base import BaseModelSettings +from semantic_kernel.kernel_pydantic import KernelBaseSettings from semantic_kernel.utils.experimental_decorator import experimental_class @experimental_class -class PostgresSettings(BaseModelSettings): +class PostgresSettings(KernelBaseSettings): """Postgres model settings. Args: @@ -15,9 +17,6 @@ class PostgresSettings(BaseModelSettings): (Env var POSTGRES_CONNECTION_STRING) """ - connection_string: SecretStr | None = None - - class Config(BaseModelSettings.Config): - """Config for Postgres settings.""" + env_prefix: ClassVar[str] = "ASTRADB_" - env_prefix = "POSTGRES_" + connection_string: SecretStr diff --git a/python/semantic_kernel/connectors/memory/redis/redis_memory_store.py b/python/semantic_kernel/connectors/memory/redis/redis_memory_store.py index 34617b7710d3..19a361d51f50 100644 --- a/python/semantic_kernel/connectors/memory/redis/redis_memory_store.py +++ b/python/semantic_kernel/connectors/memory/redis/redis_memory_store.py @@ -19,10 +19,10 @@ serialize_record_to_redis, ) from semantic_kernel.exceptions import ( - ServiceInitializationError, ServiceResourceNotFoundError, ServiceResponseException, ) +from semantic_kernel.exceptions.memory_connector_exceptions import MemoryConnectorInitializationError from semantic_kernel.memory.memory_record import MemoryRecord from semantic_kernel.memory.memory_store_base import MemoryStoreBase from semantic_kernel.utils.experimental_decorator import experimental_class @@ -54,6 +54,7 @@ def __init__( vector_index_algorithm: str = "HNSW", query_dialect: int = 2, env_file_path: str | None = None, + env_file_encoding: str | None = None, ) -> None: """RedisMemoryStore is an abstracted interface to interact with a Redis node connection. @@ -69,23 +70,21 @@ def __init__( query_dialect (int): Query dialect, must be 2 or greater for vector similarity searching, defaults to 2 env_file_path (str | None): Use the environment settings file as a fallback to environment variables, defaults to False + env_file_encoding (str | None): Encoding of the environment settings file, defaults to "utf-8" """ - redis_settings = None try: - redis_settings = RedisSettings.create(env_file_path=env_file_path) - except ValidationError as e: - logger.warning(f"Failed to load Redis pydantic settings: {e}") - - connection_string = connection_string or ( - redis_settings.connection_string.get_secret_value() - if redis_settings and redis_settings.connection_string - else None - ) + redis_settings = RedisSettings.create( + connection_string=connection_string, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + except ValidationError as ex: + raise MemoryConnectorInitializationError("Failed to create Redis settings.", ex) from ex if vector_size <= 0: - raise ServiceInitializationError("Vector dimension must be a positive integer") + raise MemoryConnectorInitializationError("Vector dimension must be a positive integer") - self._database = redis.Redis.from_url(connection_string) + self._database = redis.Redis.from_url(redis_settings.connection_string.get_secret_value()) self._ft = self._database.ft self._query_dialect = query_dialect diff --git a/python/semantic_kernel/connectors/memory/redis/redis_settings.py b/python/semantic_kernel/connectors/memory/redis/redis_settings.py index aa7220fa2eb5..62ceba3dee2f 100644 --- a/python/semantic_kernel/connectors/memory/redis/redis_settings.py +++ b/python/semantic_kernel/connectors/memory/redis/redis_settings.py @@ -1,13 +1,15 @@ # Copyright (c) Microsoft. All rights reserved. +from typing import ClassVar + from pydantic import SecretStr -from semantic_kernel.connectors.memory.memory_settings_base import BaseModelSettings +from semantic_kernel.kernel_pydantic import KernelBaseSettings from semantic_kernel.utils.experimental_decorator import experimental_class @experimental_class -class RedisSettings(BaseModelSettings): +class RedisSettings(KernelBaseSettings): """Redis model settings. Args: @@ -15,9 +17,6 @@ class RedisSettings(BaseModelSettings): Redis connection string (Env var REDIS_CONNECTION_STRING) """ - connection_string: SecretStr | None = None - - class Config(BaseModelSettings.Config): - """Model configuration.""" + env_prefix: ClassVar[str] = "REDIS_" - env_prefix = "REDIS_" + connection_string: SecretStr diff --git a/python/semantic_kernel/connectors/memory/weaviate/weaviate_memory_store.py b/python/semantic_kernel/connectors/memory/weaviate/weaviate_memory_store.py index 1dd9a23b8dcb..19c5d403fcef 100644 --- a/python/semantic_kernel/connectors/memory/weaviate/weaviate_memory_store.py +++ b/python/semantic_kernel/connectors/memory/weaviate/weaviate_memory_store.py @@ -2,13 +2,11 @@ import asyncio import logging -from dataclasses import dataclass import numpy as np import weaviate -from pydantic import ValidationError -from semantic_kernel.connectors.memory.weaviate.weaviate_settings import WeaviateSettings +from semantic_kernel.exceptions.memory_connector_exceptions import MemoryConnectorInitializationError from semantic_kernel.memory.memory_record import MemoryRecord from semantic_kernel.memory.memory_store_base import MemoryStoreBase from semantic_kernel.utils.experimental_decorator import experimental_class @@ -65,13 +63,6 @@ ALL_PROPERTIES = [property["name"] for property in SCHEMA["properties"]] -@dataclass -class WeaviateConfig: - use_embed: bool = False - url: str = None - api_key: str = None - - @experimental_class class WeaviateMemoryStore(MemoryStoreBase): class FieldMapper: @@ -115,55 +106,43 @@ def remove_underscore_prefix(cls, sk_dict): """Used to initialize a MemoryRecord from a SK's dict of private attribute-values.""" return {key.lstrip("_"): value for key, value in sk_dict.items()} - def __init__(self, config: WeaviateConfig | None = None, env_file_path: str | None = None): + def __init__( + self, + url: str | None = None, + api_key: str | None = None, + use_embed: bool = False, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + ): """Initializes a new instance of the WeaviateMemoryStore. - Optional parameters: - - env_file_path (str | None): Whether to use the environment settings (.env) file. Defaults to False. - """ - # Initialize settings from environment variables or defaults defined in WeaviateSettings - weaviate_settings = None - try: - weaviate_settings = WeaviateSettings.create(env_file_path=env_file_path) - except ValidationError as e: - logger.warning(f"Failed to load WeaviateSettings pydantic settings: {e}") - - # Override settings with provided config if available - if config: - self.settings = self.merge_settings(weaviate_settings, config) - else: - self.settings = weaviate_settings - - self.settings.validate_settings() - self.client = self._initialize_client() - - def merge_settings(self, default_settings: WeaviateSettings, config: WeaviateConfig) -> WeaviateSettings: - """Merges default settings with configuration provided through WeaviateConfig. - - This function allows for manual overriding of settings from the config parameter. + Args: + url (str): The URL of the Weaviate instance. + api_key (str): The API key to use for authentication. + use_embed (bool): Whether to use the client embedding options. + env_file_path (str): Whether to use the environment settings (.env) file. + env_file_encoding (str): The encoding of the environment settings (.env) file. Defaults to 'utf-8'. """ - return WeaviateSettings( - url=config.url or (str(default_settings.url) if default_settings and default_settings.url else None), - api_key=config.api_key - or (default_settings.api_key.get_secret_value() if default_settings and default_settings.api_key else None), - use_embed=( - config.use_embed - if config.use_embed is not None - else (default_settings.use_embed if default_settings and default_settings.use_embed else False) - ), + from semantic_kernel.connectors.memory.weaviate.weaviate_settings import WeaviateSettings + + self.settings = WeaviateSettings.create( + url=url, + api_key=api_key, + use_embed=use_embed, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, ) - - def _initialize_client(self) -> weaviate.Client: - """Initializes the Weaviate client based on the combined settings.""" if self.settings.use_embed: - return weaviate.Client(embedded_options=weaviate.EmbeddedOptions()) - - if self.settings.api_key: - return weaviate.Client( - url=self.settings.url, auth_client_secret=weaviate.auth.AuthApiKey(api_key=self.settings.api_key) + self.client = weaviate.Client(embedded_options=weaviate.EmbeddedOptions()) + elif self.settings.api_key and self.settings.url: + self.client = weaviate.Client( + url=str(self.settings.url), + auth_client_secret=weaviate.auth.AuthApiKey(api_key=self.settings.api_key.get_secret_value()), ) - - return weaviate.Client(url=self.settings.url) + elif self.settings.url: + self.client = weaviate.Client(url=str(self.settings.url)) + else: + raise MemoryConnectorInitializationError("WeaviateMemoryStore requires a URL or API key, or to use embed.") async def create_collection(self, collection_name: str) -> None: """Creates a new collection in Weaviate.""" diff --git a/python/semantic_kernel/connectors/memory/weaviate/weaviate_settings.py b/python/semantic_kernel/connectors/memory/weaviate/weaviate_settings.py index 58e06ff341eb..350dda35d989 100644 --- a/python/semantic_kernel/connectors/memory/weaviate/weaviate_settings.py +++ b/python/semantic_kernel/connectors/memory/weaviate/weaviate_settings.py @@ -1,14 +1,15 @@ # Copyright (c) Microsoft. All rights reserved. -from pydantic import SecretStr +from typing import Any, ClassVar -from semantic_kernel.connectors.memory.memory_settings_base import BaseModelSettings -from semantic_kernel.kernel_pydantic import HttpsUrl +from pydantic import SecretStr, ValidationError, model_validator + +from semantic_kernel.kernel_pydantic import HttpsUrl, KernelBaseSettings from semantic_kernel.utils.experimental_decorator import experimental_class @experimental_class -class WeaviateSettings(BaseModelSettings): +class WeaviateSettings(KernelBaseSettings): """Weaviate model settings. Args: @@ -18,16 +19,16 @@ class WeaviateSettings(BaseModelSettings): (Env var WEAVIATE_USE_EMBED) """ + env_prefix: ClassVar[str] = "WEAVIATE_" + url: HttpsUrl | None = None api_key: SecretStr | None = None use_embed: bool = False - class Config(BaseModelSettings.Config): - """Configuration for the Weaviate model settings.""" - - env_prefix = "WEAVIATE_" - - def validate_settings(self): - """Validate the Weaviate settings.""" - if not self.use_embed and not self.url: - raise ValueError("Weaviate config must have either url or use_embed set") + @model_validator(mode="before") + @classmethod + def validate_settings(cls, data: dict[str, Any]) -> dict[str, Any]: + """Validate Weaviate settings.""" + if not data.get("use_embed") and not data.get("url"): + raise ValidationError("Weaviate config must have either url or use_embed set") + return data diff --git a/python/semantic_kernel/connectors/search_engine/bing_connector.py b/python/semantic_kernel/connectors/search_engine/bing_connector.py index 8b822d7d03f6..1949485356d8 100644 --- a/python/semantic_kernel/connectors/search_engine/bing_connector.py +++ b/python/semantic_kernel/connectors/search_engine/bing_connector.py @@ -4,7 +4,6 @@ import urllib import aiohttp -from pydantic import ValidationError from semantic_kernel.connectors.search_engine.bing_connector_settings import BingSettings from semantic_kernel.connectors.search_engine.connector import ConnectorBase @@ -16,9 +15,14 @@ class BingConnector(ConnectorBase): """A search engine connector that uses the Bing Search API to perform a web search.""" - _api_key: str + _settings: BingSettings - def __init__(self, api_key: str | None = None, env_file_path: str | None = None) -> None: + def __init__( + self, + api_key: str | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + ) -> None: """Initializes a new instance of the BingConnector class. Args: @@ -26,18 +30,13 @@ def __init__(self, api_key: str | None = None, env_file_path: str | None = None) the value in the env vars or .env file. env_file_path (str | None): The optional path to the .env file. If provided, the settings are read from this file path location. + env_file_encoding (str | None): The optional encoding of the .env file. """ - bing_settings = None - try: - bing_settings = BingSettings(env_file_path=env_file_path) - except ValidationError as e: - logger.warning(f"Failed to load the Bing pydantic settings: {e}.") - - self._api_key = api_key or ( - bing_settings.api_key.get_secret_value() if bing_settings and bing_settings.api_key else None + self.settings = BingSettings.create( + api_key=api_key, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, ) - if not self._api_key: - raise ValueError("API key cannot be 'None' or empty.") async def search(self, query: str, num_results: int = 1, offset: int = 0) -> list[str]: """Returns the search results of the query provided by pinging the Bing web search API.""" @@ -62,7 +61,7 @@ async def search(self, query: str, num_results: int = 1, offset: int = 0) -> lis logger.info(f"Sending GET request to {_request_url}") - headers = {"Ocp-Apim-Subscription-Key": self._api_key} + headers = {"Ocp-Apim-Subscription-Key": self._settings.api_key.get_secret_value()} async with aiohttp.ClientSession() as session: async with session.get(_request_url, headers=headers, raise_for_status=True) as response: diff --git a/python/semantic_kernel/connectors/search_engine/bing_connector_settings.py b/python/semantic_kernel/connectors/search_engine/bing_connector_settings.py index f4639d2c7572..dec16f4e27d3 100644 --- a/python/semantic_kernel/connectors/search_engine/bing_connector_settings.py +++ b/python/semantic_kernel/connectors/search_engine/bing_connector_settings.py @@ -1,10 +1,13 @@ # Copyright (c) Microsoft. All rights reserved. +from typing import ClassVar + from pydantic import SecretStr -from pydantic_settings import BaseSettings + +from semantic_kernel.kernel_pydantic import KernelBaseSettings -class BingSettings(BaseSettings): +class BingSettings(KernelBaseSettings): """Bing Connector settings. The settings are first loaded from environment variables with the prefix 'BING_'. If the @@ -17,23 +20,6 @@ class BingSettings(BaseSettings): """ - env_file_path: str | None = None - api_key: SecretStr | None = None + env_prefix: ClassVar[str] = "BING_" - class Config: - """Configuration for the Bing Connector settings.""" - - env_prefix = "BING_" - env_file = None - env_file_encoding = "utf-8" - extra = "ignore" - case_sensitive = False - - @classmethod - def create(cls, **kwargs): - """Create an instance of the Bing Connector settings.""" - if "env_file_path" in kwargs and kwargs["env_file_path"]: - cls.Config.env_file = kwargs["env_file_path"] - else: - cls.Config.env_file = None - return cls(**kwargs) + api_key: SecretStr | None = None diff --git a/python/semantic_kernel/core_plugins/sessions_python_tool/sessions_python_plugin.py b/python/semantic_kernel/core_plugins/sessions_python_tool/sessions_python_plugin.py index 1a8c7414968a..b1fcd1a68577 100644 --- a/python/semantic_kernel/core_plugins/sessions_python_tool/sessions_python_plugin.py +++ b/python/semantic_kernel/core_plugins/sessions_python_tool/sessions_python_plugin.py @@ -53,7 +53,9 @@ def __init__( http_client = httpx.AsyncClient() try: - aca_settings = ACASessionsSettings.create(env_file_path=env_file_path) + aca_settings = ACASessionsSettings.create( + env_file_path=env_file_path, pool_management_endpoint=pool_management_endpoint + ) except ValidationError as e: logger.error(f"Failed to load the ACASessionsSettings with message: {str(e)}") raise FunctionExecutionException(f"Failed to load the ACASessionsSettings with message: {str(e)}") from e diff --git a/python/semantic_kernel/core_plugins/sessions_python_tool/sessions_python_settings.py b/python/semantic_kernel/core_plugins/sessions_python_tool/sessions_python_settings.py index 190dc49db190..9d68c1892cdb 100644 --- a/python/semantic_kernel/core_plugins/sessions_python_tool/sessions_python_settings.py +++ b/python/semantic_kernel/core_plugins/sessions_python_tool/sessions_python_settings.py @@ -3,11 +3,11 @@ import uuid from enum import Enum +from typing import ClassVar from pydantic import Field -from pydantic_settings import BaseSettings -from semantic_kernel.kernel_pydantic import HttpsUrl, KernelBaseModel +from semantic_kernel.kernel_pydantic import HttpsUrl, KernelBaseModel, KernelBaseSettings class CodeInputType(str, Enum): @@ -34,7 +34,7 @@ class SessionsPythonSettings(KernelBaseModel): sanitize_input: bool | None = Field(default=True, alias="sanitizeInput") -class ACASessionsSettings(BaseSettings): +class ACASessionsSettings(KernelBaseSettings): """Azure Container Apps sessions settings. Required: @@ -42,23 +42,6 @@ class ACASessionsSettings(BaseSettings): (Env var ACA_POOL_MANAGEMENT_ENDPOINT) """ - env_file_path: str | None = None - pool_management_endpoint: HttpsUrl + env_prefix: ClassVar[str] = "ACA_" - class Config: - """Configuration for the Azure Container Apps sessions settings.""" - - env_prefix = "ACA_" - env_file = None - env_file_encoding = "utf-8" - extra = "ignore" - case_sensitive = False - - @classmethod - def create(cls, **kwargs): - """Create an instance of the Azure Container Apps sessions settings.""" - if "env_file_path" in kwargs and kwargs["env_file_path"]: - cls.Config.env_file = kwargs["env_file_path"] - else: - cls.Config.env_file = None - return cls(**kwargs) + pool_management_endpoint: HttpsUrl diff --git a/python/semantic_kernel/kernel_pydantic.py b/python/semantic_kernel/kernel_pydantic.py index 616dead7bc8b..04f48f972b8f 100644 --- a/python/semantic_kernel/kernel_pydantic.py +++ b/python/semantic_kernel/kernel_pydantic.py @@ -1,10 +1,11 @@ # Copyright (c) Microsoft. All rights reserved. -from typing import Annotated +from typing import Annotated, Any, ClassVar, TypeVar from pydantic import BaseModel, ConfigDict, UrlConstraints from pydantic.networks import Url +from pydantic_settings import BaseSettings, SettingsConfigDict HttpsUrl = Annotated[Url, UrlConstraints(max_length=2083, allowed_schemes=["https"])] @@ -13,3 +14,41 @@ class KernelBaseModel(BaseModel): """Base class for all pydantic models in the SK.""" model_config = ConfigDict(populate_by_name=True, arbitrary_types_allowed=True, validate_assignment=True) + + +T = TypeVar("T", bound="KernelBaseSettings") + + +class KernelBaseSettings(BaseSettings): + """Base class for all settings classes in the SK. + + A subclass creates it's fields and overrides the env_prefix class variable + with the prefix for the environment variables. + + In the case where a value is specified for the same Settings field in multiple ways, + the selected value is determined as follows (in descending order of priority): + - Arguments passed to the Settings class initialiser. + - Environment variables, e.g. my_prefix_special_function as described above. + - Variables loaded from a dotenv (.env) file. + - Variables loaded from the secrets directory. + - The default field values for the Settings model. + """ + + env_prefix: ClassVar[str] = "" + env_file_path: str | None = None + env_file_encoding: str = "utf-8" + + model_config = SettingsConfigDict( + extra="ignore", + case_sensitive=False, + ) + + @classmethod + def create(cls: type["T"], **data: Any) -> "T": + """Update the model_config with the prefix.""" + cls.model_config["env_prefix"] = cls.env_prefix + if data.get("env_file_path"): + cls.model_config["env_file"] = data["env_file_path"] + cls.model_config["env_file_encoding"] = data.get("env_file_encoding", "utf-8") + data = {k: v for k, v in data.items() if v is not None} + return cls(**data) diff --git a/python/semantic_kernel/prompt_template/input_variable.py b/python/semantic_kernel/prompt_template/input_variable.py index eefeb7e3e917..10978fd3fa12 100644 --- a/python/semantic_kernel/prompt_template/input_variable.py +++ b/python/semantic_kernel/prompt_template/input_variable.py @@ -14,9 +14,8 @@ class InputVariable(KernelBaseModel): default: The default value of the input variable. is_required: Whether the input variable is required. json_schema: The JSON schema for the input variable. - allow_dangerously_set_content (bool = False): Allow content without encoding throughout, this overrides - the same settings in the prompt template config and input variables. - This reverts the behavior to unencoded input. + allow_dangerously_set_content: Allow content without encoding, this controls + if this variable is encoded before use, default is False. """ name: str diff --git a/python/tests/integration/connectors/memory/test_astradb.py b/python/tests/integration/connectors/memory/test_astradb.py index 01b742fa82f4..9ea3eade8669 100644 --- a/python/tests/integration/connectors/memory/test_astradb.py +++ b/python/tests/integration/connectors/memory/test_astradb.py @@ -38,7 +38,7 @@ def slow_down_tests(): @pytest.fixture(scope="session") def get_astradb_config(): try: - astradb_settings = AstraDBSettings() + astradb_settings = AstraDBSettings.create() app_token = astradb_settings.app_token.get_secret_value() db_id = astradb_settings.db_id region = astradb_settings.region diff --git a/python/tests/integration/connectors/memory/test_azure_cognitive_search.py b/python/tests/integration/connectors/memory/test_azure_cognitive_search.py index ac3da613897d..7f6da78506ff 100644 --- a/python/tests/integration/connectors/memory/test_azure_cognitive_search.py +++ b/python/tests/integration/connectors/memory/test_azure_cognitive_search.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft. All rights reserved. import asyncio -import time from random import randint import numpy as np @@ -48,7 +47,7 @@ async def test_collections(): assert collection in many await memory_store.delete_collection(collection) - time.sleep(1) + await asyncio.sleep(1) assert not await memory_store.does_collection_exist(collection) @@ -57,7 +56,7 @@ async def test_upsert(): collection = f"int-tests-{randint(1000, 9999)}" async with AzureCognitiveSearchMemoryStore(vector_size=4) as memory_store: await memory_store.create_collection(collection) - time.sleep(1) + await asyncio.sleep(1) try: assert await memory_store.does_collection_exist(collection) rec = MemoryRecord( @@ -70,7 +69,7 @@ async def test_upsert(): embedding=np.array([0.2, 0.1, 0.2, 0.7]), ) id = await memory_store.upsert(collection, rec) - time.sleep(1) + await asyncio.sleep(1) many = await memory_store.get_batch(collection, [id]) one = await memory_store.get(collection, id) @@ -90,7 +89,7 @@ async def test_record_not_found(): collection = f"int-tests-{randint(1000, 9999)}" async with AzureCognitiveSearchMemoryStore(vector_size=4) as memory_store: await memory_store.create_collection(collection) - time.sleep(1) + await asyncio.sleep(1) try: assert await memory_store.does_collection_exist(collection) rec = MemoryRecord( @@ -109,7 +108,7 @@ async def test_record_not_found(): try: await memory_store.remove(collection, id) - time.sleep(1) + await asyncio.sleep(1) # KeyError exception should occur await memory_store.get(collection, id) diff --git a/python/tests/integration/connectors/memory/test_azure_cosmosdb_memory_store.py b/python/tests/integration/connectors/memory/test_azure_cosmosdb_memory_store.py index 3e2a5574f2f9..bf75b5f591a9 100644 --- a/python/tests/integration/connectors/memory/test_azure_cosmosdb_memory_store.py +++ b/python/tests/integration/connectors/memory/test_azure_cosmosdb_memory_store.py @@ -5,7 +5,7 @@ import numpy as np import pytest -from semantic_kernel.connectors.memory.azure_cosmosdb.cosmosdb_utils import ( +from semantic_kernel.connectors.memory.azure_cosmosdb.utils import ( CosmosDBSimilarityType, CosmosDBVectorSearchType, ) diff --git a/python/tests/integration/connectors/memory/test_azure_cosmosdb_no_sql_memory_store.py b/python/tests/integration/connectors/memory/test_azure_cosmosdb_no_sql_memory_store.py index e676cac99717..9c1f93b3c4e5 100644 --- a/python/tests/integration/connectors/memory/test_azure_cosmosdb_no_sql_memory_store.py +++ b/python/tests/integration/connectors/memory/test_azure_cosmosdb_no_sql_memory_store.py @@ -2,19 +2,20 @@ import numpy as np import pytest -from azure.cosmos import PartitionKey -from azure.cosmos.aio import CosmosClient from semantic_kernel.memory.memory_record import MemoryRecord from semantic_kernel.memory.memory_store_base import MemoryStoreBase try: + from azure.cosmos import PartitionKey + from azure.cosmos.aio import CosmosClient + from semantic_kernel.connectors.memory.azure_cosmosdb_no_sql.azure_cosmosdb_no_sql_memory_store import ( AzureCosmosDBNoSQLMemoryStore, ) azure_cosmosdb_no_sql_memory_store_installed = True -except AssertionError: +except ImportError: azure_cosmosdb_no_sql_memory_store_installed = False pytest_mark = pytest.mark.skipif( diff --git a/python/tests/integration/connectors/memory/test_mongodb_atlas.py b/python/tests/integration/connectors/memory/test_mongodb_atlas.py index 8d45666de3f6..74633220c27a 100644 --- a/python/tests/integration/connectors/memory/test_mongodb_atlas.py +++ b/python/tests/integration/connectors/memory/test_mongodb_atlas.py @@ -1,24 +1,20 @@ # Copyright (c) Microsoft. All rights reserved. + +import asyncio import random -import time import numpy as np import pytest import pytest_asyncio -from pydantic import ValidationError -from pymongo import errors -from semantic_kernel.connectors.memory.mongodb_atlas.mongodb_atlas_memory_store import ( - MongoDBAtlasMemoryStore, -) -from semantic_kernel.connectors.memory.mongodb_atlas.mongodb_atlas_settings import ( - MongoDBAtlasSettings, -) +from semantic_kernel.connectors.memory.mongodb_atlas.mongodb_atlas_memory_store import MongoDBAtlasMemoryStore +from semantic_kernel.exceptions import MemoryConnectorInitializationError from semantic_kernel.memory.memory_record import MemoryRecord mongodb_atlas_installed: bool try: import motor # noqa: F401 + from pymongo import errors mongodb_atlas_installed = True except ImportError: @@ -67,18 +63,18 @@ def test_collection(): return f"AVSTest-{random.randint(0,9999)}" -@pytest.fixture(scope="session") -def connection_string(): +@pytest.fixture +def memory(): try: - mongodb_atlas_settings = MongoDBAtlasSettings.create() - return mongodb_atlas_settings.api_key.get_secret_value() - except ValidationError: + return MongoDBAtlasMemoryStore(database_name="pyMSKTest") + except MemoryConnectorInitializationError: pytest.skip("MongoDB Atlas connection string not found in env vars.") @pytest_asyncio.fixture -async def vector_search_store(): - async with MongoDBAtlasMemoryStore(connection_string, database_name="pyMSKTest") as memory: +async def vector_search_store(memory): + await memory.__aenter__() + try: # Delete all collections before and after for cname in await memory.get_collections(): await memory.delete_collection(cname) @@ -98,7 +94,7 @@ async def _patch(collection_name): # of a previous index not completing teardown if e.code != DUPLICATE_INDEX_ERR_CODE: raise - time.sleep(1) + await asyncio.sleep(1) return _patch @@ -110,23 +106,32 @@ async def _patch(collection_name): pass for cname in await memory.get_collections(): await memory.delete_collection(cname) + except Exception: + pass + finally: + await memory.__aexit__(None, None, None) @pytest_asyncio.fixture -async def nearest_match_store(): +async def nearest_match_store(memory): """Fixture for read only vector store; the URI for test needs atlas configured""" - async with MongoDBAtlasMemoryStore(connection_string, database_name="pyMSKTest") as memory: + await memory.__aenter__() + try: if not await memory.does_collection_exist("nearestSearch"): pytest.skip( reason="db: readOnly collection: nearestSearch not found, " + "please ensure your Atlas Test Cluster has this collection configured" ) yield memory + except Exception: + pass + finally: + await memory.__aexit__(None, None, None) @pytest.mark.asyncio -async def test_constructor(vector_search_store): - assert isinstance(vector_search_store, MongoDBAtlasMemoryStore) +async def test_constructor(memory): + assert isinstance(memory, MongoDBAtlasMemoryStore) @pytest.mark.asyncio diff --git a/python/tests/integration/connectors/memory/test_weaviate_memory_store.py b/python/tests/integration/connectors/memory/test_weaviate_memory_store.py index e51b70ab66a3..e69f723a1972 100644 --- a/python/tests/integration/connectors/memory/test_weaviate_memory_store.py +++ b/python/tests/integration/connectors/memory/test_weaviate_memory_store.py @@ -7,7 +7,7 @@ import numpy.testing as npt import pytest -from semantic_kernel.connectors.memory.weaviate.weaviate_memory_store import WeaviateConfig, WeaviateMemoryStore +from semantic_kernel.connectors.memory.weaviate.weaviate_memory_store import WeaviateMemoryStore from semantic_kernel.memory.memory_record import MemoryRecord if not sys.platform.startswith("linux"): @@ -76,10 +76,9 @@ def memory_store(): max_attempts = 5 # the number of retry attempts delay = 3 # delay in seconds between each attempt - config = WeaviateConfig(use_embed=True) for attempt in range(max_attempts): try: - store = WeaviateMemoryStore(config=config) + store = WeaviateMemoryStore(use_embed=True) store.client.schema.delete_all() except Exception: if attempt < max_attempts - 1: # it's not the final attempt @@ -116,8 +115,7 @@ def memory_store_with_collection(memory_store, event_loop, documents): def test_embedded_weaviate(): - config = WeaviateConfig(use_embed=True) - memory_store = WeaviateMemoryStore(config=config) + memory_store = WeaviateMemoryStore(use_embed=True) assert memory_store.client._connection.embedded_db diff --git a/python/tests/unit/connectors/google_palm/services/test_palm_chat_completion.py b/python/tests/unit/connectors/google_palm/services/test_palm_chat_completion.py index 895c402f257f..5380a176a340 100644 --- a/python/tests/unit/connectors/google_palm/services/test_palm_chat_completion.py +++ b/python/tests/unit/connectors/google_palm/services/test_palm_chat_completion.py @@ -5,19 +5,17 @@ import pytest from google.generativeai.types import ChatResponse, MessageDict -from pydantic import ValidationError from semantic_kernel.connectors.ai.google_palm import GooglePalmChatPromptExecutionSettings from semantic_kernel.connectors.ai.google_palm.services.gp_chat_completion import GooglePalmChatCompletion from semantic_kernel.contents.chat_history import ChatHistory +from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError def test_google_palm_chat_completion_init(google_palm_unit_test_env) -> None: ai_model_id = "test_model_id" - gp_chat_completion = GooglePalmChatCompletion( - ai_model_id=ai_model_id, - ) + gp_chat_completion = GooglePalmChatCompletion(ai_model_id=ai_model_id) assert gp_chat_completion.ai_model_id == ai_model_id assert gp_chat_completion.api_key == google_palm_unit_test_env["GOOGLE_PALM_API_KEY"] @@ -28,7 +26,7 @@ def test_google_palm_chat_completion_init(google_palm_unit_test_env) -> None: def test_google_palm_chat_completion_init_with_empty_api_key(google_palm_unit_test_env) -> None: ai_model_id = "test_model_id" - with pytest.raises(ValidationError): + with pytest.raises(ServiceInitializationError): GooglePalmChatCompletion( ai_model_id=ai_model_id, ) diff --git a/python/tests/unit/connectors/google_palm/services/test_palm_text_completion.py b/python/tests/unit/connectors/google_palm/services/test_palm_text_completion.py index 935527551ea6..91596bf54427 100644 --- a/python/tests/unit/connectors/google_palm/services/test_palm_text_completion.py +++ b/python/tests/unit/connectors/google_palm/services/test_palm_text_completion.py @@ -4,10 +4,10 @@ import pytest from google.generativeai.types import Completion from google.generativeai.types.text_types import TextCompletion -from pydantic import ValidationError from semantic_kernel.connectors.ai.google_palm import GooglePalmTextPromptExecutionSettings from semantic_kernel.connectors.ai.google_palm.services.gp_text_completion import GooglePalmTextCompletion +from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError def test_google_palm_text_completion_init(google_palm_unit_test_env) -> None: @@ -27,7 +27,7 @@ def test_google_palm_text_completion_init(google_palm_unit_test_env) -> None: def test_google_palm_text_completion_init_with_empty_api_key(google_palm_unit_test_env) -> None: ai_model_id = "test_model_id" - with pytest.raises(ValidationError): + with pytest.raises(ServiceInitializationError): GooglePalmTextCompletion( ai_model_id=ai_model_id, ) diff --git a/python/tests/unit/connectors/google_palm/services/test_palm_text_embedding.py b/python/tests/unit/connectors/google_palm/services/test_palm_text_embedding.py index 42a022d22944..aa1b61ee09e4 100644 --- a/python/tests/unit/connectors/google_palm/services/test_palm_text_embedding.py +++ b/python/tests/unit/connectors/google_palm/services/test_palm_text_embedding.py @@ -3,11 +3,11 @@ from unittest.mock import MagicMock, patch import pytest -from pydantic import ValidationError from semantic_kernel.connectors.ai.google_palm.services.gp_text_embedding import ( GooglePalmTextEmbedding, ) +from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError def test_google_palm_text_embedding_init(google_palm_unit_test_env) -> None: @@ -27,7 +27,7 @@ def test_google_palm_text_embedding_init(google_palm_unit_test_env) -> None: def test_google_palm_text_embedding_init_with_empty_api_key(google_palm_unit_test_env) -> None: ai_model_id = "test_model_id" - with pytest.raises(ValidationError): + with pytest.raises(ServiceInitializationError): GooglePalmTextEmbedding( ai_model_id=ai_model_id, ) diff --git a/python/tests/unit/connectors/open_ai/services/test_azure_chat_completion.py b/python/tests/unit/connectors/open_ai/services/test_azure_chat_completion.py index fd81fa3c2fe6..425b0ddba0a4 100644 --- a/python/tests/unit/connectors/open_ai/services/test_azure_chat_completion.py +++ b/python/tests/unit/connectors/open_ai/services/test_azure_chat_completion.py @@ -8,7 +8,6 @@ from httpx import Request, Response from openai import AsyncAzureOpenAI from openai.resources.chat.completions import AsyncCompletions as AsyncChatCompletions -from pydantic import ValidationError from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase from semantic_kernel.connectors.ai.function_call_behavior import FunctionCallBehavior @@ -58,7 +57,7 @@ def test_azure_chat_completion_init_base_url(azure_openai_unit_test_env) -> None @pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"]], indirect=True) def test_azure_chat_completion_init_with_empty_deployment_name(azure_openai_unit_test_env) -> None: - with pytest.raises(ValidationError): + with pytest.raises(ServiceInitializationError): AzureChatCompletion() diff --git a/python/tests/unit/connectors/open_ai/services/test_azure_text_completion.py b/python/tests/unit/connectors/open_ai/services/test_azure_text_completion.py index 5fab03e92a20..137b7fa50439 100644 --- a/python/tests/unit/connectors/open_ai/services/test_azure_text_completion.py +++ b/python/tests/unit/connectors/open_ai/services/test_azure_text_completion.py @@ -5,7 +5,6 @@ import pytest from openai import AsyncAzureOpenAI from openai.resources.completions import AsyncCompletions -from pydantic import ValidationError from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.open_ai_prompt_execution_settings import ( OpenAITextPromptExecutionSettings, @@ -45,7 +44,7 @@ def test_azure_text_completion_init_with_custom_header(azure_openai_unit_test_en @pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_TEXT_DEPLOYMENT_NAME"]], indirect=True) def test_azure_text_completion_init_with_empty_deployment_name(azure_openai_unit_test_env) -> None: - with pytest.raises(ValidationError): + with pytest.raises(ServiceInitializationError): AzureTextCompletion() diff --git a/python/tests/unit/connectors/open_ai/services/test_azure_text_embedding.py b/python/tests/unit/connectors/open_ai/services/test_azure_text_embedding.py index 0c1853324d5c..77bd4ec55004 100644 --- a/python/tests/unit/connectors/open_ai/services/test_azure_text_embedding.py +++ b/python/tests/unit/connectors/open_ai/services/test_azure_text_embedding.py @@ -5,7 +5,6 @@ import pytest from openai import AsyncAzureOpenAI from openai.resources.embeddings import AsyncEmbeddings -from pydantic import ValidationError from semantic_kernel.connectors.ai.embeddings.embedding_generator_base import EmbeddingGeneratorBase from semantic_kernel.connectors.ai.open_ai.services.azure_text_embedding import AzureTextEmbedding @@ -24,7 +23,7 @@ def test_azure_text_embedding_init(azure_openai_unit_test_env) -> None: @pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME"]], indirect=True) def test_azure_text_embedding_init_with_empty_deployment_name(azure_openai_unit_test_env) -> None: - with pytest.raises(ValidationError): + with pytest.raises(ServiceInitializationError): AzureTextEmbedding()